diff --git a/src/app/(web)/(auth)/login/page.tsx b/src/app/(web)/(auth)/login/page.tsx
index 3241bfa1..515fdad2 100644
--- a/src/app/(web)/(auth)/login/page.tsx
+++ b/src/app/(web)/(auth)/login/page.tsx
@@ -1,3 +1,85 @@
+'use client';
+import Link from 'next/link';
+import { loginAction } from '@/lib/auth/auth-service';
+import { useActionState, useEffect } from 'react';
+
+export interface LoginState {
+ success: boolean;
+ errors: {
+ email?: string[];
+ password?: string[];
+ };
+ message: string;
+}
+
+const initialState: LoginState = {
+ success: true,
+ errors: {},
+ message: '',
+};
+
export default function LoginPage() {
- return
login
;
+ const [state, formAction] = useActionState(loginAction, initialState);
+
+ useEffect(() => {
+ if (state && !state.success && Object.keys(state.errors).length === 0 && state.message) {
+ alert(state.message);
+ }
+ }, [state]);
+
+ return (
+
+ );
}
diff --git a/src/app/(web)/(auth)/signup/page.tsx b/src/app/(web)/(auth)/signup/page.tsx
index e523d7d2..b39f9c55 100644
--- a/src/app/(web)/(auth)/signup/page.tsx
+++ b/src/app/(web)/(auth)/signup/page.tsx
@@ -1,3 +1,102 @@
+'use client';
+import Link from 'next/link';
+import { signup } from '@/lib/auth/auth-service';
+import { useFormState } from 'react-dom';
+import { useEffect } from 'react';
+
+export interface SignupState {
+ success: boolean;
+ errors: {
+ name?: string[];
+ email?: string[];
+ password?: string[];
+ };
+ message: string;
+}
+
+const initialState: SignupState = {
+ success: true,
+ errors: {},
+ message: '',
+};
+
export default function SignupPage() {
- return signup
;
+ const [state, formAction] = useFormState(signup, initialState);
+
+ useEffect(() => {
+ if (state && !state.success && Object.keys(state.errors).length === 0 && state.message) {
+ alert(state.message);
+ }
+ }, [state]);
+
+ return (
+
+
+
Create an account
+
+
+
+
+ );
}
diff --git a/src/app/(web)/(crm)/dashboard/page.tsx b/src/app/(web)/(crm)/dashboard/page.tsx
new file mode 100644
index 00000000..f3907fdc
--- /dev/null
+++ b/src/app/(web)/(crm)/dashboard/page.tsx
@@ -0,0 +1,7 @@
+import { getCurrentUser } from '@/lib/auth/auth-service';
+
+export default async function DashboardPage() {
+ const name = await getCurrentUser();
+
+ return {name?.name}
;
+}
diff --git a/src/app/(web)/dashboard/page.tsx b/src/app/(web)/dashboard/page.tsx
deleted file mode 100644
index 7057af71..00000000
--- a/src/app/(web)/dashboard/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function DashboardPage() {
- return ;
-}
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
deleted file mode 100644
index 942b0f06..00000000
--- a/src/app/api/auth/login/route.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { type NextRequest } from 'next/server';
-import { sargeApiError, sargeApiResponse } from '@/lib/responses';
-import { login } from '@/lib/auth/auth-service';
-import { InvalidInputError } from '@/lib/schemas/errors';
-
-export async function POST(request: NextRequest) {
- try {
- const body = await request.json();
-
- if (!body.email || !body.password) {
- return sargeApiError('Email and password are required', 400);
- }
-
- const user = await login({
- email: body.email,
- password: body.password,
- });
-
- return sargeApiResponse(user, 200);
- } catch (error) {
- if (error instanceof InvalidInputError) {
- return sargeApiError(error.message, 400);
- }
-
- const message = error instanceof Error ? error.message : String(error);
-
- if (message.includes('Invalid credentials')) {
- return sargeApiError('Invalid email or password', 401);
- }
-
- if (message.includes('Login implementation needed')) {
- return sargeApiError('Authentication not yet configured', 501);
- }
-
- return sargeApiError(message, 500);
- }
-}
diff --git a/src/app/globals.css b/src/app/globals.css
index 3af1742e..9e158c36 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,31 @@
@import 'tailwindcss';
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --background: #171717;
+ --foreground: #ffffff;
+ --sarge-purple: #5d5bf7;
+ --sarge-lightgrey: #f3f3f5;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
+ --color-s-purple: var(--sarge-purple);
+ --color-s-lightgrey: var(--sarge-lightgrey);
+ --font-sans: 'Inter', 'system-ui';
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --background: #ffffff;
+ --foreground: #0a0a0a;
}
}
body {
background: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: var(--font-sans);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a1e14ec4..801d84ab 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
+ title: 'Sarge',
+ description: 'Standardized Assessment Review, Grading, & Evaluation',
};
export default function RootLayout({
diff --git a/src/lib/auth/auth-client.tsx b/src/lib/auth/auth-client.tsx
new file mode 100644
index 00000000..871bec3f
--- /dev/null
+++ b/src/lib/auth/auth-client.tsx
@@ -0,0 +1,3 @@
+'use client';
+
+// We need to create a React context to get the current user on the frontend in the authenticated pages
diff --git a/src/lib/auth/auth-service.ts b/src/lib/auth/auth-service.ts
index b7f37d3a..0191190c 100644
--- a/src/lib/auth/auth-service.ts
+++ b/src/lib/auth/auth-service.ts
@@ -1,4 +1,4 @@
-import 'server-only';
+'use server';
import { createSession, deleteSession, verifySession } from './auth';
import { redirect } from 'next/navigation';
@@ -7,13 +7,84 @@ import { prisma } from '../prisma';
import bcrypt from 'bcrypt';
import { AuthorizationError } from '../schemas/errors';
import { type User } from '@/generated/prisma';
+import { createUserSchema, loginUserSchema } from '../schemas/user.schema';
+import { type SignupState } from '@/app/(web)/(auth)/signup/page';
+import { type LoginState } from '@/app/(web)/(auth)/login/page';
-export interface LoginCredentials {
- email: string;
- password: string;
+export async function signup(
+ prevState: SignupState | null,
+ formData: FormData
+): Promise {
+ const parsedCreds = createUserSchema.safeParse({
+ name: formData.get('fullName'),
+ email: formData.get('email'),
+ password: formData.get('password'),
+ });
+
+ if (!parsedCreds.success) {
+ return {
+ success: false,
+ errors: parsedCreds.error.flatten().fieldErrors,
+ message: 'Please check your input and try again.',
+ };
+ }
+
+ try {
+ const hashedPassword = await bcrypt.hash(parsedCreds.data.password, 10);
+
+ const user = await prisma.user.create({
+ data: {
+ name: parsedCreds.data.name,
+ email: parsedCreds.data.email,
+ hashedPassword,
+ },
+ });
+
+ await createSession({ userId: user.id, email: user.email });
+ } catch (error) {
+ console.error('Signup error:', error);
+ return {
+ success: false,
+ errors: {},
+ message: 'There was an error creating your account. Please try again.',
+ };
+ }
+
+ redirect('/dashboard');
+}
+
+export async function loginAction(
+ prevState: LoginState | null,
+ formData: FormData
+): Promise {
+ const parsedCreds = loginUserSchema.safeParse({
+ email: formData.get('email'),
+ password: formData.get('password'),
+ });
+
+ if (!parsedCreds.success) {
+ return {
+ success: false,
+ errors: parsedCreds.error.flatten().fieldErrors,
+ message: 'Please check your input and try again.',
+ };
+ }
+
+ try {
+ await login(parsedCreds.data);
+ } catch (error) {
+ console.error('Login error:', error);
+ return {
+ success: false,
+ errors: {},
+ message: 'Invalid email or password. Please try again.',
+ };
+ }
+
+ redirect('/dashboard');
}
-export async function login(_credentials: LoginCredentials): Promise {
+export async function login(_credentials: { email: string; password: string }): Promise {
const user = await prisma.user.findUnique({
where: {
email: _credentials.email,
diff --git a/src/lib/controllers/user.controller.ts b/src/lib/controllers/user.controller.ts
index 08e7cc8a..ef0be618 100644
--- a/src/lib/controllers/user.controller.ts
+++ b/src/lib/controllers/user.controller.ts
@@ -15,7 +15,11 @@ class UserController {
try {
const validatedUser = createUserSchema.parse(user);
return await prisma.user.create({
- data: validatedUser,
+ data: {
+ name: validatedUser.name,
+ email: validatedUser.email,
+ hashedPassword: validatedUser.password,
+ },
});
} catch (error) {
if (error instanceof z.ZodError) {
diff --git a/src/lib/schemas/user.schema.ts b/src/lib/schemas/user.schema.ts
index 9b91d669..d08ac838 100644
--- a/src/lib/schemas/user.schema.ts
+++ b/src/lib/schemas/user.schema.ts
@@ -17,7 +17,12 @@ export const createUserSchema = z.object({
.toLowerCase()
.trim()
.max(255, 'Email must be less than 255 characters'),
- hashedPassword: z.string(),
+ password: z.string().min(8),
+});
+
+export const loginUserSchema = z.object({
+ email: createUserSchema.shape.email,
+ password: z.string().min(8, 'Password is required'),
});
export const UserSchema = z.object({
@@ -30,3 +35,5 @@ export const UserSchema = z.object({
export type UserDTO = z.infer;
export type CreateUserDTO = z.infer;
+
+export type LoginUserDTO = z.infer;
diff --git a/src/middleware.ts b/src/middleware.ts
index 7c8d1249..185e573b 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -17,6 +17,7 @@ export async function middleware(request: NextRequest) {
await jwtVerify(sessionCookie, secret, {
issuer: 'sargenu',
});
+
isAuthenticated = true;
} catch {
isAuthenticated = false;