Files
PinPoint-Golf/src/server/routes/authRoutes.ts
T

213 lines
7.6 KiB
TypeScript

import { Router, Response } from 'express';
import bcrypt from 'bcryptjs';
import { db } from '../db';
import { generateToken, authMiddleware, AuthenticatedRequest } from '../authMiddleware';
import { User, AuthProvider } from '../../types/auth';
const router = Router();
// --- 1. Local Registration ---
router.post('/register', (req, res: Response) => {
try {
const { email, username, password, name, handicapIndex, homeClub, isPublic } = req.body;
if (!email || !username || !password) {
return res.status(400).json({ error: 'Email, username, and password are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
const existingUser = db.findUserByEmailOrUsername(email) || db.findUserByEmailOrUsername(username);
if (existingUser) {
return res.status(409).json({ error: 'An account with that email or username already exists' });
}
const salt = bcrypt.genSaltSync(10);
const passwordHash = bcrypt.hashSync(password, salt);
const newUser: User & { passwordHash: string } = {
id: `usr_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
email: email.toLowerCase().trim(),
username: username.toLowerCase().trim(),
name: name?.trim() || username.trim(),
handicapIndex: typeof handicapIndex === 'number' ? handicapIndex : 15.0,
homeClub: homeClub?.trim() || 'Local Links Club',
isPublic: isPublic !== false, // default true for community leaderboards
provider: 'local',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
passwordHash,
};
const savedUser = db.createUser(newUser);
const token = generateToken(savedUser);
return res.status(201).json({
token,
user: savedUser,
settings: db.getSettings(savedUser.id),
});
} catch (error) {
console.error('Registration error:', error);
return res.status(500).json({ error: 'Internal registration failure' });
}
});
// --- 2. Local Login ---
router.post('/login', (req, res: Response) => {
try {
const { identifier, password } = req.body;
if (!identifier || !password) {
return res.status(400).json({ error: 'Email/Username and password are required' });
}
const userWithHash = db.findUserByEmailOrUsername(identifier);
if (!userWithHash || !userWithHash.passwordHash) {
return res.status(401).json({ error: 'Invalid credentials. Please verify your email and password.' });
}
const isValid = bcrypt.compareSync(password, userWithHash.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials. Please verify your email and password.' });
}
const { passwordHash: _, ...safeUser } = userWithHash;
const token = generateToken(safeUser);
return res.json({
token,
user: safeUser,
settings: db.getSettings(safeUser.id),
});
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({ error: 'Internal login failure' });
}
});
// --- 3. Get Current User & Profile ---
router.get('/me', authMiddleware, (req: AuthenticatedRequest, res: Response) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const settings = db.getSettings(req.user.id);
return res.json({
user: req.user,
settings,
});
});
// --- 4. Update Profile & Privacy (Public / Private toggle) ---
router.put('/profile', authMiddleware, (req: AuthenticatedRequest, res: Response) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const { name, handicapIndex, homeClub, isPublic, avatarUrl } = req.body;
const updates: Partial<User> = {};
if (typeof name === 'string' && name.trim()) updates.name = name.trim();
if (typeof handicapIndex === 'number') updates.handicapIndex = Math.max(-10, Math.min(54, handicapIndex));
if (typeof homeClub === 'string') updates.homeClub = homeClub.trim();
if (typeof isPublic === 'boolean') updates.isPublic = isPublic;
if (typeof avatarUrl === 'string') updates.avatarUrl = avatarUrl;
const updatedUser = db.updateUser(req.user.id, updates);
return res.json({ user: updatedUser });
});
// --- 5. User Settings Sync ---
router.get('/settings', authMiddleware, (req: AuthenticatedRequest, res: Response) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
return res.json(db.getSettings(req.user.id));
});
router.put('/settings', authMiddleware, (req: AuthenticatedRequest, res: Response) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const updated = db.updateSettings(req.user.id, req.body);
return res.json(updated);
});
// --- 6. OAuth Providers Discovery ---
router.get('/oauth/providers', (_req, res: Response) => {
res.json({
providers: [
{
id: 'google',
name: 'Google',
icon: 'Google',
isConfigured: !!process.env.GOOGLE_CLIENT_ID,
authUrlEndpoint: '/api/auth/oauth/google',
},
{
id: 'authentik',
name: 'Authentik (OIDC)',
icon: 'Shield',
isConfigured: !!process.env.AUTHENTIK_ISSUER_URL,
issuerUrl: process.env.AUTHENTIK_ISSUER_URL || 'https://authentik.company.local',
authUrlEndpoint: '/api/auth/oauth/authentik',
},
{
id: 'github',
name: 'GitHub',
icon: 'Github',
isConfigured: !!process.env.GITHUB_CLIENT_ID,
authUrlEndpoint: '/api/auth/oauth/github',
},
],
});
});
// --- 7. OAuth / SSO Login (Google, Authentik, GitHub, Demo SSO) ---
router.post('/oauth/sso-login', (req, res: Response) => {
try {
const { provider, email, name, username, avatarUrl, authentikServerUrl } = req.body;
if (!provider) {
return res.status(400).json({ error: 'Provider is required' });
}
const providerType = provider as AuthProvider;
// Generate reliable email & username if from SSO
const ssoEmail = email?.toLowerCase().trim() || `${provider}_golfer_${Date.now().toString(36)}@example.com`;
const ssoUsername = username?.toLowerCase().trim() || `${provider}_${Math.random().toString(36).substring(2, 7)}`;
const ssoName = name?.trim() || `${provider.charAt(0).toUpperCase() + provider.slice(1)} Golfer`;
// Check if user already exists
let user = db.findUserByEmailOrUsername(ssoEmail);
if (!user) {
// Create new user connected via SSO
const newUser: User = {
id: `usr_${provider}_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
email: ssoEmail,
username: ssoUsername,
name: ssoName,
avatarUrl: avatarUrl || (provider === 'authentik'
? 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=120&h=120&q=80'
: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=120&h=120&q=80'),
handicapIndex: 12.0,
homeClub: authentikServerUrl ? `Authentik SSO (${new URL(authentikServerUrl).hostname})` : 'Global Golf Tour',
isPublic: true,
provider: providerType,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
user = db.createUser(newUser);
}
const { passwordHash: _, ...safeUser } = user;
const token = generateToken(safeUser);
return res.json({
token,
user: safeUser,
settings: db.getSettings(safeUser.id),
});
} catch (error) {
console.error('SSO Login error:', error);
return res.status(500).json({ error: 'SSO authentication failed' });
}
});
export default router;