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
Empty file added apps/web/.devlog/devlog.sqlite
Empty file.
55 changes: 55 additions & 0 deletions apps/web/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* User login endpoint
*/

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const loginSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(1, 'Password is required'),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const validatedData = loginSchema.parse(body);

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();
const result = await authService.login(validatedData);

return NextResponse.json({
success: true,
message: 'Login successful',
user: result.user,
tokens: result.tokens,
}, { status: 200 });

} catch (error) {
console.error('Login error:', error);

if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation error',
details: error.errors,
}, { status: 400 });
}

if (error instanceof Error) {
if (error.message.includes('Invalid email or password')) {
return NextResponse.json({
success: false,
error: 'Invalid email or password',
}, { status: 401 });
}
}

return NextResponse.json({
success: false,
error: 'Login failed',
}, { status: 500 });
}
}
34 changes: 34 additions & 0 deletions apps/web/app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Get current user information endpoint
*/

import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
try {
const authHeader = req.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Missing or invalid authorization header' }, {
status: 401,
});
}

const token = authHeader.substring(7); // Remove 'Bearer ' prefix

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();

const user = await authService.verifyToken(token);

return NextResponse.json({
success: true,
user,
}, { status: 200 });

} catch (error) {
return NextResponse.json({ error: 'Invalid or expired token' }, {
status: 401,
});
}
}
44 changes: 44 additions & 0 deletions apps/web/app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Token refresh endpoint
*/

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const refreshSchema = z.object({
refreshToken: z.string().min(1, 'Refresh token is required'),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const validatedData = refreshSchema.parse(body);

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();
const newTokens = await authService.refreshToken(validatedData.refreshToken);

return NextResponse.json({
success: true,
message: 'Token refreshed successfully',
tokens: newTokens,
}, { status: 200 });

} catch (error) {
console.error('Token refresh error:', error);

if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation error',
details: error.errors,
}, { status: 400 });
}

return NextResponse.json({
success: false,
error: 'Invalid or expired refresh token',
}, { status: 401 });
}
}
58 changes: 58 additions & 0 deletions apps/web/app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* User registration endpoint
*/

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const registrationSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
name: z.string().optional(),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const validatedData = registrationSchema.parse(body);

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();
const result = await authService.register(validatedData);

// TODO: Send email verification email with result.emailToken
// For now, we'll just return success

return NextResponse.json({
success: true,
message: 'Registration successful. Please check your email for verification.',
user: result.user,
}, { status: 201 });

} catch (error) {
console.error('Registration error:', error);

if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation error',
details: error.errors,
}, { status: 400 });
}

if (error instanceof Error) {
if (error.message.includes('already exists')) {
return NextResponse.json({
success: false,
error: 'User with this email already exists',
}, { status: 409 });
}
}

return NextResponse.json({
success: false,
error: 'Registration failed',
}, { status: 500 });
}
}
87 changes: 87 additions & 0 deletions apps/web/app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Password reset endpoints
*/

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const requestResetSchema = z.object({
email: z.string().email('Invalid email format'),
});

const confirmResetSchema = z.object({
token: z.string().min(1, 'Reset token is required'),
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { searchParams } = new URL(req.url);
const action = searchParams.get('action');

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();

if (action === 'request') {
const validatedData = requestResetSchema.parse(body);

// Generate reset token (returns null if email doesn't exist, for security)
const resetToken = await authService.generatePasswordResetToken(validatedData.email);

// TODO: Send password reset email with resetToken.token
// Always return success for security (don't reveal if email exists)

return NextResponse.json({
success: true,
message: 'If your email is registered, you will receive a password reset link.',
}, { status: 200 });

} else if (action === 'confirm') {
const validatedData = confirmResetSchema.parse(body);

const user = await authService.resetPassword(
validatedData.token,
validatedData.newPassword
);

return NextResponse.json({
success: true,
message: 'Password reset successfully',
user,
}, { status: 200 });

} else {
return NextResponse.json({
success: false,
error: 'Invalid action. Use ?action=request or ?action=confirm',
}, { status: 400 });
}

} catch (error) {
console.error('Password reset error:', error);

if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation error',
details: error.errors,
}, { status: 400 });
}

if (error instanceof Error) {
if (error.message.includes('Invalid or expired')) {
return NextResponse.json({
success: false,
error: 'Invalid or expired reset token',
}, { status: 400 });
}
}

return NextResponse.json({
success: false,
error: 'Password reset failed',
}, { status: 500 });
}
}
53 changes: 53 additions & 0 deletions apps/web/app/api/auth/verify-email/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Email verification endpoint
*/

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const verifyEmailSchema = z.object({
token: z.string().min(1, 'Verification token is required'),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const validatedData = verifyEmailSchema.parse(body);

// Dynamic import to keep server-only
const { AuthService } = await import('@codervisor/devlog-core/auth');
const authService = AuthService.getInstance();
const user = await authService.verifyEmail(validatedData.token);

return NextResponse.json({
success: true,
message: 'Email verified successfully',
user,
}, { status: 200 });

} catch (error) {
console.error('Email verification error:', error);

if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation error',
details: error.errors,
}, { status: 400 });
}

if (error instanceof Error) {
if (error.message.includes('Invalid or expired')) {
return NextResponse.json({
success: false,
error: 'Invalid or expired verification token',
}, { status: 400 });
}
}

return NextResponse.json({
success: false,
error: 'Email verification failed',
}, { status: 500 });
}
}
37 changes: 37 additions & 0 deletions apps/web/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Login page
*/

import Link from 'next/link';
import { LoginForm } from '@/components/auth';

export default function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
<div className="w-full max-w-md space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Welcome Back
</h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Sign in to your devlog account
</p>
</div>

<LoginForm />

<div className="text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
Don't have an account?{' '}
<Link
href="/register"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Sign up
</Link>
</p>
</div>
</div>
</div>
);
}
Loading
Loading