File size: 5,138 Bytes
24727a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6001748
24727a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6001748
 
 
24727a3
6001748
 
24727a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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 } })
}