first commit
This commit is contained in:
3
app/api/auth/[...nextauth]/route.ts
Normal file
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
77
app/api/auth/forgot-password/route.ts
Normal file
77
app/api/auth/forgot-password/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { userDb } from '@/lib/db';
|
||||
import { passwordResetDb } from '@/lib/password-reset-db';
|
||||
import { emailService } from '@/lib/email-service';
|
||||
|
||||
/**
|
||||
* POST /api/auth/forgot-password
|
||||
* Request password reset email
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email address' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const users = userDb.findAll();
|
||||
const user = users.find(u => u.email?.toLowerCase() === email.toLowerCase());
|
||||
|
||||
// SECURITY: Always return success to prevent user enumeration
|
||||
// Even if user doesn't exist, return success but don't send email
|
||||
if (!user) {
|
||||
console.log('[ForgotPassword] Email not found, but returning success (security)');
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'If an account exists with this email, a password reset link has been sent.',
|
||||
});
|
||||
}
|
||||
|
||||
// Create password reset token
|
||||
const token = passwordResetDb.create(user.id, 1); // 1 hour expiry
|
||||
|
||||
// Send password reset email
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
const resetUrl = `${baseUrl}/reset-password?token=${token}`;
|
||||
|
||||
try {
|
||||
await emailService.sendPasswordResetEmail({
|
||||
email: user.email!,
|
||||
username: user.username,
|
||||
resetUrl,
|
||||
expiresIn: '1 hour',
|
||||
});
|
||||
|
||||
console.log('[ForgotPassword] Password reset email sent to:', user.email);
|
||||
} catch (emailError) {
|
||||
console.error('[ForgotPassword] Failed to send email:', emailError);
|
||||
// Don't fail the request if email fails - log and continue
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'If an account exists with this email, a password reset link has been sent.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ForgotPassword] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again later.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
121
app/api/auth/register/route.ts
Normal file
121
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { userDb } from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { emailService } from '@/lib/email-service';
|
||||
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
* Public user registration endpoint
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { username, email, password } = body;
|
||||
|
||||
// Validation
|
||||
if (!username || !email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: username, email, password' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Username validation (at least 3 characters, alphanumeric + underscore)
|
||||
if (username.length < 3) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username must be at least 3 characters long' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username can only contain letters, numbers, and underscores' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email address' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Password validation (at least 6 characters)
|
||||
if (password.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Password must be at least 6 characters long' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if username already exists
|
||||
const existingUser = userDb.findByUsername(username);
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username already taken' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const allUsers = userDb.findAll();
|
||||
const emailExists = allUsers.find(u => u.email?.toLowerCase() === email.toLowerCase());
|
||||
if (emailExists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email already registered' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user with VIEWER role (new users get viewer access by default)
|
||||
const user = userDb.create({
|
||||
id: randomUUID(),
|
||||
username,
|
||||
email,
|
||||
passwordHash,
|
||||
role: 'VIEWER', // New registrations get VIEWER role
|
||||
});
|
||||
|
||||
console.log('[Register] New user registered:', username);
|
||||
|
||||
// Send welcome email (don't fail registration if email fails)
|
||||
try {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
await emailService.sendWelcomeEmail({
|
||||
email,
|
||||
username,
|
||||
loginUrl: `${baseUrl}/login`,
|
||||
});
|
||||
console.log('[Register] Welcome email sent to:', email);
|
||||
} catch (emailError) {
|
||||
console.error('[Register] Failed to send welcome email:', emailError);
|
||||
// Don't fail registration if email fails
|
||||
}
|
||||
|
||||
// Remove password hash from response
|
||||
const { passwordHash: _, ...safeUser } = user;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Account created successfully',
|
||||
user: safeUser,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[Register] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Registration failed. Please try again later.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
106
app/api/auth/reset-password/route.ts
Normal file
106
app/api/auth/reset-password/route.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { userDb } from '@/lib/db';
|
||||
import { passwordResetDb } from '@/lib/password-reset-db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
/**
|
||||
* POST /api/auth/reset-password
|
||||
* Reset password with token
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { token, newPassword } = body;
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token and new password are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (newPassword.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Password must be at least 6 characters' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate token
|
||||
if (!passwordResetDb.isValid(token)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or expired reset token' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get token details
|
||||
const resetToken = passwordResetDb.findByToken(token);
|
||||
if (!resetToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid reset token' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user
|
||||
const user = userDb.findById(resetToken.user_id);
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const passwordHash = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update user password
|
||||
userDb.update(user.id, { passwordHash });
|
||||
|
||||
// Mark token as used
|
||||
passwordResetDb.markUsed(token);
|
||||
|
||||
console.log('[ResetPassword] Password reset successful for user:', user.username);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Password has been reset successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ResetPassword] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to reset password' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/reset-password?token=xxx
|
||||
* Validate reset token (for checking if link is still valid)
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const token = searchParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const isValid = passwordResetDb.isValid(token);
|
||||
|
||||
return NextResponse.json({ valid: isValid });
|
||||
} catch (error) {
|
||||
console.error('[ResetPassword] Validation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to validate token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user