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
61 changes: 58 additions & 3 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/ratelimit';

import { validateBody } from '@/lib/validation';
import { LoginRequestSchema } from '@/types/api/auth.dto';
import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';

import type { AuthResponse } from '@/types/api';
import { edgeLog } from '@/../infra/edge-config';

export const runtime = 'edge';


// ---------------------------------------------------------------------------
// POST /api/auth/login
// ---------------------------------------------------------------------------


export async function POST(
request: NextRequest,
): Promise<NextResponse<AuthResponseDTO | AuthErrorDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
if (rateLimitResponse) return rateLimitResponse as NextResponse;
export async function POST(request: NextRequest) {
edgeLog('info', '/api/auth/login', 'POST request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
if (rateLimitResponse) {
return rateLimitResponse as NextResponse<{ message: string }>;
}

try {
const body = await request.json();
const { email, password } = body;

const result = validateBody(LoginRequestSchema, await request.json());
if (!result.ok) return addHeaders(result.error) as NextResponse;


const { email, password } = result.data;

// Mock validation
if (!email || !password) {
Expand All @@ -23,6 +42,39 @@ export async function POST(request: NextRequest) {
) as NextResponse<{ message: string }>;
}



// Mock: demo credentials
if (email === '[email protected]' && password === 'password123') {
return addHeaders(
NextResponse.json(
{
message: 'Login successful',
user: { id: '1', name: 'Demo User', email },
token: `mock-jwt-token-${Date.now()}`,
},
{ status: 200 },
),
);
}

// Mock: accept any valid email + password >= 6 chars
if (password.length >= 6) {
return addHeaders(
NextResponse.json(
{
message: 'Login successful',
user: {
id: Math.random().toString(36).substring(2, 9),
name: email.split('@')[0],
email,
},
token: `mock-jwt-token-${Date.now()}`,
},
{ status: 200 },
),
);

// Mock authentication - check for demo credentials
if (email === '[email protected]' && password === 'password123') {
// Simulate successful login
Expand Down Expand Up @@ -69,5 +121,8 @@ export async function POST(request: NextRequest) {
return addHeaders(
NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
) as NextResponse<{ message: string }>;

}

return addHeaders(NextResponse.json({ message: 'Invalid email or password' }, { status: 401 }));
}
58 changes: 48 additions & 10 deletions src/app/api/auth/signup/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/ratelimit';

import { validateBody } from '@/lib/validation';
import { SignupRequestSchema } from '@/types/api/auth.dto';
import type { AuthResponseDTO, AuthErrorDTO } from '@/types/api/auth.dto';

// ---------------------------------------------------------------------------
// POST /api/auth/signup
// ---------------------------------------------------------------------------


export async function POST(
request: NextRequest,
): Promise<NextResponse<AuthResponseDTO | AuthErrorDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
if (rateLimitResponse) return rateLimitResponse as NextResponse;

import type { AuthResponse } from '@/types/api';
import { edgeLog } from '@/../infra/edge-config';

export const runtime = 'edge';


export async function POST(request: NextRequest) {
edgeLog('info', '/api/auth/signup', 'POST request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH');
Expand All @@ -16,6 +33,19 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const { name, email, password, confirmPassword } = body;



const result = validateBody(SignupRequestSchema, await request.json());
if (!result.ok) return addHeaders(result.error) as NextResponse;

const { name, email } = result.data;


// Mock: block already-registered email
if (email === '[email protected]') {
return addHeaders(NextResponse.json({ message: 'Email already registered' }, { status: 409 }));
}

if (!name || !email || !password || !confirmPassword) {
return addHeaders(
NextResponse.json({ message: 'All fields are required' }, { status: 400 }),
Expand All @@ -28,6 +58,7 @@ export async function POST(request: NextRequest) {
) as NextResponse<{ message: string }>;
}


if (password.length < 6) {
return addHeaders(
NextResponse.json({ message: 'Password must be at least 6 characters' }, { status: 400 }),
Expand All @@ -40,17 +71,23 @@ export async function POST(request: NextRequest) {
) as NextResponse<{ message: string }>;
}

return addHeaders(
NextResponse.json(
{
message: 'Account created successfully',
user: {
id: Math.random().toString(36).substr(2, 9),
name: name,
email: email,
},
token: 'mock-jwt-token-' + Date.now(),

return addHeaders(
NextResponse.json(
{
message: 'Account created successfully',
user: {
id: Math.random().toString(36).substring(2, 9),
name,
email,
},

token: `mock-jwt-token-${Date.now()}`,
},
{ status: 201 },
),
);

{ status: 201 },
),
) as NextResponse<AuthResponse>;
Expand All @@ -60,4 +97,5 @@ export async function POST(request: NextRequest) {
NextResponse.json({ message: 'Internal server error' }, { status: 500 }),
) as NextResponse<{ message: string }>;
}

}
131 changes: 105 additions & 26 deletions src/app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import { NextResponse } from 'next/server';
import type { VideoBookmark, ApiResponse, SuccessResponse } from '@/types/api';
import type { VideoBookmark } from '@/types/api';
import { withRateLimit } from '@/lib/ratelimit';

import { validateBody, validateQuery } from '@/lib/validation';
import {
BookmarksGetQuerySchema,
BookmarksCreateBodySchema,
BookmarksPatchBodySchema,
BookmarksDeleteBodySchema,
} from '@/types/api/bookmarks.dto';
import type {
BookmarksListResponseDTO,
BookmarkResponseDTO,
BookmarksSuccessResponseDTO,
} from '@/types/api/bookmarks.dto';

// ---------------------------------------------------------------------------
// In-memory store (replace with DB layer)
// ---------------------------------------------------------------------------

const bookmarksStore = new Map<string, VideoBookmark[]>();

const keyFor = (userId: string | undefined, lessonId: string): string => {

import { edgeLog } from '@/../infra/edge-config';

export const runtime = 'edge';
Expand All @@ -10,10 +32,26 @@ type PersistedVideoBookmark = VideoBookmark;
const bookmarksStore = new Map<string, PersistedVideoBookmark[]>();

const keyFor = (userId: string | undefined, lessonId: string) => {

const safeUserId = encodeURIComponent(userId ?? 'anon');
return `${safeUserId}::${encodeURIComponent(lessonId)}`;
};


// ---------------------------------------------------------------------------
// GET /api/bookmarks?lessonId=&userId=
// ---------------------------------------------------------------------------

export async function GET(
request: Request,
): Promise<NextResponse<BookmarksListResponseDTO | BookmarksSuccessResponseDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
if (rateLimitResponse) return rateLimitResponse as NextResponse;

const { searchParams } = new URL(request.url);
const result = validateQuery(BookmarksGetQuerySchema, searchParams);
if (!result.ok) return addHeaders(result.error) as NextResponse;

export async function GET(request: Request) {
edgeLog('info', '/api/bookmarks', 'GET request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
Expand All @@ -33,14 +71,29 @@ export async function GET(request: Request) {
);
}


return addHeaders(
NextResponse.json({
data: bookmarksStore.get(keyFor(userId, lessonId)) ?? [],
data: bookmarksStore.get(keyFor(result.data.userId, result.data.lessonId)) ?? [],
success: true,
}),
);
}


// ---------------------------------------------------------------------------
// POST /api/bookmarks
// ---------------------------------------------------------------------------

export async function POST(
request: Request,
): Promise<NextResponse<BookmarkResponseDTO | BookmarksSuccessResponseDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
if (rateLimitResponse) return rateLimitResponse as NextResponse;

const result = validateBody(BookmarksCreateBodySchema, await request.json());
if (!result.ok) return addHeaders(result.error) as NextResponse;

export async function POST(request: Request) {
edgeLog('info', '/api/bookmarks', 'POST request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
Expand All @@ -60,26 +113,36 @@ export async function POST(request: Request) {
);
}

const now = new Date().toISOString();
const id = body.bookmark.id ?? `bookmark-${Date.now()}`;

const persisted: PersistedVideoBookmark = {
id,
time: Math.max(0, body.bookmark.time),
title: body.bookmark.title.trim(),
note: body.bookmark.note?.trim() ? body.bookmark.note.trim() : undefined,
const now = new Date().toISOString();
const persisted: VideoBookmark = {
id: result.data.bookmark.id ?? `bookmark-${Date.now()}`,
time: result.data.bookmark.time,
title: result.data.bookmark.title,
note: result.data.bookmark.note,
createdAt: now,
updatedAt: now,
};

const key = keyFor(body.userId, body.lessonId);
const key = keyFor(result.data.userId, result.data.lessonId);
const prev = bookmarksStore.get(key) ?? [];
const next = [persisted, ...prev.filter((b) => b.id !== persisted.id)];
bookmarksStore.set(key, next);
bookmarksStore.set(key, [persisted, ...prev.filter((b) => b.id !== persisted.id)]);

return addHeaders(NextResponse.json({ success: true, data: persisted }));
}


// ---------------------------------------------------------------------------
// PATCH /api/bookmarks
// ---------------------------------------------------------------------------

export async function PATCH(request: Request): Promise<NextResponse<BookmarksSuccessResponseDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
if (rateLimitResponse) return rateLimitResponse as NextResponse;

const result = validateBody(BookmarksPatchBodySchema, await request.json());
if (!result.ok) return addHeaders(result.error) as NextResponse;

export async function PATCH(request: Request) {
edgeLog('info', '/api/bookmarks', 'PATCH request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
Expand All @@ -102,26 +165,41 @@ export async function PATCH(request: Request) {
);
}

const key = keyFor(body.userId, body.lessonId);

const key = keyFor(result.data.userId, result.data.lessonId);
const prev = bookmarksStore.get(key) ?? [];
const now = new Date().toISOString();

const next = prev.map((b) =>
b.id === body.id
? {
...b,
title: body.title.trim(),
note: body.note?.trim() ? body.note.trim() : undefined,
time: typeof body.time === 'number' ? Math.max(0, body.time) : b.time,
updatedAt: now,
}
: b,
bookmarksStore.set(
key,
prev.map((b) =>
b.id === result.data.id
? {
...b,
title: result.data.title,
note: result.data.note,
time: result.data.time !== undefined ? result.data.time : b.time,
updatedAt: now,
}
: b,
),
);

bookmarksStore.set(key, next);
return addHeaders(NextResponse.json({ success: true }));
}


// ---------------------------------------------------------------------------
// DELETE /api/bookmarks
// ---------------------------------------------------------------------------

export async function DELETE(request: Request): Promise<NextResponse<BookmarksSuccessResponseDTO>> {
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
if (rateLimitResponse) return rateLimitResponse as NextResponse;

const result = validateBody(BookmarksDeleteBodySchema, await request.json());
if (!result.ok) return addHeaders(result.error) as NextResponse;

export async function DELETE(request: Request) {
edgeLog('info', '/api/bookmarks', 'DELETE request received');
const { addHeaders, rateLimitResponse } = withRateLimit(request, 'WRITE');
Expand All @@ -136,11 +214,12 @@ export async function DELETE(request: Request) {
);
}

const key = keyFor(body.userId, body.lessonId);

const key = keyFor(result.data.userId, result.data.lessonId);
const prev = bookmarksStore.get(key) ?? [];
bookmarksStore.set(
key,
prev.filter((b) => b.id !== body.id),
prev.filter((b) => b.id !== result.data.id),
);

return addHeaders(NextResponse.json({ success: true }));
Expand Down
Loading
Loading