feat: registration and login with JWT auth
- users table: email, password_hash (bcrypt), role, is_active - POST /auth/register — checks blocklist, hashes password, returns JWT - POST /auth/login — verifies password, returns JWT - Auth middleware: accepts env tokens (dev) OR valid JWTs - end-user role → 403 Insufficient permissions on all /api/* routes - JWT_SECRET + JWT_EXPIRES_IN env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
src/routes/auth.js
Normal file
84
src/routes/auth.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const router = require('express').Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { query } = require('../db');
|
||||
|
||||
function signToken(user) {
|
||||
return jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
||||
);
|
||||
}
|
||||
|
||||
// POST /auth/register
|
||||
router.post('/register', async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password)
|
||||
return res.status(400).json({ error: 'email and password are required' });
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
||||
return res.status(400).json({ error: 'Invalid email format' });
|
||||
if (password.length < 8)
|
||||
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
|
||||
// Blocklist prüfen
|
||||
const blockCheck = await query(
|
||||
`SELECT id FROM blocklist WHERE is_blocked = true AND lower(email) = $1 LIMIT 1`,
|
||||
[normalizedEmail]
|
||||
);
|
||||
if (blockCheck.rows.length)
|
||||
return res.status(403).json({ error: 'Registration not allowed' });
|
||||
|
||||
// Bereits registriert?
|
||||
const existing = await query(
|
||||
`SELECT id FROM users WHERE lower(email) = $1`, [normalizedEmail]
|
||||
);
|
||||
if (existing.rows.length)
|
||||
return res.status(409).json({ error: 'Email already registered' });
|
||||
|
||||
const password_hash = await bcrypt.hash(password, 12);
|
||||
const result = await query(
|
||||
`INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email, role`,
|
||||
[normalizedEmail, password_hash]
|
||||
);
|
||||
|
||||
const user = result.rows[0];
|
||||
res.status(201).json({ user, token: signToken(user) });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// POST /auth/login
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password)
|
||||
return res.status(400).json({ error: 'email and password are required' });
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, email, role, password_hash, is_active FROM users WHERE lower(email) = $1`,
|
||||
[email.toLowerCase().trim()]
|
||||
);
|
||||
|
||||
if (!result.rows.length)
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
if (!user.is_active)
|
||||
return res.status(403).json({ error: 'Account deactivated' });
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid)
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
const { password_hash: _, is_active: __, ...safeUser } = user;
|
||||
res.json({ user: safeUser, token: signToken(safeUser) });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user