flwo / apps /server /src /modules /auth /auth.service.js
moustafa7's picture
fix: separate client/admin refresh cookies to prevent cross-session takeover on same browser
6001748
Raw History Blame Contribute Delete
5.14 kB
import bcrypt from 'bcryptjs'
import { createHmac } from 'crypto'
import { prisma } from '../../config/database.js'
import { signAccessToken, signRefreshToken, verifyRefreshToken, blacklistToken } from '../../lib/jwt.js'
import { AppError } from '../../lib/AppError.js'
import { sendEmail, WELCOME } from '../../lib/email.js'
import { config } from '../../config/index.js'
import { SAFE_USER_SELECT } from '../../lib/constants.js'
const BCRYPT_ROUNDS = 12
const TIMING_DUMMY_HASH = bcrypt.hashSync('dummy-timing-attack-protection', BCRYPT_ROUNDS)
export function hashToken(token) {
return createHmac('sha256', config.TOKEN_HASH_SECRET)
.update(token)
.digest('hex')
}
function parseExpires(expiresStr) {
const match = expiresStr.match(/^(\d+)([dhms])$/)
if (!match) return 7 * 24 * 60 * 60 * 1000
const num = parseInt(match[1])
const unit = match[2]
const mul = { d: 86400000, h: 3600000, m: 60000, s: 1000 }
return num * (mul[unit] || 86400000)
}
async function createTokenPair(userId, role) {
const accessToken = signAccessToken(userId, role)
const refreshToken = signRefreshToken(userId)
const hashedRefresh = hashToken(refreshToken)
const expiresAt = new Date(Date.now() + parseExpires(config.JWT_REFRESH_EXPIRES))
await prisma.refreshToken.create({
data: { token: hashedRefresh, userId, expiresAt }
})
return { accessToken, refreshToken }
}
export async function registerUser(data, avatarUrl = null) {
const existing = await prisma.user.findFirst({
where: { OR: [{ email: data.email }, { phone: data.phone }] },
select: { id: true }
})
if (existing) {
throw new AppError('البيانات المدخلة مستخدمة بالفعل', 409, 'DUPLICATE_ENTRY')
}
const passwordHash = await bcrypt.hash(data.password, BCRYPT_ROUNDS)
const user = await prisma.user.create({
data: {
firstName: data.firstName,
lastName: data.lastName,
phone: data.phone,
email: data.email,
passwordHash,
avatarUrl,
role: 'CLIENT'
},
select: SAFE_USER_SELECT,
})
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
sendEmail(data.email, WELCOME(user)).catch(() => {})
return { user, accessToken, refreshToken }
}
export async function loginUser(email, password) {
const user = await prisma.user.findUnique({
where: { email },
select: { ...SAFE_USER_SELECT, passwordHash: true }
})
const isValid = await bcrypt.compare(password, user?.passwordHash || TIMING_DUMMY_HASH)
if (!user || !isValid || !user.isActive) {
throw new AppError('بيانات تسجيل الدخول غير صحيحة', 401, 'INVALID_CREDENTIALS')
}
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
const { passwordHash, ...safeUser } = user
return { user: safeUser, accessToken, refreshToken }
}
export async function refreshUserToken(oldRefreshToken, expectClient = true) {
if (!oldRefreshToken) throw new AppError('لم يتم توفير refresh token', 401, 'NO_REFRESH_TOKEN')
let decoded
try { decoded = verifyRefreshToken(oldRefreshToken) }
catch { throw new AppError('Refresh token منتهي أو غير صحيح', 401, 'INVALID_REFRESH_TOKEN') }
const hashedOld = hashToken(oldRefreshToken)
const stored = await prisma.refreshToken.findUnique({ where: { token: hashedOld } })
if (!stored || stored.expiresAt < new Date()) {
if (stored) await prisma.refreshToken.deleteMany({ where: { userId: decoded.sub } })
throw new AppError('Refresh token غير صحيح', 401, 'REFRESH_TOKEN_REUSE')
}
await prisma.refreshToken.delete({ where: { token: hashedOld } })
const user = await prisma.user.findUnique({
where: { id: decoded.sub },
select: SAFE_USER_SELECT,
})
if (!user || !user.isActive) {
throw new AppError('بيانات تسجيل الدخول غير صحيحة', 401, 'INVALID_CREDENTIALS')
}
if (expectClient && user.role !== 'CLIENT') {
throw new AppError('نوع الحساب غير متطابق', 401, 'ROLE_MISMATCH')
}
const { accessToken, refreshToken } = await createTokenPair(user.id, user.role)
const isAdmin = user.role !== 'CLIENT'
return { user, accessToken, refreshToken, isAdmin }
}
export async function changeUserPassword(userId, currentPassword, newPassword) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { passwordHash: true }
})
if (!user) throw new AppError('المستخدم غير موجود', 404, 'USER_NOT_FOUND')
const isValid = await bcrypt.compare(currentPassword, user.passwordHash)
if (!isValid) throw new AppError('كلمة المرور الحالية غير صحيحة', 401, 'INVALID_PASSWORD')
const passwordHash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS)
await prisma.$transaction([
prisma.user.update({ where: { id: userId }, data: { passwordHash } }),
prisma.refreshToken.deleteMany({ where: { userId } }),
])
}
export async function logoutUser(userId, accessToken) {
if (accessToken) await blacklistToken(accessToken)
await prisma.refreshToken.deleteMany({ where: { userId } })
}