spaces/src/api/users/auth.route.js
Arnav ec00da25b8 Frontend improvements and security enhancements
- Improved auth UI with better verification flow
- Added theme system with 8 themes (light, dark, rainbow, etc.)
- Dynamic theme-based logo colors
- Migrated from localStorage to secure cookies for auth tokens
- Removed duplicate auth token storage
- Added httpOnly: false cookies with 7-day expiry and SameSite protection
- Clean, icon-free login page design
- Signup verification code stays on right panel (no slide animation)
- Removed debug console.log statements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 15:48:44 +00:00

292 lines
No EOL
7.3 KiB
JavaScript

import express from 'express';
import { sendEmail, checkEmail } from '../../utils/airtable.js';
import pg from '../../utils/db.js';
import crypto from 'crypto';
import { strictLimiter, authLimiter } from '../../middlewares/rate-limit.middleware.js';
const router = express.Router();
const randomToken = () => {
return crypto.randomBytes(32).toString('hex');
};
router.get('/send', (req, res) => {
res.status(200).json({
message: 'Use Post to /send to send verification code',
});
});
// POST /api/v1/users/send
router.post('/send', /* strictLimiter, */ async (req, res) => {
try {
const { email, mode } = req.body;
if (!email) {
return res.status(400).json({
success: false,
message: 'Email is required'
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
message: 'Invalid email format'
});
}
if (mode === 'login') {
const user = await pg('users')
.where('email', email)
.first();
if (!user) {
return res.status(404).json({
success: false,
message: 'No account found with this email. Please sign up first.'
});
}
}
const result = await sendEmail(email);
res.status(200).json({
success: true,
message: 'Verification code sent successfully',
data: {
email: result.email,
}
});
} catch (error) {
console.error('Error in /send route:', error);
res.status(500).json({
success: false,
message: 'Failed to send verification code',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// POST /api/v1/users/signup
router.post('/signup', /* authLimiter, */ async (req, res) => {
try {
const { email, username, verificationCode } = req.body;
if (!email || !username || !verificationCode) {
return res.status(400).json({
success: false,
message: 'Email, username, and verification code are required'
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
message: 'Invalid email format'
});
}
if (username.length > 100) {
return res.status(400).json({
success: false,
message: 'Username must be 100 characters or less'
});
}
const codeValid = await checkEmail(email, verificationCode);
if (!codeValid) {
return res.status(400).json({
success: false,
message: 'Invalid or expired verification code'
});
}
const existingUser = await pg('users')
.where('email', email)
.orWhere('username', username)
.first();
if (existingUser) {
return res.status(409).json({
success: false,
message: existingUser.email === email ? 'Email already registered' : 'Username already taken'
});
}
const authToken = randomToken();
const [newUser] = await pg('users')
.insert({
email,
username,
authorization: authToken,
max_spaces: 3,
is_admin: false
})
.returning(['id', 'email', 'username', 'authorization', 'is_admin']);
res.cookie('auth_token', newUser.authorization, {
httpOnly: false,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
res.status(201).json({
success: true,
message: 'User created successfully',
data: {
id: newUser.id,
email: newUser.email,
username: newUser.username,
authorization: newUser.authorization,
is_admin: newUser.is_admin
}
});
} catch (error) {
console.error('Error in /signup route:', error);
if (error.code === '23505') {
return res.status(409).json({
success: false,
message: 'Email or username already exists'
});
}
res.status(500).json({
success: false,
message: 'Failed to create user account',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// POST /api/v1/users/login
router.post('/login', /* authLimiter, */ async (req, res) => {
try {
const { email, verificationCode } = req.body;
if (!email || !verificationCode) {
return res.status(400).json({
success: false,
message: 'Email and verification code are required'
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
message: 'Invalid email format'
});
}
const codeValid = await checkEmail(email, verificationCode);
if (!codeValid) {
return res.status(400).json({
success: false,
message: 'Invalid or expired verification code'
});
}
const user = await pg('users')
.where('email', email)
.first();
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found. Please sign up first.'
});
}
const newAuthToken = randomToken();
const [updatedUser] = await pg('users')
.where('email', email)
.update({ authorization: newAuthToken })
.returning(['email', 'username', 'authorization', 'is_admin']);
res.cookie('auth_token', updatedUser.authorization, {
httpOnly: false,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
res.status(200).json({
success: true,
message: 'Login successful',
data: {
email: updatedUser.email,
username: updatedUser.username,
authorization: updatedUser.authorization,
is_admin: updatedUser.is_admin
}
});
} catch (error) {
console.error('Error in /login route:', error);
res.status(500).json({
success: false,
message: 'Failed to login',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// POST /api/v1/users/signout
router.post('/signout', async (req, res) => {
try {
const { authorization } = req.body;
if (!authorization) {
return res.status(400).json({
success: false,
message: 'Authorization token is required'
});
}
const user = await pg('users')
.where('authorization', authorization)
.first();
if (!user) {
return res.status(404).json({
success: false,
message: 'Invalid authorization token'
});
}
const newAuthToken = randomToken();
const [updatedUser] = await pg('users')
.where('authorization', authorization)
.update({ authorization: newAuthToken })
.returning(['email']);
res.clearCookie('auth_token');
res.status(200).json({
success: true,
message: 'Sign out successful',
data: {
email: updatedUser.email,
}
});
} catch (error) {
console.error('Error in /signout route:', error);
res.status(500).json({
success: false,
message: 'Failed to sign out',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
export default router;