Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
T
trainee-schedule
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Đoàn Vũ Bình Dương
trainee-schedule
Commits
d505ed0d
Commit
d505ed0d
authored
Apr 13, 2026
by
Blockchain-vn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat/authentication
parent
4f023fe9
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
751 additions
and
1044 deletions
+751
-1044
login.ts
src/controllers/api/v1.0/auth/login.ts
+4
-5
logout.ts
src/controllers/api/v1.0/auth/logout.ts
+1
-1
profile.ts
src/controllers/api/v1.0/auth/profile.ts
+19
-20
refresh.ts
src/controllers/api/v1.0/auth/refresh.ts
+1
-8
register.ts
src/controllers/api/v1.0/auth/register.ts
+62
-0
auth.ts
src/middlewares/auth.ts
+5
-5
auth.ts
src/middlewares/validators/auth.ts
+3
-0
Role.ts
src/models/mongodb/Role.ts
+28
-0
User.ts
src/models/mongodb/User.ts
+67
-0
UserSession.ts
src/models/mongodb/UserSession.ts
+59
-0
seed-mongo.ts
src/scripts/seed-mongo.ts
+64
-0
authService.ts
src/services/authService.ts
+265
-815
schemas.js
src/templates/swagger/auth/schemas.js
+50
-6
responses.js
src/templates/swagger/common/responses.js
+65
-0
securitySchemes.js
src/templates/swagger/common/securitySchemes.js
+4
-4
authUtils.ts
src/utils/authUtils.ts
+54
-180
No files found.
src/controllers/api/v1.0/auth/login.ts
View file @
d505ed0d
...
@@ -102,18 +102,17 @@ export default (_express: Application) => {
...
@@ -102,18 +102,17 @@ export default (_express: Application) => {
return
res
.
sendOk
({
return
res
.
sendOk
({
data
:
{
data
:
{
user
:
{
user
:
{
id
:
loginResult
.
user
.
id
,
id
:
loginResult
.
user
.
_
id
,
email
:
loginResult
.
user
.
email
,
email
:
loginResult
.
user
.
email
,
username
:
loginResult
.
user
.
username
,
username
:
loginResult
.
user
.
username
,
first_name
:
loginResult
.
user
.
first_name
,
first_name
:
loginResult
.
user
.
first_name
,
last_name
:
loginResult
.
user
.
last_name
,
last_name
:
loginResult
.
user
.
last_name
,
roles
:
(
loginResult
.
user
as
any
).
roles
,
roles
:
(
loginResult
.
user
.
roles
as
any
[]).
map
(
r
=>
r
.
name
),
permissions
:
(
loginResult
.
user
as
any
).
permissions
,
status
:
loginResult
.
user
.
status
,
status
:
loginResult
.
user
.
status
,
last_login_at
:
loginResult
.
user
_auth
.
last_login_at
,
last_login_at
:
loginResult
.
user
.
last_login
,
},
},
session
:
{
session
:
{
id
:
loginResult
.
session
.
id
,
id
:
loginResult
.
session
.
_
id
,
expires_at
:
loginResult
.
session
.
expires_at
,
expires_at
:
loginResult
.
session
.
expires_at
,
refresh_expires_at
:
loginResult
.
session
.
refresh_expires_at
,
refresh_expires_at
:
loginResult
.
session
.
refresh_expires_at
,
},
},
...
...
src/controllers/api/v1.0/auth/logout.ts
View file @
d505ed0d
...
@@ -14,7 +14,7 @@ export default (_express: Application) => {
...
@@ -14,7 +14,7 @@ export default (_express: Application) => {
* summary: Logout user
* summary: Logout user
* description: Logout user and invalidate current session
* description: Logout user and invalidate current session
* security:
* security:
* - Bearer: []
* - Bearer
Auth
: []
* responses:
* responses:
* 200:
* 200:
* description: Logged out successfully
* description: Logged out successfully
...
...
src/controllers/api/v1.0/auth/profile.ts
View file @
d505ed0d
...
@@ -2,7 +2,7 @@ import { Application } from "express";
...
@@ -2,7 +2,7 @@ import { Application } from "express";
import
{
Resource
}
from
"express-automatic-routes"
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
authenticate
}
from
"#middlewares/auth"
;
import
{
authenticate
}
from
"#middlewares/auth"
;
import
{
User
}
from
"#models/User"
;
import
{
User
}
from
"#models/
mongodb/
User"
;
export
default
(
_express
:
Application
)
=>
{
export
default
(
_express
:
Application
)
=>
{
return
<
Resource
>
{
return
<
Resource
>
{
...
@@ -14,7 +14,7 @@ export default (_express: Application) => {
...
@@ -14,7 +14,7 @@ export default (_express: Application) => {
* summary: Get user profile
* summary: Get user profile
* description: Get current authenticated user's profile information
* description: Get current authenticated user's profile information
* security:
* security:
* - Bearer: []
* - Bearer
Auth
: []
* responses:
* responses:
* 200:
* 200:
* description: Profile retrieved successfully
* description: Profile retrieved successfully
...
@@ -25,48 +25,47 @@ export default (_express: Application) => {
...
@@ -25,48 +25,47 @@ export default (_express: Application) => {
* properties:
* properties:
* success:
* success:
* type: boolean
* type: boolean
* example: true
* data:
* data:
* type: object
* type: object
* properties:
* properties:
* id:
* id:
* type: string
* type: string
* example: "uuid-string"
* email:
* email:
* type: string
* type: string
* example: "user@example.com"
* username:
* username:
* type: string
* type: string
* example: "john_doe"
* first_name:
* role:
* type: string
* type: string
* enum: [user, admin, system_admin]
* last_name:
* example: "user"
* type: string
* permissions:
* status:
* type: string
* roles:
* type: array
* type: array
* items:
* items:
* type: string
* type: string
* example: ["read:profile"]
* example: ["admin"]
* 401:
* $ref: '#/components/responses/Unauthorized'
*/
*/
get
:
{
get
:
{
middleware
:
[
authenticate
],
middleware
:
[
authenticate
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
try
{
// Get user with roles and permissions
const
user
=
await
User
.
findById
(
req
.
user
?.
id
)
const
user
=
await
User
.
findByPk
(
req
.
user
?.
id
,
{
.
select
(
"-password_hash"
)
attributes
:
[
"id"
,
"email"
,
"username"
,
"first_name"
,
"last_name"
,
"status"
],
.
populate
(
"roles"
,
"name"
);
});
if
(
!
user
)
{
if
(
!
user
)
{
return
res
.
error
({
message
:
"User not found"
,
status
:
404
});
return
res
.
error
({
message
:
"User not found"
,
status
:
404
});
}
}
const
userData
=
{
const
userData
=
{
...
user
.
toJSON
(),
id
:
user
.
_id
,
roles
:
req
.
user
?.
roles
,
email
:
user
.
email
,
permissions
:
req
.
user
?.
permissions
,
username
:
user
.
username
,
first_name
:
user
.
first_name
,
last_name
:
user
.
last_name
,
status
:
user
.
status
,
roles
:
(
user
.
roles
as
any
[]).
map
(
r
=>
r
.
name
),
};
};
return
res
.
sendOk
({
data
:
userData
});
return
res
.
sendOk
({
data
:
userData
});
...
...
src/controllers/api/v1.0/auth/refresh.ts
View file @
d505ed0d
...
@@ -18,14 +18,7 @@ export default (_express: Application) => {
...
@@ -18,14 +18,7 @@ export default (_express: Application) => {
* content:
* content:
* application/json:
* application/json:
* schema:
* schema:
* type: object
* $ref: '#/components/schemas/RefreshRequest'
* properties:
* refresh_token:
* type: string
* description: Refresh token (optional if sent via cookie)
* device_info:
* type: object
* description: Device information
* responses:
* responses:
* 200:
* 200:
* description: Token refreshed successfully
* description: Token refreshed successfully
...
...
src/controllers/api/v1.0/auth/register.ts
0 → 100644
View file @
d505ed0d
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
authenticate
,
requireAdmin
}
from
"#middlewares/auth"
;
import
{
validateRegister
}
from
"#middlewares/validators/auth"
;
export
default
(
_express
:
Application
)
=>
{
return
<
Resource
>
{
/**
* @openapi
* /auth/register:
* post:
* tags: [Authentication]
* summary: Register a new user (Admin only)
* description: Create a new account. Only accessible by administrators.
* security:
* - BearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RegisterRequest'
* responses:
* 201:
* description: User created successfully
* 401:
* $ref: '#/components/responses/Unauthorized'
* 403:
* $ref: '#/components/responses/Forbidden'
* 409:
* description: User already exists
*/
post
:
{
middleware
:
[
authenticate
,
requireAdmin
,
validateRegister
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
userData
=
req
.
body
;
const
user
=
await
AuthService
.
register
(
userData
);
await
user
.
populate
(
"roles"
,
"name"
);
return
res
.
sendOk
({
data
:
{
id
:
user
.
_id
,
email
:
user
.
email
,
username
:
user
.
username
,
status
:
user
.
status
,
roles
:
(
user
.
roles
as
any
[]).
map
(
r
=>
r
.
name
)
},
message
:
"Người dùng đã được tạo thành công"
,
message_en
:
"User created successfully"
,
statusCode
:
201
});
}
catch
(
error
)
{
return
res
.
error
(
error
);
}
},
},
};
};
src/middlewares/auth.ts
View file @
d505ed0d
...
@@ -3,7 +3,7 @@ import { NextFunction } from "express";
...
@@ -3,7 +3,7 @@ import { NextFunction } from "express";
import
{
AuthService
,
JWTPayload
}
from
"../services/authService"
;
import
{
AuthService
,
JWTPayload
}
from
"../services/authService"
;
import
{
GenericError
}
from
"#interfaces/error/generic"
;
import
{
GenericError
}
from
"#interfaces/error/generic"
;
import
{
console
}
from
"inspector"
;
import
{
console
}
from
"inspector"
;
type
UserRole
=
"
user"
|
"admin"
|
"system_admin
"
;
type
UserRole
=
"
admin"
|
"mentor"
|
"student
"
;
// Extend Request interface
// Extend Request interface
declare
module
"express"
{
declare
module
"express"
{
...
@@ -113,14 +113,14 @@ export function authorize(allowedRoles: UserRole[] = []) {
...
@@ -113,14 +113,14 @@ export function authorize(allowedRoles: UserRole[] = []) {
* Admin only middleware
* Admin only middleware
*/
*/
export
function
requireAdmin
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
):
void
{
export
function
requireAdmin
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
):
void
{
return
authorize
([
"admin"
,
"system_admin"
])(
req
,
res
,
next
);
return
authorize
([
"admin"
])(
req
,
res
,
next
);
}
}
/**
/**
*
Super admin
only middleware
*
Mentor
only middleware
*/
*/
export
function
require
SuperAdmin
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
):
void
{
export
function
require
Mentor
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
):
void
{
return
authorize
([
"
system_
admin"
])(
req
,
res
,
next
);
return
authorize
([
"
mentor"
,
"
admin"
])(
req
,
res
,
next
);
}
}
/**
/**
...
...
src/middlewares/validators/auth.ts
View file @
d505ed0d
...
@@ -35,6 +35,9 @@ export const authSchemas = {
...
@@ -35,6 +35,9 @@ export const authSchemas = {
first_name
:
Joi
.
string
().
optional
(),
first_name
:
Joi
.
string
().
optional
(),
last_name
:
Joi
.
string
().
optional
(),
last_name
:
Joi
.
string
().
optional
(),
phone
:
Joi
.
string
().
optional
(),
phone
:
Joi
.
string
().
optional
(),
roles
:
Joi
.
array
().
items
(
Joi
.
string
().
valid
(
'admin'
,
'mentor'
,
'student'
)).
optional
().
messages
({
"any.only"
:
"Role không hợp lệ"
,
}),
}),
}),
params
:
Joi
.
object
(),
params
:
Joi
.
object
(),
query
:
Joi
.
object
(),
query
:
Joi
.
object
(),
...
...
src/models/mongodb/Role.ts
0 → 100644
View file @
d505ed0d
import
mongoose
,
{
Schema
,
Document
}
from
'mongoose'
;
export
interface
IRole
extends
Document
{
name
:
string
;
description
?:
string
;
permissions
:
string
[];
createdAt
:
Date
;
updatedAt
:
Date
;
}
const
RoleSchemaEntity
:
Schema
=
new
Schema
(
{
name
:
{
type
:
String
,
required
:
true
,
unique
:
true
,
enum
:
[
'admin'
,
'mentor'
,
'student'
]
},
description
:
{
type
:
String
},
permissions
:
[{
type
:
String
}],
},
{
timestamps
:
true
,
versionKey
:
false
}
);
export
const
Role
=
mongoose
.
models
.
Role
||
mongoose
.
model
<
IRole
>
(
'Role'
,
RoleSchemaEntity
);
src/models/mongodb/User.ts
0 → 100644
View file @
d505ed0d
import
mongoose
,
{
Schema
,
Document
}
from
'mongoose'
;
export
interface
IUser
extends
Document
{
email
:
string
;
password_hash
:
string
;
username
?:
string
;
first_name
?:
string
;
last_name
?:
string
;
phone
?:
string
;
avatar_url
?:
string
;
status
:
'active'
|
'inactive'
|
'suspended'
|
'pending'
;
roles
:
mongoose
.
Types
.
ObjectId
[]
|
string
[];
last_login
?:
Date
;
password_changed_at
?:
Date
;
login_attempts
:
number
;
lock_until
?:
Date
;
createdAt
:
Date
;
updatedAt
:
Date
;
}
const
UserSchemaEntity
:
Schema
=
new
Schema
(
{
email
:
{
type
:
String
,
required
:
true
,
unique
:
true
,
lowercase
:
true
,
trim
:
true
},
password_hash
:
{
type
:
String
,
required
:
true
},
username
:
{
type
:
String
,
unique
:
true
,
sparse
:
true
},
first_name
:
{
type
:
String
},
last_name
:
{
type
:
String
},
phone
:
{
type
:
String
},
avatar_url
:
{
type
:
String
},
status
:
{
type
:
String
,
enum
:
[
'active'
,
'inactive'
,
'suspended'
,
'pending'
],
default
:
'active'
},
roles
:
[{
type
:
Schema
.
Types
.
ObjectId
,
ref
:
'Role'
}],
last_login
:
{
type
:
Date
},
password_changed_at
:
{
type
:
Date
},
login_attempts
:
{
type
:
Number
,
required
:
true
,
default
:
0
},
lock_until
:
{
type
:
Date
}
},
{
timestamps
:
true
,
versionKey
:
false
}
);
export
const
User
=
mongoose
.
models
.
User
||
mongoose
.
model
<
IUser
>
(
'User'
,
UserSchemaEntity
);
src/models/mongodb/UserSession.ts
0 → 100644
View file @
d505ed0d
import
mongoose
,
{
Schema
,
Document
}
from
'mongoose'
;
export
interface
IUserSession
extends
Document
{
user
:
mongoose
.
Types
.
ObjectId
;
session_token
:
string
;
refresh_token
:
string
;
ip_address
?:
string
;
user_agent
?:
string
;
device_info
?:
any
;
expires_at
:
Date
;
refresh_expires_at
:
Date
;
is_active
:
boolean
;
last_activity_at
:
Date
;
createdAt
:
Date
;
updatedAt
:
Date
;
}
const
UserSessionSchemaEntity
:
Schema
=
new
Schema
(
{
user
:
{
type
:
Schema
.
Types
.
ObjectId
,
ref
:
'User'
,
required
:
true
},
session_token
:
{
type
:
String
,
required
:
true
,
unique
:
true
},
refresh_token
:
{
type
:
String
,
required
:
true
},
ip_address
:
{
type
:
String
},
user_agent
:
{
type
:
String
},
device_info
:
{
type
:
Schema
.
Types
.
Mixed
},
expires_at
:
{
type
:
Date
,
required
:
true
},
refresh_expires_at
:
{
type
:
Date
,
required
:
true
},
is_active
:
{
type
:
Boolean
,
default
:
true
},
last_activity_at
:
{
type
:
Date
,
default
:
Date
.
now
}
},
{
timestamps
:
true
,
versionKey
:
false
}
);
// TTL index to automatically remove expired sessions
UserSessionSchemaEntity
.
index
({
refresh_expires_at
:
1
},
{
expireAfterSeconds
:
0
});
UserSessionSchemaEntity
.
index
({
session_token
:
1
});
UserSessionSchemaEntity
.
index
({
user
:
1
});
export
const
UserSession
=
mongoose
.
models
.
UserSession
||
mongoose
.
model
<
IUserSession
>
(
'UserSession'
,
UserSessionSchemaEntity
);
src/scripts/seed-mongo.ts
0 → 100644
View file @
d505ed0d
import
mongoose
from
'mongoose'
;
import
dotenv
from
'dotenv'
;
import
{
Role
}
from
'../models/mongodb/Role'
;
import
{
User
}
from
'../models/mongodb/User'
;
import
*
as
bcrypt
from
'bcryptjs'
;
dotenv
.
config
();
const
MONGODB_URI
=
process
.
env
.
MONGODB_URI
||
'mongodb://localhost:27017/trainee-schedule'
;
const
seed
=
async
()
=>
{
try
{
await
mongoose
.
connect
(
MONGODB_URI
);
console
.
log
(
'Connected to MongoDB'
);
// Create Roles
const
roles
=
[
{
name
:
'admin'
,
description
:
'System Administrator'
},
{
name
:
'mentor'
,
description
:
'Instructor/Mentor'
},
{
name
:
'student'
,
description
:
'Trainee/Student'
}
];
for
(
const
roleData
of
roles
)
{
await
Role
.
findOneAndUpdate
(
{
name
:
roleData
.
name
},
roleData
,
{
upsert
:
true
,
new
:
true
}
);
console
.
log
(
`Role
${
roleData
.
name
}
initialized`
);
}
const
adminRole
=
await
Role
.
findOne
({
name
:
'admin'
});
if
(
!
adminRole
)
throw
new
Error
(
'Admin role not found'
);
// Create Default Admin User
const
adminEmail
=
'admin@admin.com'
;
const
existingAdmin
=
await
User
.
findOne
({
email
:
adminEmail
});
if
(
!
existingAdmin
)
{
const
hashedPassword
=
await
bcrypt
.
hash
(
'admin123'
,
12
);
await
User
.
create
({
email
:
adminEmail
,
password_hash
:
hashedPassword
,
username
:
'admin'
,
first_name
:
'System'
,
last_name
:
'Admin'
,
status
:
'active'
,
roles
:
[
adminRole
.
_id
]
as
any
});
console
.
log
(
'Default admin user created: admin@admin.com / admin123'
);
}
else
{
console
.
log
(
'Admin user already exists'
);
}
await
mongoose
.
disconnect
();
console
.
log
(
'Seeding completed'
);
process
.
exit
(
0
);
}
catch
(
error
)
{
console
.
error
(
'Seeding failed:'
,
error
);
process
.
exit
(
1
);
}
};
seed
();
src/services/authService.ts
View file @
d505ed0d
import
dayjs
from
"dayjs"
;
import
*
as
bcrypt
from
"bcryptjs"
;
import
*
as
bcrypt
from
"bcryptjs"
;
import
{
sign
,
verify
,
SignOptions
}
from
"jsonwebtoken"
;
import
{
sign
,
verify
,
SignOptions
}
from
"jsonwebtoken"
;
import
{
QueryTypes
}
from
"sequelize"
;
import
{
User
,
IUser
}
from
"../models/mongodb/User"
;
import
sequelize
from
"#services/database/sequelize/service"
;
import
{
Role
,
IRole
}
from
"../models/mongodb/Role"
;
import
{
User
,
UserAttributes
}
from
"../models/User"
;
import
{
UserSession
,
IUserSession
}
from
"../models/mongodb/UserSession"
;
import
{
UserAuth
}
from
"../models/UserAuth"
;
import
{
Role
}
from
"../models/Role"
;
import
{
UserRole
as
UserRoleModel
}
from
"../models/UserRole"
;
import
{
RolePermission
}
from
"../models/RolePermission"
;
import
{
Permission
}
from
"../models/Permission"
;
import
{
UserSession
}
from
"../models/UserSession"
;
import
{
GenericError
}
from
"#interfaces/error/generic"
;
import
{
GenericError
}
from
"#interfaces/error/generic"
;
import
{
import
{
isUserLocked
,
isUserLocked
,
incrementUserLoginAttempts
,
incrementUserLoginAttempts
,
resetUserLoginAttempts
,
resetUserLoginAttempts
,
updateUserLastLogin
,
updateUserLastLogin
,
checkAndUnlockExpiredLockout
,
checkAndUnlockExpiredLockout
,
isSessionExpired
,
isSessionRefreshExpired
,
deactivateUserSession
,
updateSessionActivity
,
canUserChangePassword
,
updateUserPasswordChanged
,
isPasswordRecentlyUsed
,
}
from
"../utils/authUtils"
;
}
from
"../utils/authUtils"
;
enum
UserStatus
{
ACTIVE
=
"active"
,
INACTIVE
=
"inactive"
,
SUSPENDED
=
"suspended"
,
PENDING_VERIFICATION
=
"pending_verification"
,
}
enum
UserRole
{
USER
=
"user"
,
ADMIN
=
"admin"
,
SYSTEM_ADMIN
=
"system_admin"
,
}
export
interface
LoginCredentials
{
export
interface
LoginCredentials
{
email
:
string
;
email
:
string
;
password
:
string
;
password
:
string
;
device_info
?:
Record
<
string
,
unknown
>
;
device_info
?:
Record
<
string
,
unknown
>
;
ip_address
?:
string
;
ip_address
?:
string
;
user_agent
?:
string
;
user_agent
?:
string
;
}
}
export
interface
RegisterData
{
export
interface
RegisterData
{
email
:
string
;
email
:
string
;
password
:
string
;
password
:
string
;
username
?:
string
;
username
?:
string
;
first_name
?:
string
;
first_name
?:
string
;
last_name
?:
string
;
last_name
?:
string
;
phone
?:
string
;
phone
?:
string
;
roles
?:
string
[];
// Array of role names
}
}
export
interface
TokenPair
{
export
interface
TokenPair
{
access_token
:
string
;
access_token
:
string
;
refresh_token
:
string
;
refresh_token
:
string
;
expires_in
:
number
;
expires_in
:
number
;
refresh_expires_in
:
number
;
refresh_expires_in
:
number
;
token_type
:
string
;
token_type
:
string
;
}
}
export
interface
LoginResult
extends
TokenPair
{
export
interface
LoginResult
extends
TokenPair
{
user
:
User
;
user
:
IUser
;
user_auth
:
UserAuth
;
session
:
IUserSession
;
session
:
UserSession
;
}
export
interface
CookieOptions
{
httpOnly
:
boolean
;
secure
:
boolean
;
sameSite
:
"strict"
|
"lax"
|
"none"
;
maxAge
:
number
;
path
:
string
;
domain
?:
string
;
}
}
export
interface
JWTPayload
{
export
interface
JWTPayload
{
id
:
string
;
id
:
string
;
email
:
string
;
email
:
string
;
username
?:
string
;
username
?:
string
;
roles
?:
string
[];
roles
?:
string
[];
permissions
?:
string
[];
type
?:
string
;
type
?:
string
;
iat
?:
number
;
iat
?:
number
;
exp
?:
number
;
exp
?:
number
;
}
}
export
class
AuthService
{
export
class
AuthService
{
private
static
readonly
JWT_SECRET
=
process
.
env
.
JWT_SECRET
!
;
private
static
readonly
JWT_SECRET
=
process
.
env
.
JWT_SECRET
!
;
private
static
readonly
JWT_REFRESH_SECRET
=
process
.
env
.
JWT_REFRESH_SECRET
!
;
private
static
readonly
JWT_REFRESH_SECRET
=
process
.
env
.
JWT_REFRESH_SECRET
!
;
private
static
readonly
JWT_EXPIRES_IN
=
process
.
env
.
JWT_EXPIRES_IN
||
"15m"
;
private
static
readonly
JWT_EXPIRES_IN
=
process
.
env
.
JWT_EXPIRES_IN
||
"1h"
;
private
static
readonly
JWT_REFRESH_EXPIRES_IN
=
process
.
env
.
JWT_REFRESH_EXPIRES_IN
||
"7d"
;
private
static
readonly
JWT_REFRESH_EXPIRES_IN
=
process
.
env
.
JWT_REFRESH_EXPIRES_IN
||
"7d"
;
private
static
readonly
BCRYPT_ROUNDS
=
parseInt
(
process
.
env
.
BCRYPT_ROUNDS
||
"12"
);
private
static
readonly
BCRYPT_ROUNDS
=
parseInt
(
process
.
env
.
BCRYPT_ROUNDS
||
"12"
);
private
static
readonly
TOKEN_ENCRYPTION_KEY
=
process
.
env
.
TOKEN_ENCRYPTION_KEY
!
;
private
static
validateSecrets
()
{
// Validate required secrets
if
(
!
this
.
JWT_SECRET
||
!
this
.
JWT_REFRESH_SECRET
)
{
private
static
validateSecrets
()
{
throw
new
Error
(
"JWT secrets are missing in environment variables"
);
if
(
!
this
.
JWT_SECRET
)
{
}
throw
new
Error
(
"JWT_SECRET environment variable is required"
);
}
}
if
(
!
this
.
JWT_REFRESH_SECRET
)
{
static
parseTimeToSeconds
(
timeStr
:
string
):
number
{
throw
new
Error
(
"JWT_REFRESH_SECRET environment variable is required"
);
const
unit
=
timeStr
.
slice
(
-
1
);
}
const
value
=
parseInt
(
timeStr
.
slice
(
0
,
-
1
));
if
(
!
this
.
TOKEN_ENCRYPTION_KEY
)
{
switch
(
unit
)
{
throw
new
Error
(
"TOKEN_ENCRYPTION_KEY environment variable is required for token encryption"
);
case
'h'
:
return
value
*
3600
;
}
case
'd'
:
return
value
*
86400
;
}
case
'm'
:
return
value
*
60
;
case
's'
:
return
value
;
/**
default
:
return
value
;
* Get secure cookie options for tokens
}
*/
}
static
getAccessTokenCookieOptions
():
CookieOptions
{
const
isProduction
=
process
.
env
.
NODE_ENV
===
"production"
;
static
async
hashPassword
(
password
:
string
):
Promise
<
string
>
{
const
maxAge
=
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
)
*
1000
;
return
bcrypt
.
hash
(
password
,
this
.
BCRYPT_ROUNDS
);
return
{
}
httpOnly
:
true
,
// Prevent XSS attacks
secure
:
isProduction
,
// HTTPS only in production
static
async
verifyPassword
(
password
:
string
,
hash
:
string
):
Promise
<
boolean
>
{
sameSite
:
"lax"
,
// Allow cross-subdomain requests
return
bcrypt
.
compare
(
password
,
hash
);
maxAge
,
}
path
:
"/api"
,
...(
isProduction
&&
process
.
env
.
COOKIE_DOMAIN
?
{
domain
:
process
.
env
.
COOKIE_DOMAIN
}
:
{}),
static
generateAccessToken
(
user
:
any
,
roles
:
string
[]):
string
{
};
this
.
validateSecrets
();
}
const
payload
:
JWTPayload
=
{
id
:
user
.
_id
.
toString
(),
/**
email
:
user
.
email
,
* Get secure cookie options for refresh tokens
username
:
user
.
username
,
*/
roles
:
roles
static
getRefreshTokenCookieOptions
():
CookieOptions
{
};
const
isProduction
=
process
.
env
.
NODE_ENV
===
"production"
;
return
sign
(
payload
,
this
.
JWT_SECRET
,
{
expiresIn
:
this
.
JWT_EXPIRES_IN
as
any
});
const
maxAge
=
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
)
*
1000
;
}
return
{
httpOnly
:
true
,
static
generateRefreshToken
(
user
:
any
):
string
{
secure
:
isProduction
,
this
.
validateSecrets
();
sameSite
:
"lax"
,
const
payload
=
{
maxAge
,
id
:
user
.
_id
.
toString
(),
path
:
"/api/v1.0/auth/refresh"
,
type
:
"refresh"
...(
isProduction
&&
process
.
env
.
COOKIE_DOMAIN
?
{
domain
:
process
.
env
.
COOKIE_DOMAIN
}
:
{}),
};
};
return
sign
(
payload
,
this
.
JWT_REFRESH_SECRET
,
{
expiresIn
:
this
.
JWT_REFRESH_EXPIRES_IN
as
any
});
}
}
/**
static
verifyAccessToken
(
token
:
string
):
JWTPayload
{
* Hash password using bcrypt
this
.
validateSecrets
();
*/
try
{
static
async
hashPassword
(
password
:
string
):
Promise
<
string
>
{
return
verify
(
token
,
this
.
JWT_SECRET
)
as
JWTPayload
;
return
bcrypt
.
hash
(
password
,
this
.
BCRYPT_ROUNDS
);
}
catch
(
error
)
{
}
throw
new
GenericError
({
vi
:
"Token không hợp lệ"
,
en
:
"Invalid token"
},
"UNAUTHORIZED"
,
401
);
}
/**
}
* Verify password against hash
*/
static
verifyRefreshToken
(
token
:
string
):
any
{
static
async
verifyPassword
(
password
:
string
,
hash
:
string
):
Promise
<
boolean
>
{
this
.
validateSecrets
();
return
bcrypt
.
compare
(
password
,
hash
);
try
{
}
const
decoded
=
verify
(
token
,
this
.
JWT_REFRESH_SECRET
)
as
any
;
if
(
decoded
.
type
!==
"refresh"
)
throw
new
Error
(
"Invalid token type"
);
/**
return
decoded
;
* Encrypt token using PostgreSQL pgcrypto
}
catch
(
error
)
{
*/
throw
new
GenericError
({
vi
:
"Refresh token không hợp lệ"
,
en
:
"Invalid refresh token"
},
"UNAUTHORIZED"
,
401
);
private
static
async
encryptToken
(
plainToken
:
string
):
Promise
<
Buffer
>
{
}
const
[
result
]
=
await
sequelize
.
query
(
"SELECT encrypt_token(:token, :key) as encrypted"
,
{
}
replacements
:
{
token
:
plainToken
,
static
async
register
(
data
:
RegisterData
):
Promise
<
IUser
>
{
key
:
this
.
TOKEN_ENCRYPTION_KEY
,
const
{
email
,
password
,
username
,
first_name
,
last_name
,
phone
,
roles
}
=
data
;
},
type
:
QueryTypes
.
SELECT
,
const
existingUser
=
await
User
.
findOne
({
email
});
});
if
(
existingUser
)
{
return
(
result
as
any
).
encrypted
;
throw
new
GenericError
({
vi
:
"Người dùng đã tồn tại"
,
en
:
"User already exists"
},
"CONFLICT"
,
409
);
}
}
/**
const
password_hash
=
await
this
.
hashPassword
(
password
);
* Decrypt token using PostgreSQL pgcrypto
*/
// Find roles
private
static
async
decryptToken
(
encryptedToken
:
Buffer
):
Promise
<
string
>
{
const
roleNames
=
roles
&&
roles
.
length
>
0
?
roles
:
[
'student'
];
const
[
result
]
=
await
sequelize
.
query
(
"SELECT decrypt_token(:encrypted_token, :key) as decrypted"
,
{
const
dbRoles
=
await
Role
.
find
({
name
:
{
$in
:
roleNames
}
});
replacements
:
{
encrypted_token
:
encryptedToken
,
const
userRoleIds
=
dbRoles
.
map
(
r
=>
r
.
_id
);
key
:
this
.
TOKEN_ENCRYPTION_KEY
,
},
const
userDataObj
:
any
=
{
type
:
QueryTypes
.
SELECT
,
email
,
});
password_hash
,
return
(
result
as
any
).
decrypted
;
status
:
'active'
,
}
roles
:
userRoleIds
};
/**
* Generate JWT access token
if
(
username
)
userDataObj
.
username
=
username
;
*/
if
(
first_name
)
userDataObj
.
first_name
=
first_name
;
static
generateAccessToken
(
user
:
User
):
string
{
if
(
last_name
)
userDataObj
.
last_name
=
last_name
;
this
.
validateSecrets
();
if
(
phone
)
userDataObj
.
phone
=
phone
;
const
primaryUserRole
=
(
user
as
any
).
user_user_roles
?.
find
((
ur
:
any
)
=>
ur
.
is_primary
);
const
payload
:
JWTPayload
=
{
const
user
=
await
User
.
create
(
userDataObj
);
id
:
user
.
id
,
email
:
user
.
email
,
return
user
;
roles
:
(
user
as
any
).
roles
||
[],
// All role names
}
permissions
:
(
user
as
any
).
permissions
||
[],
// Permissions collected from all roles
};
static
async
login
(
credentials
:
LoginCredentials
):
Promise
<
LoginResult
>
{
const
{
email
,
password
,
device_info
,
ip_address
,
user_agent
}
=
credentials
;
if
(
user
.
username
)
{
payload
.
username
=
user
.
username
;
const
user
=
await
User
.
findOne
({
email
}).
populate
(
'roles'
);
}
if
(
!
user
)
{
throw
new
GenericError
({
vi
:
"Email hoặc mật khẩu không đúng"
,
en
:
"Invalid email or password"
},
"UNAUTHORIZED"
,
401
);
return
sign
(
payload
,
this
.
JWT_SECRET
as
any
,
{
}
expiresIn
:
this
.
JWT_EXPIRES_IN
as
any
,
issuer
:
"backend-template"
,
await
checkAndUnlockExpiredLockout
(
user
);
audience
:
"api-users"
,
});
if
(
isUserLocked
(
user
))
{
}
throw
new
GenericError
({
vi
:
"Tài khoản bị khóa tạm thời"
,
en
:
"Account is temporarily locked"
},
"LOCKED"
,
423
);
}
/**
* Generate JWT refresh token
const
isValidPassword
=
await
this
.
verifyPassword
(
password
,
user
.
password_hash
);
*/
if
(
!
isValidPassword
)
{
static
generateRefreshToken
(
user
:
User
):
string
{
await
incrementUserLoginAttempts
(
user
);
this
.
validateSecrets
();
throw
new
GenericError
({
vi
:
"Email hoặc mật khẩu không đúng"
,
en
:
"Invalid email or password"
},
"UNAUTHORIZED"
,
401
);
const
payload
=
{
}
id
:
user
.
id
,
email
:
user
.
email
,
await
resetUserLoginAttempts
(
user
);
roles
:
(
user
as
any
).
roles
||
[],
await
updateUserLastLogin
(
user
);
permissions
:
(
user
as
any
).
permissions
||
[],
type
:
"refresh"
,
const
roleNames
=
(
user
.
roles
as
any
[]).
map
(
r
=>
r
.
name
);
};
const
access_token
=
this
.
generateAccessToken
(
user
,
roleNames
);
const
refresh_token
=
this
.
generateRefreshToken
(
user
);
const
options
:
SignOptions
=
{
expiresIn
:
this
.
JWT_REFRESH_EXPIRES_IN
as
any
,
const
expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
);
issuer
:
"backend-template"
,
const
refresh_expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
);
audience
:
"api-users"
,
};
const
sessionDataObj
:
any
=
{
user
:
user
.
_id
,
return
sign
(
payload
,
this
.
JWT_REFRESH_SECRET
as
any
,
options
);
session_token
:
access_token
,
}
refresh_token
:
refresh_token
,
device_info
:
device_info
||
{},
/**
expires_at
:
new
Date
(
Date
.
now
()
+
expires_in
*
1000
),
* Verify JWT access token
refresh_expires_at
:
new
Date
(
Date
.
now
()
+
refresh_expires_in
*
1000
),
*/
is_active
:
true
static
verifyAccessToken
(
token
:
string
):
JWTPayload
{
};
this
.
validateSecrets
();
try
{
if
(
ip_address
)
sessionDataObj
.
ip_address
=
ip_address
;
const
decoded
=
verify
(
token
,
this
.
JWT_SECRET
,
{
if
(
user_agent
)
sessionDataObj
.
user_agent
=
user_agent
;
issuer
:
"backend-template"
,
audience
:
"api-users"
,
const
session
=
await
UserSession
.
create
(
sessionDataObj
);
})
as
unknown
as
JWTPayload
;
return
{
return
decoded
;
access_token
,
}
catch
(
error
)
{
refresh_token
,
throw
new
GenericError
({
vi
:
"Token không hợp lệ"
,
en
:
"Invalid token"
},
"UNAUTHORIZED"
,
401
);
expires_in
,
}
refresh_expires_in
,
}
token_type
:
"Bearer"
,
user
,
/**
session
* Verify JWT refresh token
};
*/
}
static
verifyRefreshToken
(
token
:
string
):
JWTPayload
{
this
.
validateSecrets
();
static
async
logout
(
token
:
string
):
Promise
<
void
>
{
try
{
await
UserSession
.
updateOne
({
session_token
:
token
},
{
is_active
:
false
});
const
decoded
=
verify
(
token
,
this
.
JWT_REFRESH_SECRET
,
{
}
issuer
:
"backend-template"
,
audience
:
"api-users"
,
static
async
validateSession
(
token
:
string
):
Promise
<
any
>
{
})
as
unknown
as
JWTPayload
;
const
session
=
await
UserSession
.
findOne
({
session_token
:
token
,
is_active
:
true
}).
populate
(
'user'
);
if
(
!
session
||
dayjs
().
isAfter
(
dayjs
(
session
.
expires_at
)))
{
if
(
decoded
.
type
!==
"refresh"
)
{
return
null
;
throw
new
Error
(
"Invalid token type"
);
}
}
await
session
.
updateOne
({
last_activity_at
:
new
Date
()
});
return
session
.
user
;
return
decoded
;
}
}
catch
(
error
)
{
throw
new
GenericError
({
vi
:
"Refresh token không hợp lệ"
,
en
:
"Invalid refresh token"
},
"UNAUTHORIZED"
,
401
);
static
async
refreshToken
(
refreshToken
:
string
,
device_info
?:
Record
<
string
,
unknown
>
):
Promise
<
TokenPair
>
{
}
const
session
=
await
UserSession
.
findOne
({
refresh_token
:
refreshToken
,
is_active
:
true
}).
populate
(
'user'
);
}
if
(
!
session
||
dayjs
().
isAfter
(
dayjs
(
session
.
refresh_expires_at
)))
{
/**
throw
new
GenericError
({
vi
:
"Refresh token không hợp lệ"
,
en
:
"Invalid refresh token"
},
"UNAUTHORIZED"
,
401
);
* Register new user
}
*/
static
async
register
(
data
:
RegisterData
):
Promise
<
User
>
{
const
decoded
=
this
.
verifyRefreshToken
(
refreshToken
);
return
sequelize
.
transaction
(
async
(
transaction
)
=>
{
const
user
=
session
.
user
as
any
;
const
{
email
,
password
,
username
,
first_name
,
last_name
,
phone
}
=
data
;
const
roleNames
=
user
.
roles
?
await
Role
.
find
({
_id
:
{
$in
:
user
.
roles
}
}).
then
(
roles
=>
roles
.
map
(
r
=>
r
.
name
))
:
[];
// Check if user already exists
const
existingUser
=
await
User
.
findOne
({
const
new_access_token
=
this
.
generateAccessToken
(
user
,
roleNames
);
where
:
{
email
},
const
new_refresh_token
=
this
.
generateRefreshToken
(
user
);
paranoid
:
false
,
// Include soft deleted users
transaction
,
const
expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
);
});
const
refresh_expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
);
if
(
existingUser
)
{
await
session
.
updateOne
({
if
(
existingUser
.
deleted_at
)
{
session_token
:
new_access_token
,
// Reactivate soft deleted user
refresh_token
:
new_refresh_token
,
await
existingUser
.
restore
({
transaction
});
expires_at
:
new
Date
(
Date
.
now
()
+
expires_in
*
1000
),
// Update UserAuth for reactivated user
refresh_expires_at
:
new
Date
(
Date
.
now
()
+
refresh_expires_in
*
1000
),
const
existingUserAuth
=
await
UserAuth
.
findOne
({
device_info
:
device_info
||
session
.
device_info
,
where
:
{
user_id
:
existingUser
.
id
},
last_activity_at
:
new
Date
()
transaction
,
});
});
if
(
existingUserAuth
)
{
return
{
existingUserAuth
.
password_hash
=
await
this
.
hashPassword
(
password
);
access_token
:
new_access_token
,
await
existingUserAuth
.
save
({
transaction
});
refresh_token
:
new_refresh_token
,
}
expires_in
,
existingUser
.
status
=
"pending_verification"
;
refresh_expires_in
,
return
existingUser
.
save
({
transaction
});
token_type
:
"Bearer"
}
else
{
};
throw
new
GenericError
({
vi
:
"Người dùng đã tồn tại"
,
en
:
"User already exists"
},
"CONFLICT"
,
409
);
}
}
}
static
getAccessTokenCookieOptions
()
{
return
{
// Hash password
httpOnly
:
true
,
const
password_hash
=
await
this
.
hashPassword
(
password
);
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
sameSite
:
'lax'
as
const
,
// Get default user role
maxAge
:
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
)
*
1000
,
const
defaultRole
=
await
Role
.
findOne
({
where
:
{
name
:
"user"
}
});
path
:
'/api'
if
(
!
defaultRole
)
{
};
throw
new
Error
(
"Default user role not found"
);
}
}
static
getRefreshTokenCookieOptions
()
{
// Create user
return
{
const
userData
:
Partial
<
UserAttributes
>
=
{
httpOnly
:
true
,
email
,
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
status
:
UserStatus
.
PENDING_VERIFICATION
,
sameSite
:
'lax'
as
const
,
};
maxAge
:
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
)
*
1000
,
path
:
'/api/v1.0/auth/refresh'
if
(
username
)
userData
.
username
=
username
;
};
if
(
first_name
)
userData
.
first_name
=
first_name
;
}
if
(
last_name
)
userData
.
last_name
=
last_name
;
if
(
phone
)
userData
.
phone
=
phone
;
const
user
=
await
User
.
create
(
userData
as
any
,
{
transaction
});
// Create UserAuth
await
UserAuth
.
create
(
{
user_id
:
user
.
id
,
password_hash
,
},
{
transaction
},
);
// Assign default role
await
UserRoleModel
.
create
(
{
user_id
:
user
.
id
,
role_id
:
defaultRole
.
id
,
is_primary
:
true
,
},
{
transaction
},
);
return
user
;
});
}
/**
* Authenticate user login
*/
static
async
login
(
credentials
:
LoginCredentials
):
Promise
<
LoginResult
>
{
const
{
email
,
password
,
device_info
,
ip_address
,
user_agent
}
=
credentials
;
// Find user with auth data
const
userInstance
=
await
User
.
findOne
({
where
:
{
email
},
include
:
[{
model
:
UserAuth
,
as
:
"user_auth"
}],
});
if
(
!
userInstance
)
{
throw
new
GenericError
(
{
vi
:
"Email hoặc mật khẩu không đúng"
,
en
:
"Invalid email or password"
},
"UNAUTHORIZED"
,
401
,
);
}
// Get plain data values
const
user
=
userInstance
.
dataValues
as
User
;
let
userAuth
=
userInstance
.
user_auth
;
// If UserAuth doesn't exist, this is an error (every user should have UserAuth)
if
(
!
userAuth
)
{
throw
new
GenericError
(
{
vi
:
"Tài khoản chưa được thiết lập đầy đủ"
,
en
:
"Account not fully set up"
},
"UNAUTHORIZED"
,
401
,
);
}
// Fetch user roles with permissions
const
userRoles
=
await
UserRoleModel
.
findAll
({
where
:
{
user_id
:
user
.
id
},
include
:
[
{
model
:
Role
,
as
:
"role"
,
include
:
[
{
model
:
RolePermission
,
as
:
"role_permissions"
,
include
:
[
{
model
:
Permission
,
as
:
"permission"
,
attributes
:
[
"name"
],
},
],
},
],
},
],
});
// Collect permissions and roles from all roles (unique)
const
permissionsSet
=
new
Set
<
string
>
();
const
rolesSet
=
new
Set
<
string
>
();
if
(
userRoles
)
{
for
(
const
userRole
of
userRoles
)
{
if
(
userRole
.
role
)
{
rolesSet
.
add
(
userRole
.
role
.
name
);
if
(
userRole
.
role
.
role_permissions
)
{
for
(
const
rp
of
userRole
.
role
.
role_permissions
)
{
if
(
rp
.
permission
&&
rp
.
permission
.
name
)
{
permissionsSet
.
add
(
rp
.
permission
.
name
);
}
}
}
}
}
}
(
user
as
any
).
permissions
=
Array
.
from
(
permissionsSet
);
(
user
as
any
).
roles
=
Array
.
from
(
rolesSet
);
// Check if account lockout has expired and unlock if needed
if
(
userAuth
)
await
checkAndUnlockExpiredLockout
(
userAuth
);
// Check if account is locked
if
(
userAuth
&&
isUserLocked
(
userAuth
))
{
throw
new
GenericError
({
vi
:
"Tài khoản bị khóa tạm thời"
,
en
:
"Account is temporarily locked"
},
"LOCKED"
,
423
);
}
// Check if account is active
if
(
user
.
status
!==
UserStatus
.
ACTIVE
)
{
throw
new
GenericError
({
vi
:
"Tài khoản chưa được kích hoạt"
,
en
:
"Account is not active"
},
"FORBIDDEN"
,
403
);
}
// Check if password_hash exists
if
(
!
userAuth
?.
password_hash
)
{
throw
new
GenericError
(
{
vi
:
"Tài khoản chưa được thiết lập mật khẩu"
,
en
:
"Account password not set"
},
"UNAUTHORIZED"
,
401
,
);
}
// Verify password
const
isValidPassword
=
await
this
.
verifyPassword
(
password
,
userAuth
.
password_hash
);
if
(
!
isValidPassword
)
{
if
(
userAuth
)
await
incrementUserLoginAttempts
(
userAuth
);
throw
new
GenericError
(
{
vi
:
"Email hoặc mật khẩu không đúng"
,
en
:
"Invalid email or password"
},
"UNAUTHORIZED"
,
401
,
);
}
// Reset login attempts on successful login
if
(
userAuth
)
await
resetUserLoginAttempts
(
userAuth
);
if
(
userAuth
)
await
updateUserLastLogin
(
userAuth
);
// Generate tokens
const
access_token
=
this
.
generateAccessToken
(
user
);
const
refresh_token
=
this
.
generateRefreshToken
(
user
);
// Encrypt tokens before storing
const
encrypted_access_token
=
await
this
.
encryptToken
(
access_token
);
const
encrypted_refresh_token
=
await
this
.
encryptToken
(
refresh_token
);
// Calculate expiration times
const
expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
);
const
refresh_expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
);
// Create session with encrypted tokens
const
sessionData
:
any
=
{
user_id
:
user
.
id
,
session_token
:
encrypted_access_token
,
refresh_token
:
encrypted_refresh_token
,
device_info
:
device_info
||
{},
expires_at
:
new
Date
(
Date
.
now
()
+
expires_in
*
1000
),
refresh_expires_at
:
new
Date
(
Date
.
now
()
+
refresh_expires_in
*
1000
),
is_active
:
true
,
permissions
:
(
user
as
any
).
permissions
||
[],
roles
:
(
user
as
any
).
roles
||
[],
};
if
(
ip_address
)
sessionData
.
ip_address
=
ip_address
;
if
(
user_agent
)
sessionData
.
user_agent
=
user_agent
;
await
UserSession
.
create
(
sessionData
as
any
);
// Get the created session
const
createdSession
=
await
UserSession
.
findOne
({
where
:
{
session_token
:
encrypted_access_token
},
});
return
{
access_token
,
refresh_token
,
expires_in
,
refresh_expires_in
,
token_type
:
"Bearer"
,
user
,
user_auth
:
userAuth
.
dataValues
as
UserAuth
,
session
:
createdSession
?.
dataValues
as
UserSession
,
};
}
/**
* Refresh access token
*/
static
async
refreshToken
(
refreshToken
:
string
,
device_info
?:
Record
<
string
,
unknown
>
):
Promise
<
TokenPair
>
{
// Find active session by decrypting stored refresh tokens
const
sessions
=
await
sequelize
.
query
(
`
SELECT us.*
FROM user_sessions us
WHERE us.is_active = true
AND us.refresh_expires_at > NOW()
`
,
{
type
:
QueryTypes
.
SELECT
,
},
);
// Find matching session by decrypting refresh tokens
let
matchingSession
:
any
=
null
;
for
(
const
session
of
sessions
as
any
[])
{
try
{
const
decryptedRefreshToken
=
await
this
.
decryptToken
(
session
.
refresh_token
);
if
(
decryptedRefreshToken
===
refreshToken
)
{
matchingSession
=
session
;
break
;
}
}
catch
(
error
)
{
// Skip invalid encrypted tokens
continue
;
}
}
if
(
!
matchingSession
)
{
throw
new
GenericError
({
vi
:
"Refresh token không hợp lệ"
,
en
:
"Invalid refresh token"
},
"UNAUTHORIZED"
,
401
);
}
// Verify refresh token JWT
const
decoded
=
this
.
verifyRefreshToken
(
refreshToken
);
// Create user object from token data
const
user
=
{
id
:
decoded
.
id
,
email
:
decoded
.
email
,
roles
:
decoded
.
roles
||
[],
permissions
:
decoded
.
permissions
||
[],
}
as
any
;
// Generate new tokens
const
access_token
=
this
.
generateAccessToken
(
user
);
const
new_refresh_token
=
this
.
generateRefreshToken
(
user
);
// Encrypt new tokens
const
encrypted_access_token
=
await
this
.
encryptToken
(
access_token
);
const
encrypted_refresh_token
=
await
this
.
encryptToken
(
new_refresh_token
);
// Calculate expiration times
const
expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_EXPIRES_IN
);
const
refresh_expires_in
=
this
.
parseTimeToSeconds
(
this
.
JWT_REFRESH_EXPIRES_IN
);
// Update session with encrypted tokens
await
sequelize
.
query
(
`
UPDATE user_sessions
SET session_token = :access_token,
refresh_token = :refresh_token,
expires_at = :expires_at,
refresh_expires_at = :refresh_expires_at,
device_info = :device_info,
last_activity_at = NOW()
WHERE id = :session_id
`
,
{
replacements
:
{
access_token
:
encrypted_access_token
,
refresh_token
:
encrypted_refresh_token
,
expires_at
:
new
Date
(
Date
.
now
()
+
expires_in
*
1000
),
refresh_expires_at
:
new
Date
(
Date
.
now
()
+
refresh_expires_in
*
1000
),
device_info
:
device_info
&&
Object
.
keys
(
device_info
).
length
>
0
?
device_info
:
null
,
session_id
:
matchingSession
.
id
,
},
type
:
QueryTypes
.
UPDATE
,
},
);
return
{
access_token
,
refresh_token
:
new_refresh_token
,
expires_in
,
refresh_expires_in
,
token_type
:
"Bearer"
,
};
}
/**
* Logout user (deactivate session)
*/
static
async
logout
(
accessToken
:
string
):
Promise
<
void
>
{
// Find session by decrypting access tokens
const
sessions
=
await
sequelize
.
query
(
`
SELECT id, session_token
FROM user_sessions
WHERE is_active = true
`
,
{
type
:
QueryTypes
.
SELECT
,
},
);
// Find matching session
let
sessionId
:
string
|
null
=
null
;
for
(
const
session
of
sessions
as
any
[])
{
try
{
const
decryptedToken
=
await
this
.
decryptToken
(
session
.
session_token
);
if
(
decryptedToken
===
accessToken
)
{
sessionId
=
session
.
id
;
break
;
}
}
catch
(
error
)
{
continue
;
}
}
if
(
sessionId
)
{
await
sequelize
.
query
(
"UPDATE user_sessions SET is_active = false WHERE id = :session_id"
,
{
replacements
:
{
session_id
:
sessionId
},
type
:
QueryTypes
.
UPDATE
,
});
}
}
/**
* Logout from all devices
*/
static
async
logoutAll
(
userId
:
string
):
Promise
<
void
>
{
await
UserSession
.
update
(
{
is_active
:
false
},
{
where
:
{
user_id
:
userId
,
is_active
:
true
,
},
},
);
}
/**
* Get user by ID with sessions
*/
static
async
getUserWithSessions
(
userId
:
string
):
Promise
<
User
|
null
>
{
return
User
.
findByPk
(
userId
,
{
include
:
[
{
model
:
UserSession
,
as
:
"sessions"
,
where
:
{
is_active
:
true
},
required
:
false
,
},
],
});
}
/**
* Validate session token
*/
static
async
validateSession
(
sessionToken
:
string
):
Promise
<
User
|
null
>
{
let
sessions
:
any
[];
try
{
// Find sessions and decrypt tokens to match
sessions
=
await
sequelize
.
query
(
`
SELECT us.*, u.email, u.username, u.first_name, u.last_name, u.status
FROM user_sessions us
JOIN users u ON us.user_id = u.id
WHERE us.is_active = true
AND us.expires_at > NOW()
`
,
{
type
:
QueryTypes
.
SELECT
,
},
);
}
catch
(
queryError
)
{
throw
queryError
;
}
// Find matching session by decrypting access tokens
let
matchingSession
:
any
=
null
;
let
decryptedToken
:
string
|
null
=
null
;
for
(
const
session
of
sessions
as
any
[])
{
try
{
const
decryptedAccessToken
=
await
this
.
decryptToken
(
session
.
session_token
);
if
(
decryptedAccessToken
===
sessionToken
)
{
matchingSession
=
session
;
decryptedToken
=
decryptedAccessToken
;
break
;
}
}
catch
(
error
)
{
// Skip invalid encrypted tokens
continue
;
}
}
if
(
!
matchingSession
||
!
decryptedToken
)
{
return
null
;
}
// Verify JWT token
try
{
this
.
verifyAccessToken
(
decryptedToken
);
}
catch
(
error
)
{
return
null
;
}
// Create user object
const
user
=
{
id
:
matchingSession
.
user_id
,
email
:
matchingSession
.
email
,
username
:
matchingSession
.
username
,
first_name
:
matchingSession
.
first_name
,
last_name
:
matchingSession
.
last_name
,
status
:
matchingSession
.
status
,
}
as
any
;
// Update last activity
try
{
await
sequelize
.
query
(
"UPDATE user_sessions SET last_activity_at = NOW() WHERE id = :session_id"
,
{
replacements
:
{
session_id
:
matchingSession
.
id
},
type
:
QueryTypes
.
UPDATE
,
});
}
catch
(
updateError
)
{
// Don't fail validation just because we can't update activity
}
return
user
;
}
/**
* Change user password
*/
static
async
changePassword
(
userId
:
string
,
oldPassword
:
string
,
newPassword
:
string
):
Promise
<
void
>
{
const
user
=
await
User
.
findByPk
(
userId
,
{
include
:
[{
model
:
UserAuth
,
as
:
"user_auth"
}],
});
if
(
!
user
)
{
throw
new
GenericError
({
vi
:
"Người dùng không tồn tại"
,
en
:
"User not found"
},
"NOT_FOUND"
,
404
);
}
const
userAuth
=
user
.
user_auth
;
if
(
!
userAuth
)
{
throw
new
GenericError
(
{
vi
:
"Dữ liệu xác thực không tồn tại"
,
en
:
"Authentication data not found"
},
"NOT_FOUND"
,
404
,
);
}
// Check if password can be changed (not too recently)
if
(
!
canUserChangePassword
(
userAuth
))
{
throw
new
GenericError
(
{
vi
:
"Mật khẩu chỉ có thể thay đổi sau 24 giờ"
,
en
:
"Password can only be changed after 24 hours"
},
"FORBIDDEN"
,
403
,
);
}
// Verify old password
const
isValidOldPassword
=
await
this
.
verifyPassword
(
oldPassword
,
userAuth
.
password_hash
);
if
(
!
isValidOldPassword
)
{
throw
new
GenericError
({
vi
:
"Mật khẩu cũ không đúng"
,
en
:
"Invalid old password"
},
"UNAUTHORIZED"
,
401
);
}
// Hash new password
const
newPasswordHash
=
await
this
.
hashPassword
(
newPassword
);
// Check if password was recently used
const
passwordHistory
=
(
userAuth
.
password_history
as
string
[])
||
[];
if
(
isPasswordRecentlyUsed
(
userAuth
,
passwordHistory
,
newPasswordHash
))
{
throw
new
GenericError
(
{
vi
:
"Mật khẩu đã được sử dụng gần đây"
,
en
:
"Password was recently used"
},
"FORBIDDEN"
,
403
,
);
}
// Update password history
passwordHistory
.
unshift
(
userAuth
.
password_hash
);
// Add old hash
if
(
passwordHistory
.
length
>
5
)
passwordHistory
.
pop
();
// Keep last 5
userAuth
.
password_history
=
passwordHistory
;
// Update password
userAuth
.
password_hash
=
newPasswordHash
;
await
updateUserPasswordChanged
(
userAuth
);
// Logout from all other sessions
await
this
.
logoutAll
(
userId
);
}
/**
* Parse time string to seconds (e.g., '15m' -> 900)
*/
private
static
parseTimeToSeconds
(
timeStr
:
string
|
undefined
):
number
{
if
(
!
timeStr
)
return
900
;
// 15 minutes default
const
regex
=
/^
(\d
+
)([
smhd
])
$/
;
const
match
=
timeStr
.
match
(
regex
);
if
(
!
match
)
{
throw
new
Error
(
`Invalid time format:
${
timeStr
||
"undefined"
}
`
);
}
const
value
=
parseInt
(
match
[
1
]
||
"0"
);
const
unit
=
match
[
2
]
||
"m"
;
switch
(
unit
)
{
case
"s"
:
return
value
;
case
"m"
:
return
value
*
60
;
case
"h"
:
return
value
*
60
*
60
;
case
"d"
:
return
value
*
60
*
60
*
24
;
default
:
throw
new
Error
(
`Invalid time unit:
${
unit
}
`
);
}
}
}
}
src/templates/swagger/auth/schemas.js
View file @
d505ed0d
...
@@ -6,23 +6,67 @@ module.exports = {
...
@@ -6,23 +6,67 @@ module.exports = {
email
:
{
email
:
{
type
:
"string"
,
type
:
"string"
,
format
:
"email"
,
format
:
"email"
,
example
:
"
user@example
.com"
,
example
:
"
admin@admin
.com"
,
},
},
password
:
{
password
:
{
type
:
"string"
,
type
:
"string"
,
example
:
"
password
123"
,
example
:
"
admin
123"
,
},
},
device_info
:
{
device_info
:
{
type
:
"object"
,
type
:
"object"
,
description
:
"Device information for session tracking"
,
description
:
"Device information for session tracking"
,
},
},
ip_address
:
{
},
},
RegisterRequest
:
{
type
:
"object"
,
required
:
[
"email"
,
"password"
],
properties
:
{
email
:
{
type
:
"string"
,
type
:
"string"
,
description
:
"IP address for security logging"
,
format
:
"email"
,
example
:
"mentor1@example.com"
,
},
},
user_agent
:
{
password
:
{
type
:
"string"
,
example
:
"password123"
,
},
username
:
{
type
:
"string"
,
type
:
"string"
,
description
:
"User agent string"
,
example
:
"mentor_john"
,
},
first_name
:
{
type
:
"string"
,
example
:
"John"
,
},
last_name
:
{
type
:
"string"
,
example
:
"Doe"
,
},
phone
:
{
type
:
"string"
,
example
:
"0912345678"
,
},
roles
:
{
type
:
"array"
,
items
:
{
type
:
"string"
,
enum
:
[
"admin"
,
"mentor"
,
"student"
],
},
example
:
[
"mentor"
],
},
},
},
RefreshRequest
:
{
type
:
"object"
,
properties
:
{
refresh_token
:
{
type
:
"string"
,
description
:
"Refresh token (optional if sent via cookie)"
,
},
device_info
:
{
type
:
"object"
,
description
:
"Device information"
,
},
},
},
},
},
},
...
...
src/templates/swagger/common/responses.js
View file @
d505ed0d
...
@@ -129,4 +129,69 @@ module.exports = {
...
@@ -129,4 +129,69 @@ module.exports = {
},
},
},
},
},
},
Forbidden
:
{
description
:
"Forbidden"
,
content
:
{
"application/json"
:
{
schema
:
{
type
:
"object"
,
properties
:
{
success
:
{
type
:
"boolean"
,
example
:
false
,
},
error
:
{
type
:
"object"
,
properties
:
{
code
:
{
type
:
"string"
,
example
:
"FORBIDDEN"
,
},
message
:
{
type
:
"object"
,
properties
:
{
vi
:
{
type
:
"string"
,
example
:
"Bạn không có quyền thực hiện hành động này"
,
},
en
:
{
type
:
"string"
,
example
:
"You do not have permission to perform this action"
,
},
},
},
},
},
message
:
{
type
:
"string"
,
nullable
:
true
,
},
message_en
:
{
type
:
"string"
,
nullable
:
true
,
},
responseData
:
{
type
:
"object"
,
nullable
:
true
,
},
status
:
{
type
:
"string"
,
example
:
"fail"
,
},
timeStamp
:
{
type
:
"string"
,
example
:
"2025-12-07 10:00:00"
,
},
violation
:
{
type
:
"array"
,
items
:
{
type
:
"object"
,
},
nullable
:
true
,
},
},
},
},
},
},
};
};
src/templates/swagger/common/securitySchemes.js
View file @
d505ed0d
module
.
exports
=
{
module
.
exports
=
{
Bearer
:
{
Bearer
Auth
:
{
name
:
"Authorization
"
,
type
:
"http
"
,
in
:
"head
er"
,
scheme
:
"bear
er"
,
type
:
"apiKey
"
,
bearerFormat
:
"JWT
"
,
},
},
};
};
src/utils/authUtils.ts
View file @
d505ed0d
import
{
User
}
from
"../models/User"
;
import
{
IUser
}
from
"../models/mongodb/User"
;
import
{
UserAuth
}
from
"../models/UserAuth"
;
import
{
IUserSession
}
from
"../models/mongodb/UserSession"
;
import
{
UserSession
}
from
"../models/UserSession"
;
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
/**
/**
* Authentication utility functions
* Authentication utility functions for Mongoose models
* These functions provide the logic that would normally be instance methods
* but are kept separate to avoid conflicts with auto-generated models
*/
*/
/**
* Maximum number of login attempts before account lockout
*/
const
MAX_LOGIN_ATTEMPTS
=
5
;
const
MAX_LOGIN_ATTEMPTS
=
5
;
/**
export
const
isUserLocked
=
(
user
:
IUser
):
boolean
=>
{
* Check if user account is locked due to too many failed login attempts
if
(
user
.
lock_until
)
{
*/
return
dayjs
().
isBefore
(
dayjs
(
user
.
lock_until
));
export
const
isUserLocked
=
(
userAuth
:
UserAuth
):
boolean
=>
{
}
if
(
userAuth
.
locked_until
)
{
return
false
;
const
now
=
dayjs
();
const
lockedUntil
=
dayjs
(
userAuth
.
locked_until
);
return
now
.
isBefore
(
lockedUntil
);
}
return
false
;
};
/**
* Increment login attempts for a user
*/
export
const
incrementUserLoginAttempts
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
void
>
=>
{
const
currentAttempts
=
(
userAuthInstance
.
login_attempts
||
0
)
+
1
;
// Lock account if too many attempts
const
updates
:
any
=
{
login_attempts
:
currentAttempts
};
if
(
currentAttempts
>=
MAX_LOGIN_ATTEMPTS
)
{
updates
.
locked_until
=
dayjs
().
add
(
15
,
"minutes"
).
toDate
();
}
await
userAuthInstance
.
update
(
updates
);
};
/**
* Reset login attempts for a user (on successful login)
*/
export
const
resetUserLoginAttempts
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
void
>
=>
{
await
userAuthInstance
.
update
({
login_attempts
:
0
,
locked_until
:
new
Date
(
0
),
});
};
/**
* Unlock user account (reset lockout)
*/
export
const
unlockUserAccount
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
void
>
=>
{
await
userAuthInstance
.
update
({
locked_until
:
new
Date
(
0
),
login_attempts
:
0
,
});
};
/**
* Check if user account lockout has expired and unlock if needed
*/
export
const
checkAndUnlockExpiredLockout
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
boolean
>
=>
{
if
(
userAuthInstance
.
locked_until
)
{
const
now
=
dayjs
();
const
lockedUntil
=
dayjs
(
userAuthInstance
.
locked_until
);
if
(
now
.
isAfter
(
lockedUntil
))
{
await
unlockUserAccount
(
userAuthInstance
);
return
true
;
// Account was unlocked
}
}
return
false
;
// Account was not locked or still locked
};
/**
* Update user's last login timestamp
*/
export
const
updateUserLastLogin
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
void
>
=>
{
await
userAuthInstance
.
update
({
last_login_at
:
new
Date
(),
});
};
/**
* Check if user session is expired
*/
export
const
isSessionExpired
=
(
session
:
UserSession
):
boolean
=>
{
const
now
=
dayjs
();
const
expiresAt
=
dayjs
(
session
.
expires_at
);
return
now
.
isAfter
(
expiresAt
);
};
/**
* Check if user session refresh token is expired
*/
export
const
isSessionRefreshExpired
=
(
session
:
UserSession
):
boolean
=>
{
const
now
=
dayjs
();
const
refreshExpiresAt
=
dayjs
(
session
.
refresh_expires_at
);
return
now
.
isAfter
(
refreshExpiresAt
);
};
};
/**
export
const
incrementUserLoginAttempts
=
async
(
user
:
IUser
):
Promise
<
void
>
=>
{
* Deactivate a user session
const
currentAttempts
=
(
user
.
login_attempts
||
0
)
+
1
;
*/
const
updates
:
any
=
{
login_attempts
:
currentAttempts
};
export
const
deactivateUserSession
=
async
(
sessionInstance
:
UserSession
):
Promise
<
void
>
=>
{
await
sessionInstance
.
update
({
is_active
:
false
});
if
(
currentAttempts
>=
MAX_LOGIN_ATTEMPTS
)
{
updates
.
lock_until
=
dayjs
().
add
(
15
,
"minutes"
).
toDate
();
}
await
user
.
updateOne
(
updates
);
};
};
/**
export
const
resetUserLoginAttempts
=
async
(
user
:
IUser
):
Promise
<
void
>
=>
{
* Update session activity timestamp
await
user
.
updateOne
({
*/
login_attempts
:
0
,
export
const
updateSessionActivity
=
async
(
sessionInstance
:
UserSession
):
Promise
<
void
>
=>
{
lock_until
:
new
Date
(
0
),
await
sessionInstance
.
update
({
last_activity_at
:
new
Date
()
});
});
};
};
/**
export
const
unlockUserAccount
=
async
(
user
:
IUser
):
Promise
<
void
>
=>
{
* Check if user can change password (not recently changed)
await
user
.
updateOne
({
*/
lock_until
:
new
Date
(
0
),
export
const
canUserChangePassword
=
(
userAuth
:
UserAuth
,
minDaysBetweenChanges
:
number
=
1
):
boolean
=>
{
login_attempts
:
0
,
if
(
!
userAuth
.
password_changed_at
)
return
true
;
});
const
lastChange
=
dayjs
(
userAuth
.
password_changed_at
);
const
now
=
dayjs
();
const
daysSinceChange
=
now
.
diff
(
lastChange
,
"day"
);
return
daysSinceChange
>=
minDaysBetweenChanges
;
};
};
/**
export
const
checkAndUnlockExpiredLockout
=
async
(
user
:
IUser
):
Promise
<
boolean
>
=>
{
* Update user's password changed timestamp
if
(
user
.
lock_until
&&
dayjs
().
isAfter
(
dayjs
(
user
.
lock_until
)))
{
*/
await
unlockUserAccount
(
user
);
export
const
updateUserPasswordChanged
=
async
(
userAuthInstance
:
UserAuth
):
Promise
<
void
>
=>
{
return
true
;
await
userAuthInstance
.
update
({
}
password_changed_at
:
new
Date
(),
return
false
;
});
};
};
/**
export
const
updateUserLastLogin
=
async
(
user
:
IUser
):
Promise
<
void
>
=>
{
* Check if password was used recently (prevent reuse)
await
user
.
updateOne
({
*/
last_login
:
new
Date
(),
export
const
isPasswordRecentlyUsed
=
(
});
userAuth
:
UserAuth
,
passwordHistory
:
string
[],
newPasswordHash
:
string
,
):
boolean
=>
{
// Check if new password hash matches any in recent history
return
passwordHistory
.
includes
(
newPasswordHash
);
};
};
/**
export
const
isSessionExpired
=
(
session
:
IUserSession
):
boolean
=>
{
* Get user full name
return
dayjs
().
isAfter
(
dayjs
(
session
.
expires_at
));
*/
export
const
getUserFullName
=
(
user
:
User
):
string
=>
{
const
firstName
=
user
.
first_name
||
""
;
const
lastName
=
user
.
last_name
||
""
;
return
`
${
firstName
}
${
lastName
}
`
.
trim
();
};
};
/**
export
const
isSessionRefreshExpired
=
(
session
:
IUserSession
):
boolean
=>
{
* Check if user has specific role
return
dayjs
().
isAfter
(
dayjs
(
session
.
refresh_expires_at
));
*/
export
const
hasUserRole
=
(
user
:
User
,
role
:
string
):
boolean
=>
{
return
(
user
as
any
).
role
?.
name
===
role
;
};
};
/**
export
const
deactivateUserSession
=
async
(
session
:
IUserSession
):
Promise
<
void
>
=>
{
* Check if user has admin privileges
await
session
.
updateOne
({
is_active
:
false
});
*/
export
const
isUserAdmin
=
(
user
:
User
):
boolean
=>
{
return
(
user
as
any
).
role
?.
name
===
"admin"
||
(
user
as
any
).
role
?.
name
===
"system_admin"
;
};
};
/**
export
const
updateSessionActivity
=
async
(
session
:
IUserSession
):
Promise
<
void
>
=>
{
* Check if user is system admin
await
session
.
updateOne
({
last_activity_at
:
new
Date
()
});
*/
export
const
isUserSystemAdmin
=
(
user
:
User
):
boolean
=>
{
return
(
user
as
any
).
role
?.
name
===
"system_admin"
;
};
};
/**
export
const
canUserChangePassword
=
(
user
:
IUser
,
minDaysBetweenChanges
:
number
=
1
):
boolean
=>
{
* Get user status display text
if
(
!
user
.
password_changed_at
)
return
true
;
*/
return
dayjs
().
diff
(
dayjs
(
user
.
password_changed_at
),
"day"
)
>=
minDaysBetweenChanges
;
export
const
getUserStatusText
=
(
status
:
string
):
{
vi
:
string
;
en
:
string
}
=>
{
const
statusMap
:
Record
<
string
,
{
vi
:
string
;
en
:
string
}
>
=
{
active
:
{
vi
:
"Hoạt động"
,
en
:
"Active"
},
inactive
:
{
vi
:
"Không hoạt động"
,
en
:
"Inactive"
},
suspended
:
{
vi
:
"Đã tạm ngừng"
,
en
:
"Suspended"
},
pending_verification
:
{
vi
:
"Chờ xác minh"
,
en
:
"Pending Verification"
},
};
return
statusMap
[
status
]
||
{
vi
:
"Không xác định"
,
en
:
"Unknown"
};
};
};
/**
* Get role display text
*/
export
const
getRoleText
=
(
role
:
string
):
{
vi
:
string
;
en
:
string
}
=>
{
export
const
getRoleText
=
(
role
:
string
):
{
vi
:
string
;
en
:
string
}
=>
{
const
roleMap
:
Record
<
string
,
{
vi
:
string
;
en
:
string
}
>
=
{
const
roleMap
:
Record
<
string
,
{
vi
:
string
;
en
:
string
}
>
=
{
user
:
{
vi
:
"Người dùng"
,
en
:
"User"
},
student
:
{
vi
:
"Sinh viên"
,
en
:
"Student"
},
admin
:
{
vi
:
"Quản trị viên"
,
en
:
"Administrator"
},
mentor
:
{
vi
:
"Người hướng dẫn"
,
en
:
"Mentor"
},
system_admin
:
{
vi
:
"Quản trị hệ thống"
,
en
:
"System Administrator"
},
admin
:
{
vi
:
"Quản trị viên"
,
en
:
"Administrator"
},
};
};
return
roleMap
[
role
]
||
{
vi
:
"Không xác định"
,
en
:
"Unknown"
};
return
roleMap
[
role
]
||
{
vi
:
"Không xác định"
,
en
:
"Unknown"
};
};
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment