- Implemented forgot password and reset password routes in the backend. - Added email sending capabilities using Nodemailer for password reset requests. - Created ResetPassword page in the frontend for users to reset their passwords. - Updated user model to include reset token and expiry fields. - Integrated hiscores API with caching mechanism for improved performance. - Enhanced authentication modal to include forgot password option. - Updated environment configuration for SMTP settings.
234 lines
5.5 KiB
TypeScript
234 lines
5.5 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { randomBytes } from 'crypto';
|
|
import { prisma } from '../db';
|
|
import { hashPassword, verifyPassword } from '../utils/password';
|
|
import { createSession, destroySession, requireAuth } from '../middleware/session';
|
|
import { sendPasswordResetEmail, isEmailConfigured } from '../utils/email';
|
|
|
|
const auth = new Hono();
|
|
|
|
/**
|
|
* POST /api/register
|
|
* Create a new user account
|
|
*/
|
|
auth.post('/register', async (c) => {
|
|
const body = await c.req.json();
|
|
const { username, email, password } = body;
|
|
|
|
// Validation
|
|
if (!username || !email || !password) {
|
|
return c.json({ error: 'Username, email, and password are required' }, 400);
|
|
}
|
|
|
|
if (username.length < 3 || username.length > 30) {
|
|
return c.json({ error: 'Username must be 3-30 characters' }, 400);
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
return c.json({ error: 'Password must be at least 8 characters' }, 400);
|
|
}
|
|
|
|
// Check if username or email already exists
|
|
const existingUser = await prisma.user.findFirst({
|
|
where: {
|
|
OR: [{ username }, { email }],
|
|
},
|
|
});
|
|
|
|
if (existingUser) {
|
|
if (existingUser.username === username) {
|
|
return c.json({ error: 'Username already taken' }, 400);
|
|
}
|
|
return c.json({ error: 'Email already registered' }, 400);
|
|
}
|
|
|
|
// Create user
|
|
const passwordHash = await hashPassword(password);
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username,
|
|
email,
|
|
passwordHash,
|
|
},
|
|
});
|
|
|
|
// Create session
|
|
await createSession(c, user.id);
|
|
|
|
return c.json({
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/login
|
|
* Authenticate and create session
|
|
*/
|
|
auth.post('/login', async (c) => {
|
|
const body = await c.req.json();
|
|
const { username, password } = body;
|
|
|
|
if (!username || !password) {
|
|
return c.json({ error: 'Username and password are required' }, 400);
|
|
}
|
|
|
|
// Find user by username or email
|
|
const user = await prisma.user.findFirst({
|
|
where: {
|
|
OR: [{ username }, { email: username }],
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return c.json({ error: 'Invalid username or password' }, 401);
|
|
}
|
|
|
|
// Verify password
|
|
const isValid = await verifyPassword(password, user.passwordHash);
|
|
if (!isValid) {
|
|
return c.json({ error: 'Invalid username or password' }, 401);
|
|
}
|
|
|
|
// Create session
|
|
await createSession(c, user.id);
|
|
|
|
return c.json({
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/logout
|
|
* Destroy session
|
|
*/
|
|
auth.post('/logout', async (c) => {
|
|
await destroySession(c);
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
/**
|
|
* GET /api/auth/status
|
|
* Check if user is authenticated
|
|
*/
|
|
auth.get('/auth/status', (c) => {
|
|
const user = c.get('user');
|
|
return c.json({
|
|
authenticated: !!user,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /api/me
|
|
* Get current user info
|
|
*/
|
|
auth.get('/me', requireAuth, (c) => {
|
|
const user = c.get('user');
|
|
return c.json({
|
|
username: user!.username,
|
|
email: user!.email,
|
|
role: user!.role,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/forgot-password
|
|
* Request a password reset
|
|
*/
|
|
auth.post('/forgot-password', async (c) => {
|
|
const body = await c.req.json();
|
|
const { email } = body;
|
|
|
|
if (!email) {
|
|
return c.json({ error: 'Email is required' }, 400);
|
|
}
|
|
|
|
// Find user by email
|
|
const user = await prisma.user.findUnique({
|
|
where: { email },
|
|
});
|
|
|
|
// Always return success to prevent email enumeration
|
|
if (!user) {
|
|
return c.json({ message: 'If an account with that email exists, a reset link has been sent.' });
|
|
}
|
|
|
|
// Generate secure token
|
|
const resetToken = randomBytes(32).toString('hex');
|
|
const resetTokenExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now
|
|
|
|
// Store token in database
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
resetToken,
|
|
resetTokenExpiry,
|
|
},
|
|
});
|
|
|
|
// Send email with reset link
|
|
const baseUrl = process.env.APP_URL || 'https://leagues.tools';
|
|
|
|
if (isEmailConfigured()) {
|
|
const emailResult = await sendPasswordResetEmail(email, resetToken, baseUrl);
|
|
if (!emailResult.success) {
|
|
console.error(`Failed to send password reset email to ${email}:`, emailResult.error);
|
|
}
|
|
} else {
|
|
// Log token in development when email is not configured
|
|
console.log(`Password reset requested for ${email}. Token: ${resetToken}`);
|
|
console.log(`Reset URL: ${baseUrl}/reset-password?token=${resetToken}`);
|
|
}
|
|
|
|
return c.json({ message: 'If an account with that email exists, a reset link has been sent.' });
|
|
});
|
|
|
|
/**
|
|
* POST /api/reset-password
|
|
* Reset password using token
|
|
*/
|
|
auth.post('/reset-password', async (c) => {
|
|
const body = await c.req.json();
|
|
const { token, password } = body;
|
|
|
|
if (!token || !password) {
|
|
return c.json({ error: 'Token and password are required' }, 400);
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
return c.json({ error: 'Password must be at least 8 characters' }, 400);
|
|
}
|
|
|
|
// Find user with this token
|
|
const user = await prisma.user.findFirst({
|
|
where: {
|
|
resetToken: token,
|
|
resetTokenExpiry: {
|
|
gt: new Date(), // Token must not be expired
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return c.json({ error: 'Invalid or expired reset token' }, 400);
|
|
}
|
|
|
|
// Update password and clear token
|
|
const passwordHash = await hashPassword(password);
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
passwordHash,
|
|
resetToken: null,
|
|
resetTokenExpiry: null,
|
|
},
|
|
});
|
|
|
|
return c.json({ message: 'Password has been reset successfully. You can now log in.' });
|
|
});
|
|
|
|
export default auth;
|