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,4 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { sendLoginCodeEmail } from './email.js'
|
||||
import { prisma } from './prisma.js'
|
||||
|
||||
@@ -72,3 +73,34 @@ export async function verifyEmailCode({ email, purpose, code, userId = null }) {
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_LEN = 8
|
||||
|
||||
const PASSWORD_REGEX = {
|
||||
letter: /[a-zа-яё]/i,
|
||||
digit: /[0-9]/,
|
||||
special: /[^a-zа-яё0-9\s]/i,
|
||||
}
|
||||
|
||||
export function validatePassword(password) {
|
||||
if (typeof password !== 'string') return 'Пароль обязателен'
|
||||
if (password.length < PASSWORD_MIN_LEN) return `Пароль должен быть не менее ${PASSWORD_MIN_LEN} символов`
|
||||
if (!PASSWORD_REGEX.letter.test(password)) return 'Пароль должен содержать хотя бы одну букву'
|
||||
if (!PASSWORD_REGEX.digit.test(password)) return 'Пароль должен содержать хотя бы одну цифру'
|
||||
if (!PASSWORD_REGEX.special.test(password)) return 'Пароль должен содержать хотя бы один спецсимвол'
|
||||
return null
|
||||
}
|
||||
|
||||
export async function hashPassword(password) {
|
||||
return bcrypt.hash(password, 10)
|
||||
}
|
||||
|
||||
export async function comparePassword(password, hash) {
|
||||
return bcrypt.compare(password, hash)
|
||||
}
|
||||
|
||||
export function isAdminEmail(email) {
|
||||
const adminEmail = process.env.ADMIN_EMAIL?.trim().toLowerCase()
|
||||
if (!adminEmail) return false
|
||||
return normalizeEmail(email) === adminEmail
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const windows = new Map()
|
||||
|
||||
const MAX_ATTEMPTS = 5
|
||||
const WINDOW_MS = 60_000
|
||||
|
||||
export function checkLoginRateLimit(ip) {
|
||||
const now = Date.now()
|
||||
const entry = windows.get(ip)
|
||||
if (!entry || now - entry.start > WINDOW_MS) {
|
||||
windows.set(ip, { start: now, count: 1 })
|
||||
return { allowed: true }
|
||||
}
|
||||
entry.count += 1
|
||||
if (entry.count > MAX_ATTEMPTS) {
|
||||
const retryAfter = Math.ceil((entry.start + WINDOW_MS - now) / 1000)
|
||||
return { allowed: false, retryAfter }
|
||||
}
|
||||
return { allowed: true }
|
||||
}
|
||||
Reference in New Issue
Block a user