import db from "./database"; import express from 'express'; import { encrypt, decrypt, randomString } from './encryption' import sendEmail from "./mailer"; const app = express.Router(); const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ export const encryptResponse = (req: any, res: any, data: object | string, status = 200) => { if (typeof data === 'object') { data = JSON.stringify(data) } res.status(status).text(`~e~${encrypt(data, req.key, req.iv)}`) } const createToken = async (user_id: number): Promise<[string, string, string]> => { return new Promise((resolve, reject) => { const key = randomString(32) const iv = randomString(16) const nonce = randomString(64) const token = encrypt(nonce, key, iv) db.get(`insert into auth_tokens (token, encryption_key, encryption_iv, user_id) values (?, ?, ?, ?)`, [token, key, iv, user_id], (err) => { if (err) return reject(err) resolve([token, key, iv]) }) }) } export const findKeyAndIV = async (token: string): Promise<{ key: string, iv: string }> => { return new Promise((resolve, reject) => { db.get(`select encryption_key, encryption_iv from auth_tokens where token = ?`, [token], (err, row) => { if (err) return reject(err) if (!row) return reject('Token not found') const { encryption_key: key, encryption_iv: iv } = row as { encryption_key: string, encryption_iv: string } resolve({ key, iv }) }) }) } export const decryptMessage = async (message: string, token: string) => { const { key, iv } = await findKeyAndIV(token) return decrypt(message, key, iv) } export const checkAuth = async (req: any, res: any, next: any) => { if (!req.headers.authorization) return res.status(401).json({ error: 'Unauthorized' }) const token = req.headers.authorization.split(' ')[1] const { key, iv } = await findKeyAndIV(token).catch(() => ({ key: null, iv: null })) if (!key || !iv) return res.status(401).json({ error: 'Unauthorized' }) if (typeof req.body === 'string' && req.body.startsWith('~e~')) { try { const decrypted = decrypt(req.body.substring(3), key, iv) try { req.body = JSON.parse(decrypted) } catch { req.body = decrypted } } catch { } } req.key = key req.iv = iv next() } app.post('/login', (req, res) => { const { username, password } = req.body if (!username || !password) return res.status(400).json({ error: 'Bad Request' }) const u = username.toLowerCase().trim() const p = password.trim() try { db.get(`select id from users where (username = $0 or email = $0) and password = $1`, [u, encrypt(p, process.env.KEY, process.env.IV)], async (err, row: any) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (!row) return res.status(401).json({ error: 'Unauthorized' }) const id = row.id as number const [token, key, iv] = await createToken(id) res.status(200).json({ token, key, iv }) }) } catch (e) { console.error(e) res.status(500).json({ error: 'Internal Server Error' }) } }) app.post('/register', (req, res) => { const { username, password, email } = req.body if (!username || !password || !email) return res.status(400).json({ error: 'Bad Request' }) const p = password.trim() const e = email.toLowerCase().trim() const u = username.toLowerCase().trim() if (!passwordRegex.test(p)) return res.status(400).json({ error: 'Password must be at least 12 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol' }) if (!emailRegex.test(e)) return res.status(400).json({ error: 'Invalid email' }) try { db.get(`insert into users (username, password, email) values (?, ?, ?) returning *`, [u, encrypt(p, process.env.KEY, process.env.IV), e], async (err: any, result: any) => { if (err) { console.error(err) return res.status(500).json({ error: 'Internal Server Error' }) } if (!result) return res.status(500).json({ error: 'Failed to register, perhaps that username is taken.' }) const { id } = result const [token, key, iv] = await createToken(id) res.status(201).json({ token, key, iv }) }) } catch (e) { console.error(e) res.status(500).json({ error: 'Internal Server Error' }) } }) app.post('/invalidate', checkAuth, (req, res) => { const token = req.headers.authorization?.split(' ')[1] if (!token) return res.status(400).json({ error: 'Bad Request' }) db.run(`delete from auth_tokens where token = ?`, [token], (err) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) res.status(200).json({ message: 'Token invalidated' }) }) }) app.post('/reset-password', (req, res) => { const { email } = req.body if (!email) return res.status(400).json({ error: 'Bad Request' }) const e = email.toLowerCase().trim() if (!emailRegex.test(e)) return res.status(400).json({ error: 'Invalid email' }) db.get(`select id from users where email = ?`, [e], async (err: any, row: { id: number }) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (!row) return res.status(404).json({ error: 'Email not found' }) const { id } = row const token = randomString(6) db.run(`insert into password_resets (user_id, token, expires) values (?, ?, ?)`, [id, token, Date.now() + (1000 * 60 * 15)], (err) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) sendEmail(e, `Quill Keep Password Reset Request: ${token}`, `Someone (hopefully you) has requested a password reset for your account. If this was you, please click the link below to reset your password. If you did not request this, please ignore this email. You have 15 minutes to reset your password before this code. CODE: ${token} Enter the code above or click this link to reset your password: ${process.env.FRONTEND_URL}/reset-password?token=${token}&email=${e}`).then(() => { res.status(200).json({}) }).catch((e) => { console.error(e) res.status(500).json({ error: 'Internal Server Error' }) }) }) }) }) app.post('/verify-code', (req, res) => { const { email, code } = req.body if (!email || !code) return res.status(400).json({ error: 'Bad Request' }) const e = email.toLowerCase().trim() if (!emailRegex.test(e)) return res.status(400).json({ error: 'Invalid email' }) db.get(`select user_id, expires from password_resets where token = ? and expires > ?`, [code, Date.now()], (err: any, row: { user_id: number, expires: number }) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (!row) return res.status(404).json({ error: 'Token not found or expired' }) if (Date.now() > row.expires) return res.status(404).json({ error: 'Token expired' }) const { user_id } = row db.get(`select email from users where id = ?`, [user_id], (err: any, row: { email: string }) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (row.email !== e) return res.status(404).json({ error: 'Token not found or expired' }) db.run(`delete from password_resets where token = ?`, [code], (err: any) => { if (err) console.error('Failed to remove reset token:', err) const token = randomString(32) db.run(`insert into password_allowances (user_id, token, expires) values (?, ?, ?)`, [user_id, token, Date.now() + 1000 * 60 * 60], (err) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) res.status(200).json({ token }) }) }) }) }) }) app.post('/finalize-password', (req, res) => { const { email, token, password } = req.body if (!token || !password || !email) return res.status(400).json({ error: 'Bad Request' }) if (!passwordRegex.test(password)) return res.status(400).json({ error: 'Password must be at least 12 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol' }) const e = email.toLowerCase().trim() if (!emailRegex.test(e)) return res.status(400).json({ error: 'Invalid email' }) db.get(`select user_id from password_allowances where token = ? and expires > ?`, [token, Date.now()], (err, row: { user_id: number }) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (!row) return res.status(404).json({ error: 'Token not found or expired' }) const { user_id } = row db.get(`select email from users where id = ?`, [user_id], (err, row: { email: string }) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) if (row.email !== e) return res.status(404).json({ error: 'Token not found or expired' }) db.run(`delete from password_allowances where token = ?`, [token], (err) => { if (err) console.error('Failed to remove password allowance:', err) db.run(`update users set password = ? where id = ?`, [encrypt(password, process.env.KEY, process.env.IV), user_id], (err) => { if (err) return res.status(500).json({ error: 'Internal Server Error' }) createToken(user_id).then(([token, key, iv]) => { res.status(200).json({ token, key, iv }) }).catch((e) => { console.error(e) res.status(200).json({}) }) }) }) }) }) }) export default app