Spaces:
Sleeping
Sleeping
import express from 'express'; | |
import bcrypt from 'bcryptjs'; | |
import jwt from 'jsonwebtoken'; | |
import { v4 as uuidv4 } from 'uuid'; | |
import { OAuth2Client } from 'google-auth-library'; | |
import { database } from '../db/database'; | |
import { authenticateToken } from '../middleware/auth'; | |
const router = express.Router(); | |
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); | |
// Sign up with email/password | |
router.post('/signup', async (req, res) => { | |
try { | |
const { email, password, name } = req.body; | |
// Validation | |
if (!email || !password || !name) { | |
return res.status(400).json({ error: 'Email, password, and name are required' }); | |
} | |
if (password.length < 6) { | |
return res.status(400).json({ error: 'Password must be at least 6 characters' }); | |
} | |
// Check if user already exists | |
const existingUser = await database.get('SELECT id FROM users WHERE email = ?', [email]); | |
if (existingUser) { | |
return res.status(400).json({ error: 'User already exists' }); | |
} | |
// Hash password | |
const saltRounds = 10; | |
const passwordHash = await bcrypt.hash(password, saltRounds); | |
// Generate verification token | |
const verificationToken = uuidv4(); | |
// Create user | |
const result = await database.run( | |
'INSERT INTO users (email, name, password_hash, verification_token) VALUES (?, ?, ?, ?)', | |
[email, name, passwordHash, verificationToken] | |
); | |
// Create default tenant for user | |
const tenantResult = await database.run( | |
'INSERT INTO tenants (name, subdomain) VALUES (?, ?)', | |
[`${name}'s Workspace`, `tenant-${result.lastID}`] | |
); | |
// Link user to tenant | |
await database.run( | |
'INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)', | |
[result.lastID, tenantResult.lastID, 'owner'] | |
); | |
// Generate JWT token | |
const token = jwt.sign( | |
{ userId: result.lastID, tenantId: tenantResult.lastID }, | |
process.env.JWT_SECRET!, | |
{ expiresIn: '7d' } | |
); | |
// Get user data | |
const user = await database.get( | |
'SELECT id, email, name, avatar, created_at FROM users WHERE id = ?', | |
[result.lastID] | |
); | |
res.status(201).json({ | |
message: 'User created successfully', | |
token, | |
user, | |
tenant: { id: tenantResult.lastID, name: `${name}'s Workspace` } | |
}); | |
} catch (error) { | |
console.error('Signup error:', error); | |
res.status(500).json({ error: 'Internal server error' }); | |
} | |
}); | |
// Sign in with email/password | |
router.post('/signin', async (req, res) => { | |
try { | |
const { email, password } = req.body; | |
if (!email || !password) { | |
return res.status(400).json({ error: 'Email and password are required' }); | |
} | |
// Get user | |
const user = await database.get( | |
'SELECT * FROM users WHERE email = ?', | |
[email] | |
); | |
if (!user || !user.password_hash) { | |
return res.status(401).json({ error: 'Invalid credentials' }); | |
} | |
// Check password | |
const isValidPassword = await bcrypt.compare(password, user.password_hash); | |
if (!isValidPassword) { | |
return res.status(401).json({ error: 'Invalid credentials' }); | |
} | |
// Get user's tenant | |
const userTenant = await database.get( | |
`SELECT t.id, t.name, t.subdomain, t.plan | |
FROM tenants t | |
JOIN user_tenants ut ON t.id = ut.tenant_id | |
WHERE ut.user_id = ? AND ut.role = 'owner'`, | |
[user.id] | |
); | |
// Generate JWT token | |
const token = jwt.sign( | |
{ userId: user.id, tenantId: userTenant?.id }, | |
process.env.JWT_SECRET!, | |
{ expiresIn: '7d' } | |
); | |
// Remove sensitive data | |
const { password_hash, verification_token, ...safeUser } = user; | |
res.json({ | |
message: 'Signed in successfully', | |
token, | |
user: safeUser, | |
tenant: userTenant | |
}); | |
} catch (error) { | |
console.error('Signin error:', error); | |
res.status(500).json({ error: 'Internal server error' }); | |
} | |
}); | |
// Google OAuth | |
router.post('/google', async (req, res) => { | |
try { | |
const { credential } = req.body; | |
if (!credential) { | |
return res.status(400).json({ error: 'Google credential is required' }); | |
} | |
// Verify Google token | |
const ticket = await client.verifyIdToken({ | |
idToken: credential, | |
audience: process.env.GOOGLE_CLIENT_ID, | |
}); | |
const payload = ticket.getPayload(); | |
if (!payload) { | |
return res.status(400).json({ error: 'Invalid Google token' }); | |
} | |
const { sub: googleId, email, name, picture } = payload; | |
// Check if user exists | |
let user = await database.get('SELECT * FROM users WHERE google_id = ? OR email = ?', [googleId, email]); | |
if (user) { | |
// Update Google ID if needed | |
if (!user.google_id) { | |
await database.run('UPDATE users SET google_id = ?, avatar = ? WHERE id = ?', [googleId, picture, user.id]); | |
user.google_id = googleId; | |
user.avatar = picture; | |
} | |
} else { | |
// Create new user | |
const result = await database.run( | |
'INSERT INTO users (email, name, google_id, avatar, email_verified) VALUES (?, ?, ?, ?, ?)', | |
[email, name, googleId, picture, true] | |
); | |
// Create default tenant | |
const tenantResult = await database.run( | |
'INSERT INTO tenants (name, subdomain) VALUES (?, ?)', | |
[`${name}'s Workspace`, `tenant-${result.lastID}`] | |
); | |
// Link user to tenant | |
await database.run( | |
'INSERT INTO user_tenants (user_id, tenant_id, role) VALUES (?, ?, ?)', | |
[result.lastID, tenantResult.lastID, 'owner'] | |
); | |
user = { | |
id: result.lastID, | |
email, | |
name, | |
google_id: googleId, | |
avatar: picture, | |
email_verified: true | |
}; | |
} | |
// Get user's tenant | |
const userTenant = await database.get( | |
`SELECT t.id, t.name, t.subdomain, t.plan | |
FROM tenants t | |
JOIN user_tenants ut ON t.id = ut.tenant_id | |
WHERE ut.user_id = ? AND ut.role = 'owner'`, | |
[user.id] | |
); | |
// Generate JWT token | |
const token = jwt.sign( | |
{ userId: user.id, tenantId: userTenant?.id }, | |
process.env.JWT_SECRET!, | |
{ expiresIn: '7d' } | |
); | |
// Remove sensitive data | |
const { password_hash, verification_token, ...safeUser } = user; | |
res.json({ | |
message: 'Google authentication successful', | |
token, | |
user: safeUser, | |
tenant: userTenant | |
}); | |
} catch (error) { | |
console.error('Google auth error:', error); | |
res.status(500).json({ error: 'Google authentication failed' }); | |
} | |
}); | |
// Get current user | |
router.get('/me', authenticateToken, async (req, res) => { | |
try { | |
const { userId, tenantId } = (req as any).user; | |
// Get user data | |
const user = await database.get( | |
'SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?', | |
[userId] | |
); | |
if (!user) { | |
return res.status(404).json({ error: 'User not found' }); | |
} | |
// Get tenant data | |
const tenant = await database.get( | |
'SELECT id, name, subdomain, plan, settings FROM tenants WHERE id = ?', | |
[tenantId] | |
); | |
res.json({ | |
user, | |
tenant | |
}); | |
} catch (error) { | |
console.error('Get user error:', error); | |
res.status(500).json({ error: 'Internal server error' }); | |
} | |
}); | |
// Update user profile | |
router.put('/profile', authenticateToken, async (req, res) => { | |
try { | |
const { userId } = (req as any).user; | |
const { name, avatar } = req.body; | |
const updates = []; | |
const params = []; | |
if (name) { | |
updates.push('name = ?'); | |
params.push(name); | |
} | |
if (avatar) { | |
updates.push('avatar = ?'); | |
params.push(avatar); | |
} | |
if (updates.length === 0) { | |
return res.status(400).json({ error: 'No valid fields to update' }); | |
} | |
updates.push('updated_at = CURRENT_TIMESTAMP'); | |
params.push(userId); | |
await database.run( | |
`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, | |
params | |
); | |
// Get updated user | |
const user = await database.get( | |
'SELECT id, email, name, avatar, email_verified, created_at FROM users WHERE id = ?', | |
[userId] | |
); | |
res.json({ | |
message: 'Profile updated successfully', | |
user | |
}); | |
} catch (error) { | |
console.error('Update profile error:', error); | |
res.status(500).json({ error: 'Internal server error' }); | |
} | |
}); | |
// Logout (client-side token removal, but we can blacklist if needed) | |
router.post('/logout', authenticateToken, (req, res) => { | |
res.json({ message: 'Logged out successfully' }); | |
}); | |
export default router; |