// services/auth.service.js const bcrypt = require("bcryptjs") const jwt = require("jsonwebtoken") const crypto = require("crypto") const authDb = require("../db/auth.db") const refreshTokenDb = require("../db/refreshToken.db") const { queueAuditEvent } = require("./redis/dbSync.service") const { AUDIT_ACTIONS } = require("./auditActions") const { buildAuditMeta } = require("./audit.service") const { sanitizeOptionalPlainText } = require("../utils/inputSanitizer") const { normalizeMediaPath } = require("../utils/mediaPath") const REUSE_GRACE_MS = Number(process.env.REFRESH_REUSE_GRACE_MS || 10000) function httpError(statusCode, message) { const err = new Error(message) err.statusCode = statusCode return err } // Access token: kisa ömür function signAccessToken(user) { const jti = crypto.randomUUID() const payload = { sub: String(user.id), role: user.role, // USER|MOD|ADMIN jti, } const expiresIn = process.env.ACCESS_TOKEN_EXPIRES_IN || "15m" const token = jwt.sign(payload, process.env.JWT_ACCESS_SECRET, { expiresIn }) return { token, jti } } // Refresh token: opaque (JWT degil) + DB'de hash function generateRefreshToken() { // 64 byte -> url-safe base64 return crypto.randomBytes(64).toString("base64url") } function hashToken(token) { return crypto.createHash("sha256").update(token).digest("hex") } function refreshExpiresAt() { const days = Number(process.env.REFRESH_TOKEN_DAYS || 30) return new Date(Date.now() + days * 24 * 60 * 60 * 1000) } function mapUserPublic(user) { return { id: user.id, username: user.username, email: user.email, avatarUrl: normalizeMediaPath(user.avatarUrl) ?? null, role: user.role, } } async function login({ email, password, meta = {} }) { const user = await authDb.findUserByEmail(email) if (!user) throw httpError(400, "Kullanici bulunamadi.") if (user.disabledAt) throw httpError(403, "Hesap devre disi.") const isMatch = await bcrypt.compare(password, user.passwordHash) if (!isMatch) throw httpError(401, "Sifre hatali.") const { token: accessToken } = signAccessToken(user) const refreshToken = generateRefreshToken() const tokenHash = hashToken(refreshToken) const familyId = crypto.randomUUID() const jti = crypto.randomUUID() await refreshTokenDb.createRefreshToken(user.id, { tokenHash, familyId, jti, expiresAt: refreshExpiresAt(), createdByIp: meta.ip ?? null, userAgent: meta.userAgent ?? null, }) queueAuditEvent({ userId: user.id, action: AUDIT_ACTIONS.AUTH.LOGIN_SUCCESS, ip: meta.ip ?? null, userAgent: meta.userAgent ?? null, meta: buildAuditMeta({ entityType: "USER", entityId: user.id }), createdAt: new Date().toISOString(), }).catch((err) => console.error("Audit queue login failed:", err?.message || err)) return { accessToken, refreshToken, user: mapUserPublic(user), } } async function register({ username, email, password, meta = {} }) { const existingUser = await authDb.findUserByEmail(email) if (existingUser) throw httpError(400, "Bu e-posta zaten kayitli.") const normalizedUsername = sanitizeOptionalPlainText(username, { maxLength: 18 }) if (!normalizedUsername || normalizedUsername.length < 5) { throw httpError(400, "Kullanici adi gecersiz.") } const passwordHash = await bcrypt.hash(password, 10) const user = await authDb.createUser({ username: normalizedUsername, email, passwordHash }) const { token: accessToken } = signAccessToken(user) const refreshToken = generateRefreshToken() const tokenHash = hashToken(refreshToken) const familyId = crypto.randomUUID() const jti = crypto.randomUUID() await refreshTokenDb.createRefreshToken(user.id, { tokenHash, familyId, jti, expiresAt: refreshExpiresAt(), createdByIp: meta.ip ?? null, userAgent: meta.userAgent ?? null, }) queueAuditEvent({ userId: user.id, action: AUDIT_ACTIONS.AUTH.REGISTER, ip: meta.ip ?? null, userAgent: meta.userAgent ?? null, meta: buildAuditMeta({ entityType: "USER", entityId: user.id }), createdAt: new Date().toISOString(), }).catch((err) => console.error("Audit queue register failed:", err?.message || err)) return { accessToken, refreshToken, user: mapUserPublic(user), } } // Refresh: rotate + reuse tespiti async function refresh({ refreshToken, meta = {} }) { if (!refreshToken) throw httpError(401, "Refresh token yok") const tokenHash = hashToken(refreshToken) const existing = await refreshTokenDb.findRefreshTokenByHash(tokenHash, { include: { user: true }, }) if (!existing) throw httpError(401, "Refresh token geçersiz") // süresi geçmis if (existing.expiresAt && existing.expiresAt.getTime() < Date.now()) { await refreshTokenDb.revokeRefreshTokenById(existing.id) throw httpError(401, "Refresh token süresi dolmus") } // reuse tespiti: revoke edilmis token tekrar gelirse -> tüm aileyi kapat if (existing.revokedAt) { const revokedAt = existing.revokedAt instanceof Date ? existing.revokedAt : new Date(existing.revokedAt) const withinGrace = existing.replacedById && revokedAt && Date.now() - revokedAt.getTime() <= REUSE_GRACE_MS if (!withinGrace) { await refreshTokenDb.revokeRefreshTokenFamily(existing.familyId) throw httpError(401, "Refresh token reuse tespit edildi") } } const user = existing.user if (user?.disabledAt) throw httpError(403, "Hesap devre disi.") const { token: accessToken } = signAccessToken(user) const newRefreshToken = generateRefreshToken() const newTokenHash = hashToken(newRefreshToken) const newJti = crypto.randomUUID() await refreshTokenDb.rotateRefreshToken({ oldId: existing.id, newToken: { userId: user.id, tokenHash: newTokenHash, familyId: existing.familyId, // ayni aile jti: newJti, expiresAt: refreshExpiresAt(), }, meta: { ip: meta.ip ?? null, userAgent: meta.userAgent ?? null }, }) queueAuditEvent({ userId: user.id, action: AUDIT_ACTIONS.AUTH.REFRESH, ip: meta.ip ?? null, userAgent: meta.userAgent ?? null, meta: buildAuditMeta({ entityType: "USER", entityId: user.id }), createdAt: new Date().toISOString(), }).catch((err) => console.error("Audit queue refresh failed:", err?.message || err)) return { accessToken, refreshToken: newRefreshToken, user: mapUserPublic(user), } } async function logout({ refreshToken, meta = {} }) { if (!refreshToken) return const tokenHash = hashToken(refreshToken) // token yoksa sessiz geçmek genelde daha iyi (idempotent logout) try { const existing = await refreshTokenDb.findRefreshTokenByHash(tokenHash, { select: { userId: true }, }) await refreshTokenDb.revokeRefreshTokenByHash(tokenHash) if (existing?.userId) { queueAuditEvent({ userId: existing.userId, action: AUDIT_ACTIONS.AUTH.LOGOUT, ip: meta.ip ?? null, userAgent: meta.userAgent ?? null, meta: buildAuditMeta({ entityType: "USER", entityId: existing.userId }), createdAt: new Date().toISOString(), }).catch((err) => console.error("Audit queue logout failed:", err?.message || err)) } } catch (_) {} } async function getMe(userId) { const user = await authDb.findUserById(Number(userId), { select: { id: true, username: true, email: true, avatarUrl: true, role: true }, }) if (!user) throw httpError(404, "Kullanici bulunamadi") return user } module.exports = { login, register, refresh, logout, getMe, }