import dotenv from 'dotenv'; import express from 'express'; import cors from 'cors'; import mysql from 'mysql2/promise'; import bcrypt from 'bcryptjs'; import argon2 from 'argon2'; dotenv.config(); const app = express(); const port = 3010; app.use( cors({ origin(origin, callback) { if (!origin) { callback(null, true); return; } callback(null, true); } }) ); app.use(express.json()); const pool = mysql.createPool({ host: process.env.DB_HOST || 'nw.sgcode.cn', port: Number(process.env.DB_PORT || 21434), user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || 'root', database: process.env.DB_NAME || 'opencoze', waitForConnections: true, connectionLimit: 10 }); async function ensureUsersTable() { await pool.query(` CREATE TABLE IF NOT EXISTS \`user\` ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(128) NOT NULL DEFAULT '', unique_name VARCHAR(128) NOT NULL UNIQUE, email VARCHAR(128) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL DEFAULT '', description VARCHAR(512) NOT NULL DEFAULT '', icon_uri VARCHAR(512) NOT NULL DEFAULT '', user_verified TINYINT(1) NOT NULL DEFAULT 0, locale VARCHAR(128) NOT NULL DEFAULT '', session_key VARCHAR(256) NOT NULL DEFAULT '', created_at BIGINT UNSIGNED NOT NULL DEFAULT 0, updated_at BIGINT UNSIGNED NOT NULL DEFAULT 0, deleted_at BIGINT UNSIGNED NULL DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); } app.get('/api/health', async (_req, res) => { try { await pool.query('SELECT 1'); res.json({ ok: true }); } catch (error) { console.error('Health check failed:', error); res.status(500).json({ ok: false, message: 'Database connection failed' }); } }); app.post('/api/register', async (req, res) => { let connection; try { const { email, password, confirmPassword } = req.body ?? {}; if (!email || !password || !confirmPassword) { res.status(400).json({ message: 'Email, password, and confirm password are required' }); return; } if (password !== confirmPassword) { res.status(400).json({ message: 'Passwords do not match' }); return; } const trimmedEmail = String(email).trim().toLowerCase(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { res.status(400).json({ message: 'Please enter a valid email address' }); return; } if (String(password).length < 6) { res.status(400).json({ message: 'Password must be at least 6 characters' }); return; } const [existingRows] = await pool.query('SELECT id FROM `user` WHERE email = ? LIMIT 1', [trimmedEmail]); if (existingRows.length > 0) { res.status(409).json({ message: 'Email is already registered' }); return; } // New users are stored with Argon2id hashes. const passwordHash = await argon2.hash(String(password), { type: argon2.argon2id }); const uniqueName = trimmedEmail; const displayName = trimmedEmail.split('@')[0]; const now = Date.now(); const defaultIconUri = 'default_icon/user_default_icon.png'; const personalSpaceName = 'Personal Space'; const personalSpaceDescription = 'This is your personal space'; connection = await pool.getConnection(); await connection.beginTransaction(); await connection.query( 'INSERT INTO `user` (name, unique_name, email, password, description, icon_uri, user_verified, locale, session_key, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [displayName, uniqueName, trimmedEmail, passwordHash, '', defaultIconUri, 0, 'zh-CN', '', now, now, null] ); const [createdUserRows] = await connection.query('SELECT CAST(id AS CHAR) AS id FROM `user` WHERE email = ? LIMIT 1', [ trimmedEmail ]); const userId = createdUserRows[0]?.id; if (!userId) { throw new Error('Failed to resolve created user ID'); } await connection.query( 'INSERT INTO `space` (owner_id, name, description, icon_uri, creator_id, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [userId, personalSpaceName, personalSpaceDescription, defaultIconUri, userId, now, now, null] ); const [createdSpaceRows] = await connection.query( 'SELECT CAST(id AS CHAR) AS id FROM `space` WHERE owner_id = ? ORDER BY created_at DESC, id DESC LIMIT 1', [userId] ); const spaceId = createdSpaceRows[0]?.id; if (!spaceId) { throw new Error('Failed to resolve created space ID'); } await connection.query( 'INSERT INTO `space_user` (space_id, user_id, role_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [spaceId, userId, 1, now, now] ); await connection.commit(); res.status(201).json({ message: 'Registered successfully' }); } catch (error) { if (connection) { await connection.rollback(); } console.error('Register failed:', error); res.status(500).json({ message: 'Register failed' }); } finally { if (connection) { connection.release(); } } }); app.post('/api/login', async (req, res) => { try { const { email, password } = req.body ?? {}; if (!email || !password) { res.status(400).json({ message: 'Email and password are required' }); return; } const trimmedEmail = String(email).trim().toLowerCase(); const [rows] = await pool.query('SELECT id, email, password FROM `user` WHERE email = ? LIMIT 1', [trimmedEmail]); const user = rows[0]; if (!user) { res.status(401).json({ message: 'Invalid email or password' }); return; } const rawPassword = String(password); const storedPassword = String(user.password || ''); let isPasswordValid = false; if (storedPassword.startsWith('$argon2')) { isPasswordValid = await argon2.verify(storedPassword, rawPassword); } else if (storedPassword.startsWith('$2')) { isPasswordValid = await bcrypt.compare(rawPassword, storedPassword); } if (!isPasswordValid) { res.status(401).json({ message: 'Invalid email or password' }); return; } res.json({ message: 'Login successful', user: { id: user.id, email: user.email } }); } catch (error) { console.error('Login failed:', error); res.status(500).json({ message: 'Login failed' }); } }); ensureUsersTable() .then(() => { app.listen(port, () => { console.log(`Auth API running at http://localhost:${port}`); }); }) .catch((error) => { console.error('Failed to initialize database:', error); process.exit(1); });