Skip to content

Commit a178364

Browse files
committed
feat: create auth
1 parent 43007ce commit a178364

12 files changed

Lines changed: 329 additions & 32 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
},
2222
"dependencies": {
2323
"@prisma/client": "6.15.0",
24+
"bcrypt": "^6.0.0",
25+
"jose": "^6.1.0",
2426
"next": "15.5.2",
2527
"prisma": "^6.15.0",
2628
"react": "19.1.0",
@@ -30,6 +32,7 @@
3032
"devDependencies": {
3133
"@eslint/eslintrc": "^3",
3234
"@tailwindcss/postcss": "^4",
35+
"@types/bcrypt": "^6.0.0",
3336
"@types/node": "^22",
3437
"@types/react": "^19",
3538
"@types/react-dom": "^19",

pnpm-lock.yaml

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `hashedPassword` to the `User` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT NOT NULL;

prisma/schema.prisma

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,32 @@ datasource db {
99
}
1010

1111
model Organization {
12-
id String @id @unique @default(uuid())
13-
name String
14-
createdAt DateTime @default(now())
15-
updatedAt DateTime @updatedAt
16-
createdById String @unique
17-
createdBy User @relation("CreatedOrganization", fields: [createdById], references: [id])
18-
users User[] @relation("UserOrganization")
12+
id String @id @unique @default(uuid())
13+
name String
14+
createdAt DateTime @default(now())
15+
updatedAt DateTime @updatedAt
16+
createdById String @unique
17+
createdBy User @relation("CreatedOrganization", fields: [createdById], references: [id])
18+
users User[] @relation("UserOrganization")
1919
positions Position[]
2020
candidates Candidate[]
2121
AssessmentTemplate AssessmentTemplate[]
2222
TaskTemplate TaskTemplate[]
2323
}
2424

2525
model User {
26-
id String @id @unique @default(uuid())
27-
name String
28-
email String @unique
29-
orgId String?
30-
createdAt DateTime @default(now())
31-
updatedAt DateTime @updatedAt
32-
organization Organization? @relation("UserOrganization", fields: [orgId], references: [id])
26+
id String @id @unique @default(uuid())
27+
name String
28+
email String @unique
29+
orgId String?
30+
hashedPassword String
31+
createdAt DateTime @default(now())
32+
updatedAt DateTime @updatedAt
33+
organization Organization? @relation("UserOrganization", fields: [orgId], references: [id])
3334
createdOrganization Organization? @relation("CreatedOrganization")
34-
userRoles UserRole[]
35-
Review Review[]
36-
Comment Comment[]
35+
userRoles UserRole[]
36+
Review Review[]
37+
Comment Comment[]
3738
}
3839

3940
model Role {

src/app/api/auth/login/route.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { type NextRequest } from 'next/server';
2+
import { sargeApiError, sargeApiResponse } from '@/lib/responses';
3+
import { login } from '@/lib/auth/auth-service';
4+
import { InvalidInputError } from '@/lib/schemas/errors';
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const body = await request.json();
9+
10+
if (!body.email || !body.password) {
11+
return sargeApiError('Email and password are required', 400);
12+
}
13+
14+
const user = await login({
15+
email: body.email,
16+
password: body.password,
17+
});
18+
19+
return sargeApiResponse(user, 200);
20+
} catch (error) {
21+
if (error instanceof InvalidInputError) {
22+
return sargeApiError(error.message, 400);
23+
}
24+
25+
const message = error instanceof Error ? error.message : String(error);
26+
27+
if (message.includes('Invalid credentials')) {
28+
return sargeApiError('Invalid email or password', 401);
29+
}
30+
31+
if (message.includes('Login implementation needed')) {
32+
return sargeApiError('Authentication not yet configured', 501);
33+
}
34+
35+
return sargeApiError(message, 500);
36+
}
37+
}

src/app/api/auth/logout/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type NextRequest } from 'next/server';
2+
import { sargeApiResponse } from '@/lib/responses';
3+
import { logout } from '@/lib/auth/auth-service';
4+
5+
export async function POST(_request: NextRequest) {
6+
try {
7+
await logout();
8+
return sargeApiResponse({ message: 'Logged out successfully' }, 200);
9+
} catch (error) {
10+
// Even if logout fails, we want to clear the session
11+
// This is a security best practice
12+
const message = error instanceof Error ? error.message : String(error);
13+
return sargeApiResponse({ message: 'Logged out', error: message }, 200);
14+
}
15+
}

src/app/api/auth/me/route.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type NextRequest } from 'next/server';
2+
import { sargeApiResponse, sargeApiError } from '@/lib/responses';
3+
import { getCurrentUser } from '@/lib/auth/auth-service';
4+
5+
export async function GET(_request: NextRequest) {
6+
try {
7+
const user = await getCurrentUser();
8+
9+
if (!user) {
10+
return sargeApiError('Not authenticated', 401);
11+
}
12+
13+
return sargeApiResponse(user, 200);
14+
} catch (error) {
15+
const message = error instanceof Error ? error.message : String(error);
16+
return sargeApiError(message, 500);
17+
}
18+
}
Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import { requireAuth } from '@/lib/auth/auth';
12
import { userController } from '@/lib/controllers/user.controller';
23
import { sargeApiError, sargeApiResponse } from '@/lib/responses';
34
import { UserNotFoundError } from '@/lib/schemas/user.schema';
45
import { type NextRequest } from 'next/server';
56

67
export async function DELETE(
78
_request: NextRequest,
8-
{ params }: { params: Promise<{ userId: string }> }
9+
{ params }: { params: Promise<{ id: string }> }
910
) {
1011
try {
11-
const userId = (await params).userId;
12-
const user = await userController.delete(userId);
12+
const session = await requireAuth();
13+
14+
const id = (await params).id;
15+
const user = await userController.delete(id);
1316
return sargeApiResponse(user, 200);
1417
} catch (error) {
1518
if (error instanceof UserNotFoundError) {
@@ -21,13 +24,10 @@ export async function DELETE(
2124
}
2225
}
2326

24-
export async function GET(
25-
_request: NextRequest,
26-
{ params }: { params: Promise<{ userId: string }> }
27-
) {
27+
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
2828
try {
29-
const userId = (await params).userId;
30-
const user = await userController.get(userId);
29+
const id = (await params).id;
30+
const user = await userController.get(id);
3131
return sargeApiResponse(user, 200);
3232
} catch (error) {
3333
if (error instanceof UserNotFoundError) {
@@ -39,14 +39,11 @@ export async function GET(
3939
}
4040
}
4141

42-
export async function PUT(
43-
request: NextRequest,
44-
{ params }: { params: Promise<{ userId: string }> }
45-
) {
42+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
4643
try {
47-
const userId = (await params).userId;
44+
const id = (await params).id;
4845
const body = await request.json();
49-
const user = await userController.update(userId, body);
46+
const user = await userController.update(id, body);
5047
return sargeApiResponse(user, 200);
5148
} catch (error) {
5249
if (error instanceof UserNotFoundError) {

src/lib/auth/auth-service.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'server-only';
2+
3+
import { createSession, deleteSession, verifySession } from './auth';
4+
import { redirect } from 'next/navigation';
5+
import { userController } from '../controllers/user.controller';
6+
import { prisma } from '../prisma';
7+
import bcrypt from 'bcrypt';
8+
import { AuthorizationError } from '../schemas/errors';
9+
10+
export interface LoginCredentials {
11+
email: string;
12+
password: string;
13+
}
14+
15+
export interface User {
16+
id: string;
17+
email: string;
18+
role?: string;
19+
}
20+
21+
export async function login(_credentials: LoginCredentials): Promise<User> {
22+
const user = await prisma.user.findUnique({
23+
where: {
24+
email: _credentials.email,
25+
},
26+
});
27+
28+
if (!user) {
29+
throw new AuthorizationError();
30+
}
31+
32+
const isValidPassword = await bcrypt.compare(_credentials.password, user.hashedPassword);
33+
34+
if (!isValidPassword) {
35+
throw new AuthorizationError();
36+
}
37+
38+
await createSession({ userId: user.id, email: user.email });
39+
return user;
40+
}
41+
42+
export async function logout() {
43+
await deleteSession();
44+
redirect('/login');
45+
}
46+
47+
export async function getCurrentUser() {
48+
const session = await verifySession();
49+
50+
if (!session) {
51+
return null;
52+
}
53+
54+
return await userController.get(session.userId);
55+
}

0 commit comments

Comments
 (0)