feat(server): add POST /api/auth/register and /api/auth/login
- Add register endpoint with email/password validation, bcrypt hashing - Add login endpoint with rate limiting per IP (5 attempts/min) - Add helper functions: validatePassword, hashPassword, comparePassword, isAdminEmail - Add checkLoginRateLimit for brute-force protection - Add bcrypt dependency - Remove avatarType column from User (migration)
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { NOTIFICATION_EVENTS } from '../../../shared/constants/notification-events.js'
|
||||
import { issueEmailCode, normalizeEmail, verifyEmailCode } from '../lib/auth.js'
|
||||
import {
|
||||
comparePassword,
|
||||
hashPassword,
|
||||
isAdminEmail,
|
||||
issueEmailCode,
|
||||
normalizeEmail,
|
||||
validatePassword,
|
||||
verifyEmailCode,
|
||||
} from '../lib/auth.js'
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
import { checkLoginRateLimit } from '../lib/rate-limit.js'
|
||||
|
||||
function mapUserForClient(user) {
|
||||
const adminEmail = normalizeEmail(process.env.ADMIN_EMAIL)
|
||||
@@ -64,6 +73,72 @@ export async function registerAuthRoutes(fastify) {
|
||||
return { token, user: mapUserForClient(user) }
|
||||
})
|
||||
|
||||
fastify.post('/api/auth/register', async (request, reply) => {
|
||||
const email = normalizeEmail(request.body?.email)
|
||||
const password = String(request.body?.password || '')
|
||||
const displayNameRaw = request.body?.displayName
|
||||
const displayName = displayNameRaw ? String(displayNameRaw).trim().slice(0, 100) : email.split('@')[0]
|
||||
|
||||
if (!email || !email.includes('@')) return reply.code(400).send({ error: 'Некорректная почта' })
|
||||
if (isAdminEmail(email)) return reply.code(403).send({ error: 'Администратор не может регистрироваться с паролем' })
|
||||
|
||||
const passwordErr = validatePassword(password)
|
||||
if (passwordErr) return reply.code(400).send({ error: passwordErr })
|
||||
|
||||
const exists = await prisma.user.findUnique({ where: { email } })
|
||||
if (exists) return reply.code(409).send({ error: 'Эта почта уже зарегистрирована' })
|
||||
|
||||
const passwordHash = await hashPassword(password)
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash,
|
||||
displayName: displayName || null,
|
||||
avatar: null,
|
||||
avatarStyle: 'avataaars',
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.notificationPreference.upsert({
|
||||
where: { userId: user.id },
|
||||
create: { userId: user.id, globalEnabled: true },
|
||||
update: {},
|
||||
})
|
||||
|
||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||
return reply.code(201).send({ token, user: mapUserForClient(user) })
|
||||
})
|
||||
|
||||
fastify.post('/api/auth/login', async (request, reply) => {
|
||||
const email = normalizeEmail(request.body?.email)
|
||||
const password = String(request.body?.password || '')
|
||||
const ip = request.ip
|
||||
|
||||
if (!email || !email.includes('@')) return reply.code(400).send({ error: 'Некорректная почта' })
|
||||
if (isAdminEmail(email)) return reply.code(403).send({ error: 'Администратор входит только по коду' })
|
||||
|
||||
const rate = checkLoginRateLimit(ip)
|
||||
if (!rate.allowed) {
|
||||
return reply
|
||||
.code(429)
|
||||
.header('Retry-After', String(rate.retryAfter))
|
||||
.send({ error: `Слишком много попыток. Попробуйте через ${rate.retryAfter} сек.` })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } })
|
||||
if (!user || !user.passwordHash) {
|
||||
return reply.code(401).send({ error: 'Неверная почта или пароль' })
|
||||
}
|
||||
|
||||
const valid = await comparePassword(password, user.passwordHash)
|
||||
if (!valid) {
|
||||
return reply.code(401).send({ error: 'Неверная почта или пароль' })
|
||||
}
|
||||
|
||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||
return { token, user: mapUserForClient(user) }
|
||||
})
|
||||
|
||||
fastify.get('/api/me', { preHandler: [fastify.authenticate] }, async (request) => {
|
||||
const userId = request.user.sub
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||
|
||||
Reference in New Issue
Block a user