mirror of
https://github.com/System-End/spaces.git
synced 2026-04-19 16:38:24 +00:00
- 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>
292 lines
No EOL
7.3 KiB
JavaScript
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; |