188 lines
4.9 KiB
JavaScript
188 lines
4.9 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")
|
||
|
||
function httpError(statusCode, message) {
|
||
const err = new Error(message)
|
||
err.statusCode = statusCode
|
||
return err
|
||
}
|
||
|
||
// Access token: kısa ö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 değil) + 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: user.avatarUrl ?? null,
|
||
role: user.role,
|
||
}
|
||
}
|
||
|
||
async function login({ email, password, meta = {} }) {
|
||
const user = await authDb.findUserByEmail(email)
|
||
if (!user) throw httpError(400, "Kullanıcı bulunamadı.")
|
||
|
||
const isMatch = await bcrypt.compare(password, user.passwordHash)
|
||
if (!isMatch) throw httpError(401, "Şifre hatalı.")
|
||
|
||
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,
|
||
})
|
||
|
||
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 kayıtlı.")
|
||
|
||
const passwordHash = await bcrypt.hash(password, 10)
|
||
const user = await authDb.createUser({ username, 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,
|
||
})
|
||
|
||
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çmiş
|
||
if (existing.expiresAt && existing.expiresAt.getTime() < Date.now()) {
|
||
await refreshTokenDb.revokeRefreshTokenById(existing.id)
|
||
throw httpError(401, "Refresh token süresi dolmuş")
|
||
}
|
||
|
||
// reuse tespiti: revoke edilmiş token tekrar gelirse -> tüm aileyi kapat
|
||
if (existing.revokedAt) {
|
||
await refreshTokenDb.revokeRefreshTokenFamily(existing.familyId)
|
||
throw httpError(401, "Refresh token reuse tespit edildi")
|
||
}
|
||
|
||
const user = existing.user
|
||
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, // aynı aile
|
||
jti: newJti,
|
||
expiresAt: refreshExpiresAt(),
|
||
},
|
||
meta: { ip: meta.ip ?? null, userAgent: meta.userAgent ?? null },
|
||
})
|
||
|
||
return {
|
||
accessToken,
|
||
refreshToken: newRefreshToken,
|
||
user: mapUserPublic(user),
|
||
}
|
||
}
|
||
|
||
async function logout({ refreshToken }) {
|
||
if (!refreshToken) return
|
||
const tokenHash = hashToken(refreshToken)
|
||
|
||
// token yoksa sessiz geçmek genelde daha iyi (idempotent logout)
|
||
try {
|
||
await refreshTokenDb.revokeRefreshTokenByHash(tokenHash)
|
||
} 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, "Kullanıcı bulunamadı")
|
||
return user
|
||
}
|
||
|
||
module.exports = {
|
||
login,
|
||
register,
|
||
refresh,
|
||
logout,
|
||
getMe,
|
||
}
|