HotTRDealsBackend/services/auth.service.js
2026-02-09 21:47:55 +00:00

250 lines
7.5 KiB
JavaScript

// 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,
}