Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
},
"dependencies": {
"@prisma/client": "6.15.0",
"bcrypt": "^6.0.0",
"jose": "^6.1.0",
"next": "15.5.2",
"prisma": "^6.15.0",
"react": "19.1.0",
Expand All @@ -36,6 +38,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/bcrypt": "^6.0.0",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
42 changes: 42 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- Added the required column `hashedPassword` to the `User` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT NOT NULL;
Comment on lines +7 to +8

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration will fail on non-empty tables because it adds a NOT NULL column without a default/backfill. Use a two-step migration (add nullable column, backfill, then set NOT NULL) or provide a safe default and backfill before enforcing NOT NULL.

Suggested change
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT NOT NULL;
-- Step 1: Add the column as nullable
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT;
-- Step 2: Backfill existing rows with a safe default (empty string)
UPDATE "public"."User" SET "hashedPassword" = '' WHERE "hashedPassword" IS NULL;
-- Step 3: Set the column as NOT NULL
ALTER TABLE "public"."User" ALTER COLUMN "hashedPassword" SET NOT NULL;

Copilot uses AI. Check for mistakes.

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration will fail on non-empty tables because it adds a NOT NULL column without a default/backfill. Use a two-step migration (add nullable column, backfill, then set NOT NULL) or provide a safe default and backfill before enforcing NOT NULL.

Suggested change
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT NOT NULL;
-- Step 1: Add the column as nullable
ALTER TABLE "public"."User" ADD COLUMN "hashedPassword" TEXT;
-- Step 2: Backfill existing rows with a safe default (empty string)
UPDATE "public"."User" SET "hashedPassword" = '' WHERE "hashedPassword" IS NULL;
-- Step 3: Set the column as NOT NULL
ALTER TABLE "public"."User" ALTER COLUMN "hashedPassword" SET NOT NULL;

Copilot uses AI. Check for mistakes.
35 changes: 18 additions & 17 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,32 @@ datasource db {
}

model Organization {
id String @id @unique @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String @unique
createdBy User @relation("CreatedOrganization", fields: [createdById], references: [id])
users User[] @relation("UserOrganization")
id String @id @unique @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String @unique
createdBy User @relation("CreatedOrganization", fields: [createdById], references: [id])
users User[] @relation("UserOrganization")
positions Position[]
candidates Candidate[]
AssessmentTemplate AssessmentTemplate[]
TaskTemplate TaskTemplate[]
}

model User {
id String @id @unique @default(uuid())
name String
email String @unique
orgId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization? @relation("UserOrganization", fields: [orgId], references: [id])
id String @id @unique @default(uuid())
name String
email String @unique
orgId String?
hashedPassword String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization? @relation("UserOrganization", fields: [orgId], references: [id])
createdOrganization Organization? @relation("CreatedOrganization")
userRoles UserRole[]
Review Review[]
Comment Comment[]
userRoles UserRole[]
Review Review[]
Comment Comment[]
}

model Role {
Expand Down
4 changes: 4 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ async function main() {
update: {},
create: {
id: 'e99335bd-9dd7-4260-8977-2eeaa4df799c',
hashedPassword: 'abcd',
Comment thread
cherman23 marked this conversation as resolved.
name: 'Admin User',
email: 'admin@techcorp.com',
},
Expand All @@ -20,6 +21,7 @@ async function main() {
create: {
id: '68992d1e-e119-4874-b768-bf685d10194e',
name: 'John Doe',
hashedPassword: 'abcd',
Comment thread
cherman23 marked this conversation as resolved.
email: 'john.doe@techcorp.com',
},
}),
Expand All @@ -29,6 +31,7 @@ async function main() {
create: {
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'Jane Smith',
hashedPassword: 'abcd',
Comment thread
cherman23 marked this conversation as resolved.
email: 'jane.smith@startupxyz.com',
},
}),
Expand All @@ -38,6 +41,7 @@ async function main() {
create: {
id: 'b2c3d4e5-f6g7-8901-bcde-f12345678901',
name: 'Bob Wilson',
hashedPassword: 'abcd',

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: replace with a bcrypt hash.

Copilot uses AI. Check for mistakes.
email: 'bob.wilson@enterprise.com',
},
}),
Expand Down
3 changes: 3 additions & 0 deletions src/app/(web)/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function LoginPage() {
return <div className="">login</div>;
}
3 changes: 3 additions & 0 deletions src/app/(web)/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function SignupPage() {
return <div className="">signup</div>;
}
3 changes: 3 additions & 0 deletions src/app/(web)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function DashboardPage() {
return <div className=""></div>;
}
37 changes: 37 additions & 0 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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);
}
Comment on lines +21 to +23

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The login service throws AuthorizationError on invalid credentials, but this handler does not catch it; the request will fall through to a 500. Handle AuthorizationError and return 401 (e.g., return sargeApiError('Invalid email or password', 401)).

Copilot uses AI. Check for mistakes.

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);
}
Comment on lines +27 to +33

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These string checks are unreachable with the current implementation (the service throws AuthorizationError with message 'Unauthorized'). Remove them or align the service to throw typed errors you handle explicitly.

Copilot uses AI. Check for mistakes.

return sargeApiError(message, 500);
}
}
13 changes: 13 additions & 0 deletions src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type NextRequest } from 'next/server';
import { sargeApiResponse } from '@/lib/responses';
import { logout } from '@/lib/auth/auth-service';

export async function POST(_request: NextRequest) {
try {
await logout();
Comment on lines +3 to +7

Copilot AI Oct 14, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logout() performs redirect('/login'), which short-circuits the route and prevents returning JSON. Consider splitting responsibilities: have a session deletion function for APIs, and perform redirects only in server actions/pages.

Suggested change
import { logout } from '@/lib/auth/auth-service';
export async function POST(_request: NextRequest) {
try {
await logout();
import { deleteSession } from '@/lib/auth/auth-service';
export async function POST(_request: NextRequest) {
try {
await deleteSession();

Copilot uses AI. Check for mistakes.
return sargeApiResponse({ message: 'Logged out successfully' }, 200);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return sargeApiResponse({ message: 'Logged out', error: message }, 200);
}
}
18 changes: 18 additions & 0 deletions src/app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type NextRequest } from 'next/server';
import { sargeApiResponse, sargeApiError } from '@/lib/responses';
import { getCurrentUser } from '@/lib/auth/auth-service';

export async function GET(_request: NextRequest) {
try {
const user = await getCurrentUser();

if (!user) {
return sargeApiError('Not authenticated', 401);
}

return sargeApiResponse(user, 200);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return sargeApiError(message, 500);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { type NextRequest } from 'next/server';

export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const userId = (await params).userId;
const user = await userController.delete(userId);
const id = (await params).id;
const user = await userController.delete(id);
return sargeApiResponse(user, 200);
} catch (error) {
if (error instanceof UserNotFoundError) {
Expand All @@ -21,13 +21,10 @@ export async function DELETE(
}
}

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const userId = (await params).userId;
const user = await userController.get(userId);
const id = (await params).id;
const user = await userController.get(id);
return sargeApiResponse(user, 200);
} catch (error) {
if (error instanceof UserNotFoundError) {
Expand All @@ -39,14 +36,11 @@ export async function GET(
}
}

export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const userId = (await params).userId;
const id = (await params).id;
const body = await request.json();
const user = await userController.update(userId, body);
const user = await userController.update(id, body);
return sargeApiResponse(user, 200);
} catch (error) {
if (error instanceof UserNotFoundError) {
Expand Down
Loading