Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
finwise-miniapp-be
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
ThinhNC
finwise-miniapp-be
Commits
bd98f9fe
Commit
bd98f9fe
authored
Jul 23, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'develop' into 'master'
Develop See merge request
!4
parents
9e8a7adc
ab1b41a5
Changes
23
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
1118 additions
and
96 deletions
+1118
-96
package.json
package.json
+4
-0
pnpm-lock.yaml
pnpm-lock.yaml
+48
-0
schema.prisma
prisma/schema.prisma
+59
-14
seed.ts
prisma/seed.ts
+21
-21
app.ts
src/app.ts
+2
-0
export-type.constant.ts
src/common/constants/export-type.constant.ts
+0
-10
job-status.constant.ts
src/common/constants/job-status.constant.ts
+0
-11
role.constant.ts
src/common/constants/role.constant.ts
+2
-2
mail.service.ts
src/common/services/mail.service.ts
+98
-0
mail.config.ts
src/config/mail.config.ts
+11
-0
swagger.config.ts
src/config/swagger.config.ts
+243
-6
auth.middleware.ts
src/middlewares/auth.middleware.ts
+9
-4
auth.controller.ts
src/modules/auth/auth.controller.ts
+144
-6
auth.dto.ts
src/modules/auth/auth.dto.ts
+32
-2
auth.repository.ts
src/modules/auth/auth.repository.ts
+158
-4
auth.route.ts
src/modules/auth/auth.route.ts
+8
-1
auth.service.ts
src/modules/auth/auth.service.ts
+152
-1
auth.validation.ts
src/modules/auth/auth.validation.ts
+60
-2
user.controller.ts
src/modules/users/user.controller.ts
+15
-0
user.repository.ts
src/modules/users/user.repository.ts
+31
-6
user.route.ts
src/modules/users/user.route.ts
+6
-4
user.service.ts
src/modules/users/user.service.ts
+6
-0
user.validation.ts
src/modules/users/user.validation.ts
+9
-2
No files found.
package.json
View file @
bd98f9fe
...
@@ -22,22 +22,26 @@
...
@@ -22,22 +22,26 @@
"dependencies"
:
{
"dependencies"
:
{
"
@prisma/client
"
:
"
^5.22.0
"
,
"
@prisma/client
"
:
"
^5.22.0
"
,
"
bcryptjs
"
:
"
^2.4.3
"
,
"
bcryptjs
"
:
"
^2.4.3
"
,
"
cookie-parser
"
:
"
^1.4.7
"
,
"
cors
"
:
"
^2.8.5
"
,
"
cors
"
:
"
^2.8.5
"
,
"
dotenv
"
:
"
^16.4.7
"
,
"
dotenv
"
:
"
^16.4.7
"
,
"
express
"
:
"
^4.21.2
"
,
"
express
"
:
"
^4.21.2
"
,
"
helmet
"
:
"
^8.0.0
"
,
"
helmet
"
:
"
^8.0.0
"
,
"
jsonwebtoken
"
:
"
^9.0.2
"
,
"
jsonwebtoken
"
:
"
^9.0.2
"
,
"
morgan
"
:
"
^1.10.0
"
,
"
morgan
"
:
"
^1.10.0
"
,
"
nodemailer
"
:
"
^9.0.3
"
,
"
swagger-ui-express
"
:
"
^5.0.1
"
,
"
swagger-ui-express
"
:
"
^5.0.1
"
,
"
zod
"
:
"
^3.24.1
"
"
zod
"
:
"
^3.24.1
"
},
},
"devDependencies"
:
{
"devDependencies"
:
{
"
@types/bcryptjs
"
:
"
^2.4.6
"
,
"
@types/bcryptjs
"
:
"
^2.4.6
"
,
"
@types/cookie-parser
"
:
"
^1.4.10
"
,
"
@types/cors
"
:
"
^2.8.17
"
,
"
@types/cors
"
:
"
^2.8.17
"
,
"
@types/express
"
:
"
^4.17.21
"
,
"
@types/express
"
:
"
^4.17.21
"
,
"
@types/jsonwebtoken
"
:
"
^9.0.7
"
,
"
@types/jsonwebtoken
"
:
"
^9.0.7
"
,
"
@types/morgan
"
:
"
^1.9.9
"
,
"
@types/morgan
"
:
"
^1.9.9
"
,
"
@types/node
"
:
"
^22.10.2
"
,
"
@types/node
"
:
"
^22.10.2
"
,
"
@types/nodemailer
"
:
"
^8.0.1
"
,
"
@types/swagger-ui-express
"
:
"
^4.1.8
"
,
"
@types/swagger-ui-express
"
:
"
^4.1.8
"
,
"
eslint
"
:
"
^9.17.0
"
,
"
eslint
"
:
"
^9.17.0
"
,
"
prettier
"
:
"
^3.4.2
"
,
"
prettier
"
:
"
^3.4.2
"
,
...
...
pnpm-lock.yaml
View file @
bd98f9fe
...
@@ -14,6 +14,9 @@ importers:
...
@@ -14,6 +14,9 @@ importers:
bcryptjs
:
bcryptjs
:
specifier
:
^2.4.3
specifier
:
^2.4.3
version
:
2.4.3
version
:
2.4.3
cookie-parser
:
specifier
:
^1.4.7
version
:
1.4.7
cors
:
cors
:
specifier
:
^2.8.5
specifier
:
^2.8.5
version
:
2.8.6
version
:
2.8.6
...
@@ -32,6 +35,9 @@ importers:
...
@@ -32,6 +35,9 @@ importers:
morgan
:
morgan
:
specifier
:
^1.10.0
specifier
:
^1.10.0
version
:
1.11.0
version
:
1.11.0
nodemailer
:
specifier
:
^9.0.3
version
:
9.0.3
swagger-ui-express
:
swagger-ui-express
:
specifier
:
^5.0.1
specifier
:
^5.0.1
version
:
5.0.1(express@4.22.2)
version
:
5.0.1(express@4.22.2)
...
@@ -42,6 +48,9 @@ importers:
...
@@ -42,6 +48,9 @@ importers:
'
@types/bcryptjs'
:
'
@types/bcryptjs'
:
specifier
:
^2.4.6
specifier
:
^2.4.6
version
:
2.4.6
version
:
2.4.6
'
@types/cookie-parser'
:
specifier
:
^1.4.10
version
:
1.4.10(@types/express@4.17.25)
'
@types/cors'
:
'
@types/cors'
:
specifier
:
^2.8.17
specifier
:
^2.8.17
version
:
2.8.19
version
:
2.8.19
...
@@ -57,6 +66,9 @@ importers:
...
@@ -57,6 +66,9 @@ importers:
'
@types/node'
:
'
@types/node'
:
specifier
:
^22.10.2
specifier
:
^22.10.2
version
:
22.20.0
version
:
22.20.0
'
@types/nodemailer'
:
specifier
:
^8.0.1
version
:
8.0.1
'
@types/swagger-ui-express'
:
'
@types/swagger-ui-express'
:
specifier
:
^4.1.8
specifier
:
^4.1.8
version
:
4.1.8
version
:
4.1.8
...
@@ -360,6 +372,11 @@ packages:
...
@@ -360,6 +372,11 @@ packages:
'
@types/connect@3.4.38'
:
'
@types/connect@3.4.38'
:
resolution
:
{
integrity
:
sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
}
resolution
:
{
integrity
:
sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
}
'
@types/cookie-parser@1.4.10'
:
resolution
:
{
integrity
:
sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==
}
peerDependencies
:
'
@types/express'
:
'
*'
'
@types/cors@2.8.19'
:
'
@types/cors@2.8.19'
:
resolution
:
{
integrity
:
sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==
}
resolution
:
{
integrity
:
sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==
}
...
@@ -393,6 +410,9 @@ packages:
...
@@ -393,6 +410,9 @@ packages:
'
@types/node@22.20.0'
:
'
@types/node@22.20.0'
:
resolution
:
{
integrity
:
sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==
}
resolution
:
{
integrity
:
sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==
}
'
@types/nodemailer@8.0.1'
:
resolution
:
{
integrity
:
sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==
}
'
@types/qs@6.15.1'
:
'
@types/qs@6.15.1'
:
resolution
:
{
integrity
:
sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==
}
resolution
:
{
integrity
:
sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==
}
...
@@ -528,6 +548,13 @@ packages:
...
@@ -528,6 +548,13 @@ packages:
resolution
:
{
integrity
:
sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
}
resolution
:
{
integrity
:
sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
}
engines
:
{
node
:
'
>=
0.6'
}
engines
:
{
node
:
'
>=
0.6'
}
cookie-parser@1.4.7
:
resolution
:
{
integrity
:
sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==
}
engines
:
{
node
:
'
>=
0.8.0'
}
cookie-signature@1.0.6
:
resolution
:
{
integrity
:
sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
}
cookie-signature@1.0.7
:
cookie-signature@1.0.7
:
resolution
:
{
integrity
:
sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
}
resolution
:
{
integrity
:
sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
}
...
@@ -939,6 +966,10 @@ packages:
...
@@ -939,6 +966,10 @@ packages:
resolution
:
{
integrity
:
sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
}
resolution
:
{
integrity
:
sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
}
engines
:
{
node
:
'
>=
0.6'
}
engines
:
{
node
:
'
>=
0.6'
}
nodemailer@9.0.3
:
resolution
:
{
integrity
:
sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==
}
engines
:
{
node
:
'
>=6.0.0'
}
normalize-path@3.0.0
:
normalize-path@3.0.0
:
resolution
:
{
integrity
:
sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
}
resolution
:
{
integrity
:
sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
}
engines
:
{
node
:
'
>=0.10.0'
}
engines
:
{
node
:
'
>=0.10.0'
}
...
@@ -1452,6 +1483,10 @@ snapshots:
...
@@ -1452,6 +1483,10 @@ snapshots:
dependencies
:
dependencies
:
'
@types/node'
:
22.20.0
'
@types/node'
:
22.20.0
'
@types/cookie-parser@1.4.10(@types/express@4.17.25)'
:
dependencies
:
'
@types/express'
:
4.17.25
'
@types/cors@2.8.19'
:
'
@types/cors@2.8.19'
:
dependencies
:
dependencies
:
'
@types/node'
:
22.20.0
'
@types/node'
:
22.20.0
...
@@ -1493,6 +1528,10 @@ snapshots:
...
@@ -1493,6 +1528,10 @@ snapshots:
dependencies
:
dependencies
:
undici-types
:
6.21.0
undici-types
:
6.21.0
'
@types/nodemailer@8.0.1'
:
dependencies
:
'
@types/node'
:
22.20.0
'
@types/qs@6.15.1'
:
{}
'
@types/qs@6.15.1'
:
{}
'
@types/range-parser@1.2.7'
:
{}
'
@types/range-parser@1.2.7'
:
{}
...
@@ -1643,6 +1682,13 @@ snapshots:
...
@@ -1643,6 +1682,13 @@ snapshots:
content-type@1.0.5
:
{}
content-type@1.0.5
:
{}
cookie-parser@1.4.7
:
dependencies
:
cookie
:
0.7.2
cookie-signature
:
1.0.6
cookie-signature@1.0.6
:
{}
cookie-signature@1.0.7
:
{}
cookie-signature@1.0.7
:
{}
cookie@0.7.2
:
{}
cookie@0.7.2
:
{}
...
@@ -2094,6 +2140,8 @@ snapshots:
...
@@ -2094,6 +2140,8 @@ snapshots:
negotiator@0.6.3
:
{}
negotiator@0.6.3
:
{}
nodemailer@9.0.3
:
{}
normalize-path@3.0.0
:
{}
normalize-path@3.0.0
:
{}
object-assign@4.1.1
:
{}
object-assign@4.1.1
:
{}
...
...
prisma/schema.prisma
View file @
bd98f9fe
...
@@ -13,22 +13,29 @@ enum TransactionType {
...
@@ -13,22 +13,29 @@ enum TransactionType {
}
}
model
User
{
model
User
{
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
email
String
@
unique
email
String
?
@
unique
password
String
password
String
?
fullName
String
?
@
map
(
"full_name"
)
fullName
String
?
@
map
(
"full_name"
)
isActive
Boolean
@
default
(
true
)
@
map
(
"is_active"
)
avatarUrl
String
?
@
map
(
"avatar_url"
)
roleId
String
@
map
(
"role_id"
)
@
db
.
Uuid
phoneNumber
String
?
@
unique
@
map
(
"phone_number"
)
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
isActive
Boolean
@
default
(
false
)
@
map
(
"is_active"
)
updatedAt
DateTime
@
updatedAt
@
map
(
"updated_at"
)
deletedAt
DateTime
?
@
map
(
"deleted_at"
)
deletedBy
String
?
@
map
(
"deleted_by"
)
@
db
.
Uuid
roleId
String
@
map
(
"role_id"
)
@
db
.
Uuid
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
updatedAt
DateTime
@
updatedAt
@
map
(
"updated_at"
)
//
Relations
//
Relations
role
Role
@
relation
(
fields
:
[
roleId
],
references
:
[
id
],
onDelete
:
Cascade
)
role
Role
@
relation
(
fields
:
[
roleId
],
references
:
[
id
],
onDelete
:
Cascade
)
wallets
Wallet
[]
wallets
Wallet
[]
categories
Category
[]
categories
Category
[]
transactions
Transaction
[]
transactions
Transaction
[]
budgets
Budget
[]
budgets
Budget
[]
refreshTokens
RefreshToken
[]
refreshTokens
RefreshToken
[]
socialAccounts
UserSocial
[]
verificationTokens
VerificationToken
[]
passwordResetTokens
PasswordResetToken
[]
@@
map
(
"users"
)
@@
map
(
"users"
)
}
}
...
@@ -124,3 +131,41 @@ model RefreshToken {
...
@@ -124,3 +131,41 @@ model RefreshToken {
@@
index
([
userId
])
@@
index
([
userId
])
@@
map
(
"refresh_tokens"
)
@@
map
(
"refresh_tokens"
)
}
}
model
UserSocial
{
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
userId
String
@
map
(
"user_id"
)
@
db
.
Uuid
provider
String
//
"ZALO"
,
"GOOGLE"
,
etc
.
providerUserId
String
@
map
(
"provider_user_id"
)
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
@@
unique
([
provider
,
providerUserId
])
@@
map
(
"user_socials"
)
}
model
VerificationToken
{
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
token
String
@
unique
userId
String
@
map
(
"user_id"
)
@
db
.
Uuid
expiresAt
DateTime
@
map
(
"expires_at"
)
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
@@
map
(
"verification_tokens"
)
}
model
PasswordResetToken
{
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
token
String
@
unique
userId
String
@
map
(
"user_id"
)
@
db
.
Uuid
expiresAt
DateTime
@
map
(
"expires_at"
)
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
@@
map
(
"password_reset_tokens"
)
}
prisma/seed.ts
View file @
bd98f9fe
...
@@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs';
...
@@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs';
const
prisma
=
new
PrismaClient
();
const
prisma
=
new
PrismaClient
();
async
function
main
()
{
async
function
main
()
{
const
roles
=
[
'ADMIN'
,
'
LEADER'
,
'INTERN
'
];
const
roles
=
[
'ADMIN'
,
'
MANAGER'
,
'USER
'
];
const
roleMap
:
Record
<
string
,
string
>
=
{};
const
roleMap
:
Record
<
string
,
string
>
=
{};
for
(
const
roleName
of
roles
)
{
for
(
const
roleName
of
roles
)
{
...
@@ -20,11 +20,11 @@ async function main() {
...
@@ -20,11 +20,11 @@ async function main() {
const
adminEmail
=
'admin@finwise.local'
;
const
adminEmail
=
'admin@finwise.local'
;
const
adminPassword
=
await
bcrypt
.
hash
(
'Admin@123456'
,
10
);
const
adminPassword
=
await
bcrypt
.
hash
(
'Admin@123456'
,
10
);
const
leaderEmail
=
'lead
er@finwise.local'
;
const
managerEmail
=
'manag
er@finwise.local'
;
const
leaderPassword
=
await
bcrypt
.
hash
(
'Lead
er@123456'
,
10
);
const
managerPassword
=
await
bcrypt
.
hash
(
'Manag
er@123456'
,
10
);
const
internEmail
=
'intern
@finwise.local'
;
const
userEmail
=
'user
@finwise.local'
;
const
internPassword
=
await
bcrypt
.
hash
(
'Intern
@123456'
,
10
);
const
userPassword
=
await
bcrypt
.
hash
(
'User
@123456'
,
10
);
await
prisma
.
user
.
upsert
({
await
prisma
.
user
.
upsert
({
where
:
{
email
:
adminEmail
},
where
:
{
email
:
adminEmail
},
...
@@ -44,35 +44,35 @@ async function main() {
...
@@ -44,35 +44,35 @@ async function main() {
});
});
await
prisma
.
user
.
upsert
({
await
prisma
.
user
.
upsert
({
where
:
{
email
:
lead
erEmail
},
where
:
{
email
:
manag
erEmail
},
update
:
{
update
:
{
password
:
lead
erPassword
,
password
:
manag
erPassword
,
fullName
:
'
Lead
er'
,
fullName
:
'
Manag
er'
,
roleId
:
roleMap
[
'
LEAD
ER'
],
roleId
:
roleMap
[
'
MANAG
ER'
],
isActive
:
true
,
isActive
:
true
,
},
},
create
:
{
create
:
{
email
:
lead
erEmail
,
email
:
manag
erEmail
,
password
:
lead
erPassword
,
password
:
manag
erPassword
,
fullName
:
'
Lead
er'
,
fullName
:
'
Manag
er'
,
roleId
:
roleMap
[
'
LEAD
ER'
],
roleId
:
roleMap
[
'
MANAG
ER'
],
isActive
:
true
,
isActive
:
true
,
},
},
});
});
await
prisma
.
user
.
upsert
({
await
prisma
.
user
.
upsert
({
where
:
{
email
:
intern
Email
},
where
:
{
email
:
user
Email
},
update
:
{
update
:
{
password
:
intern
Password
,
password
:
user
Password
,
fullName
:
'
Intern
'
,
fullName
:
'
User
'
,
roleId
:
roleMap
[
'
INTERN
'
],
roleId
:
roleMap
[
'
USER
'
],
isActive
:
true
,
isActive
:
true
,
},
},
create
:
{
create
:
{
email
:
intern
Email
,
email
:
user
Email
,
password
:
intern
Password
,
password
:
user
Password
,
fullName
:
'
Intern
'
,
fullName
:
'
User
'
,
roleId
:
roleMap
[
'
INTERN
'
],
roleId
:
roleMap
[
'
USER
'
],
isActive
:
true
,
isActive
:
true
,
},
},
});
});
...
...
src/app.ts
View file @
bd98f9fe
...
@@ -3,6 +3,7 @@ import cors from 'cors';
...
@@ -3,6 +3,7 @@ import cors from 'cors';
import
helmet
from
'helmet'
;
import
helmet
from
'helmet'
;
import
morgan
from
'morgan'
;
import
morgan
from
'morgan'
;
import
swaggerUi
from
'swagger-ui-express'
;
import
swaggerUi
from
'swagger-ui-express'
;
import
cookieParser
from
'cookie-parser'
;
import
{
errorMiddleware
,
notFoundMiddleware
}
from
'./middlewares/error.middleware'
;
import
{
errorMiddleware
,
notFoundMiddleware
}
from
'./middlewares/error.middleware'
;
import
routes
from
'./routes'
;
import
routes
from
'./routes'
;
import
{
swaggerSpec
,
swaggerOptions
}
from
'./config/swagger.config'
;
import
{
swaggerSpec
,
swaggerOptions
}
from
'./config/swagger.config'
;
...
@@ -17,6 +18,7 @@ app.use(cors());
...
@@ -17,6 +18,7 @@ app.use(cors());
app
.
use
(
morgan
(
'dev'
));
app
.
use
(
morgan
(
'dev'
));
app
.
use
(
express
.
json
());
app
.
use
(
express
.
json
());
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
app
.
use
(
express
.
urlencoded
({
extended
:
true
}));
app
.
use
(
cookieParser
());
app
.
use
(
'/api/docs'
,
swaggerUi
.
serve
,
swaggerUi
.
setup
(
swaggerSpec
,
swaggerOptions
));
app
.
use
(
'/api/docs'
,
swaggerUi
.
serve
,
swaggerUi
.
setup
(
swaggerSpec
,
swaggerOptions
));
...
...
src/common/constants/export-type.constant.ts
deleted
100644 → 0
View file @
9e8a7adc
export
const
EXPORT_TYPE
=
{
JSON
:
'JSON'
,
CSV
:
'CSV'
,
XLSX
:
'XLSX'
,
MARKDOWN
:
'MARKDOWN'
,
MARKDOWN_ZIP
:
'MARKDOWN_ZIP'
,
FULL_ZIP
:
'FULL_ZIP'
,
}
as
const
;
export
type
ExportType
=
keyof
typeof
EXPORT_TYPE
;
src/common/constants/job-status.constant.ts
deleted
100644 → 0
View file @
9e8a7adc
export
const
JOB_STATUS
=
{
PENDING
:
'PENDING'
,
RUNNING
:
'RUNNING'
,
COMPLETED
:
'COMPLETED'
,
PARTIAL_COMPLETED
:
'PARTIAL_COMPLETED'
,
FAILED
:
'FAILED'
,
CANCELED
:
'CANCELED'
,
BLOCKED
:
'BLOCKED'
,
}
as
const
;
export
type
JobStatus
=
keyof
typeof
JOB_STATUS
;
src/common/constants/role.constant.ts
View file @
bd98f9fe
export
const
ROLES
=
{
export
const
ROLES
=
{
ADMIN
:
'ADMIN'
,
ADMIN
:
'ADMIN'
,
CRAWLER_USER
:
'CRAWLER_US
ER'
,
MANAGER
:
'MANAG
ER'
,
VIEWER
:
'VIEW
ER'
,
USER
:
'US
ER'
,
}
as
const
;
}
as
const
;
export
type
Role
=
keyof
typeof
ROLES
;
export
type
Role
=
keyof
typeof
ROLES
;
src/common/services/mail.service.ts
0 → 100644
View file @
bd98f9fe
import
nodemailer
from
'nodemailer'
;
import
{
mailConfig
}
from
'../../config/mail.config'
;
export
class
MailService
{
private
transporter
:
nodemailer
.
Transporter
;
constructor
()
{
this
.
transporter
=
nodemailer
.
createTransport
({
host
:
mailConfig
.
host
,
port
:
mailConfig
.
port
,
secure
:
mailConfig
.
secure
,
auth
:
{
user
:
mailConfig
.
auth
.
user
,
pass
:
mailConfig
.
auth
.
pass
,
},
});
}
async
sendVerificationEmail
(
email
:
string
,
token
:
string
,
fullName
?:
string
)
{
const
verificationUrl
=
`
${
mailConfig
.
verificationUrl
}
?token=
${
token
}
`
;
const
mailOptions
=
{
from
:
mailConfig
.
from
,
to
:
email
,
subject
:
'Xác thực tài khoản FinWise'
,
html
:
`
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px;">
<h2 style="color: #4CAF50; text-align: center;">Chào mừng đến với FinWise!</h2>
<p>Chào
${
fullName
||
'bạn'
}
,</p>
<p>Cảm ơn bạn đã đăng ký tài khoản tại FinWise - Sổ tay Chi tiêu & Báo cáo Tài chính.</p>
<p>Vui lòng xác nhận địa chỉ email của bạn để kích hoạt tài khoản bằng cách click vào nút bên dưới:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="
${
verificationUrl
}
" style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold; display: inline-block;">Kích hoạt tài khoản</a>
</div>
<p>Hoặc bạn có thể sao chép liên kết dưới đây và dán vào trình duyệt của bạn:</p>
<p style="word-break: break-all; color: #888888;">
${
verificationUrl
}
</p>
<p>Liên kết này sẽ có hiệu lực trong vòng 24 giờ.</p>
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 20px 0;" />
<p style="font-size: 12px; color: #888888; text-align: center;">Nếu bạn không thực hiện yêu cầu này, vui lòng bỏ qua email này.</p>
</div>
`
,
};
if
(
!
mailConfig
.
auth
.
user
||
!
mailConfig
.
auth
.
pass
)
{
console
.
warn
(
'-------- EMAIL VERIFICATION TOKEN (DEV MODE) --------'
);
console
.
warn
(
`To:
${
email
}
`
);
console
.
warn
(
`Link:
${
verificationUrl
}
`
);
console
.
warn
(
'----------------------------------------------------'
);
return
;
}
try
{
await
this
.
transporter
.
sendMail
(
mailOptions
);
}
catch
(
error
)
{
console
.
error
(
'Failed to send verification email:'
,
error
);
throw
error
;
}
}
async
sendPasswordResetEmail
(
email
:
string
,
token
:
string
,
fullName
?:
string
)
{
const
resetUrl
=
`
${
mailConfig
.
verificationUrl
.
replace
(
'verify-email'
,
'reset-password'
)}
?token=
${
token
}
`
;
const
mailOptions
=
{
from
:
mailConfig
.
from
,
to
:
email
,
subject
:
'Khôi phục mật khẩu tài khoản FinWise'
,
html
:
`
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px;">
<h2 style="color: #f44336; text-align: center;">Khôi phục mật khẩu tài khoản</h2>
<p>Chào
${
fullName
||
'bạn'
}
,</p>
<p>Chúng tôi nhận được yêu cầu khôi phục mật khẩu cho tài khoản FinWise liên kết với địa chỉ email này.</p>
<p>Vui lòng click vào nút bên dưới để tiến hành đặt lại mật khẩu mới:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="
${
resetUrl
}
" style="background-color: #f44336; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold; display: inline-block;">Đặt lại mật khẩu</a>
</div>
<p>Hoặc bạn có thể sao chép liên kết dưới đây và dán vào trình duyệt của bạn:</p>
<p style="word-break: break-all; color: #888888;">
${
resetUrl
}
</p>
<p>Liên kết này sẽ có hiệu lực trong vòng 1 giờ.</p>
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 20px 0;" />
<p style="font-size: 12px; color: #888888; text-align: center;">Nếu bạn không thực hiện yêu cầu này, vui lòng bỏ qua email này. Mật khẩu của bạn sẽ không thay đổi.</p>
</div>
`
,
};
if
(
!
mailConfig
.
auth
.
user
||
!
mailConfig
.
auth
.
pass
)
{
console
.
warn
(
'-------- EMAIL PASSWORD RESET TOKEN (DEV MODE) --------'
);
console
.
warn
(
`To:
${
email
}
`
);
console
.
warn
(
`Link:
${
resetUrl
}
`
);
console
.
warn
(
'-------------------------------------------------------'
);
return
;
}
try
{
await
this
.
transporter
.
sendMail
(
mailOptions
);
}
catch
(
error
)
{
console
.
error
(
'Failed to send password reset email:'
,
error
);
throw
error
;
}
}
}
src/config/mail.config.ts
0 → 100644
View file @
bd98f9fe
export
const
mailConfig
=
{
host
:
process
.
env
.
MAIL_HOST
||
'smtp.gmail.com'
,
port
:
parseInt
(
process
.
env
.
MAIL_PORT
||
'587'
,
10
),
secure
:
process
.
env
.
MAIL_SECURE
===
'true'
,
// true for 465, false for 587
auth
:
{
user
:
process
.
env
.
MAIL_USER
||
''
,
pass
:
process
.
env
.
MAIL_PASS
||
''
,
},
from
:
process
.
env
.
MAIL_FROM
||
'FinWise <noreply@gmail.com>'
,
verificationUrl
:
process
.
env
.
MAIL_VERIFICATION_URL
||
'http://localhost:7777/api/v1/auth/verify-email'
,
};
src/config/swagger.config.ts
View file @
bd98f9fe
This diff is collapsed.
Click to expand it.
src/middlewares/auth.middleware.ts
View file @
bd98f9fe
...
@@ -5,15 +5,20 @@ import { AppError } from '../common/errors/app-error';
...
@@ -5,15 +5,20 @@ import { AppError } from '../common/errors/app-error';
import
{
ERROR_CODE
}
from
'../common/errors/error-code'
;
import
{
ERROR_CODE
}
from
'../common/errors/error-code'
;
export
function
authMiddleware
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
):
void
{
export
function
authMiddleware
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
):
void
{
const
authHeader
=
req
.
headers
.
authorizatio
n
;
let
token
=
req
.
cookies
?.
accessToke
n
;
if
(
!
authHeader
||
!
authHeader
.
startsWith
(
'Bearer '
))
{
if
(
!
token
)
{
const
authHeader
=
req
.
headers
.
authorization
;
if
(
authHeader
&&
authHeader
.
startsWith
(
'Bearer '
))
{
token
=
authHeader
.
split
(
' '
)[
1
];
}
}
if
(
!
token
)
{
next
(
new
AppError
(
'Unauthorized'
,
401
,
ERROR_CODE
.
UNAUTHORIZED
));
next
(
new
AppError
(
'Unauthorized'
,
401
,
ERROR_CODE
.
UNAUTHORIZED
));
return
;
return
;
}
}
const
token
=
authHeader
.
split
(
' '
)[
1
];
try
{
try
{
const
payload
=
jwt
.
verify
(
token
,
jwtConfig
.
accessSecret
)
as
{
const
payload
=
jwt
.
verify
(
token
,
jwtConfig
.
accessSecret
)
as
{
id
:
string
;
id
:
string
;
...
...
src/modules/auth/auth.controller.ts
View file @
bd98f9fe
import
{
Request
,
Response
,
NextFunction
}
from
'express'
;
import
{
Request
,
Response
,
NextFunction
}
from
'express'
;
import
{
AuthService
}
from
'./auth.service'
;
import
{
AuthService
}
from
'./auth.service'
;
import
{
LoginDto
}
from
'./auth.dto'
;
import
{
LoginDto
,
RegisterDto
,
UpdateProfileDto
,
UpdatePasswordDto
,
ForgotPasswordDto
,
ResetPasswordDto
,
ResendVerificationDto
}
from
'./auth.dto'
;
import
{
AppError
}
from
'../../common/errors/app-error'
;
import
{
ERROR_CODE
}
from
'../../common/errors/error-code'
;
export
class
AuthController
{
export
class
AuthController
{
private
readonly
service
=
new
AuthService
();
private
readonly
service
=
new
AuthService
();
...
@@ -12,9 +14,25 @@ export class AuthController {
...
@@ -12,9 +14,25 @@ export class AuthController {
const
ipAddress
=
req
.
ip
;
const
ipAddress
=
req
.
ip
;
const
result
=
await
this
.
service
.
login
(
body
,
{
userAgent
,
ipAddress
});
const
result
=
await
this
.
service
.
login
(
body
,
{
userAgent
,
ipAddress
});
res
.
cookie
(
'accessToken'
,
result
.
accessToken
,
{
httpOnly
:
true
,
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
sameSite
:
'strict'
,
maxAge
:
24
*
60
*
60
*
1000
,
// 24 hours
});
res
.
cookie
(
'refreshToken'
,
result
.
refreshToken
,
{
httpOnly
:
true
,
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
sameSite
:
'strict'
,
maxAge
:
7
*
24
*
60
*
60
*
1000
,
// 7 days
});
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
data
:
result
,
data
:
{
user
:
result
.
user
,
},
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
next
(
error
);
next
(
error
);
...
@@ -36,14 +54,31 @@ export class AuthController {
...
@@ -36,14 +54,31 @@ export class AuthController {
refresh
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
refresh
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
try
{
const
{
refreshToken
}
=
req
.
body
;
const
refreshToken
=
req
.
cookies
?.
refreshToken
||
req
.
body
.
refreshToken
;
if
(
!
refreshToken
)
{
throw
new
AppError
(
'Refresh token is required'
,
400
,
ERROR_CODE
.
TOKEN_INVALID
);
}
const
userAgent
=
req
.
headers
[
'user-agent'
];
const
userAgent
=
req
.
headers
[
'user-agent'
];
const
ipAddress
=
req
.
ip
;
const
ipAddress
=
req
.
ip
;
const
result
=
await
this
.
service
.
refresh
(
refreshToken
,
{
userAgent
,
ipAddress
});
const
result
=
await
this
.
service
.
refresh
(
refreshToken
,
{
userAgent
,
ipAddress
});
res
.
cookie
(
'accessToken'
,
result
.
accessToken
,
{
httpOnly
:
true
,
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
sameSite
:
'strict'
,
maxAge
:
24
*
60
*
60
*
1000
,
});
res
.
cookie
(
'refreshToken'
,
result
.
refreshToken
,
{
httpOnly
:
true
,
secure
:
process
.
env
.
NODE_ENV
===
'production'
,
sameSite
:
'strict'
,
maxAge
:
7
*
24
*
60
*
60
*
1000
,
});
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
data
:
result
,
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
next
(
error
);
next
(
error
);
...
@@ -52,8 +87,13 @@ export class AuthController {
...
@@ -52,8 +87,13 @@ export class AuthController {
logout
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
logout
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
try
{
const
{
refreshToken
}
=
req
.
body
;
const
refreshToken
=
req
.
cookies
?.
refreshToken
||
req
.
body
.
refreshToken
;
await
this
.
service
.
logout
(
refreshToken
);
if
(
refreshToken
)
{
await
this
.
service
.
logout
(
refreshToken
);
}
res
.
clearCookie
(
'accessToken'
);
res
.
clearCookie
(
'refreshToken'
);
res
.
json
({
res
.
json
({
success
:
true
,
success
:
true
,
...
@@ -63,4 +103,102 @@ export class AuthController {
...
@@ -63,4 +103,102 @@ export class AuthController {
next
(
error
);
next
(
error
);
}
}
};
};
register
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
RegisterDto
;
await
this
.
service
.
register
(
body
);
res
.
status
(
201
).
json
({
success
:
true
,
message
:
'Registration successful. Please check your email to verify your account.'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
verifyEmail
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
{
token
}
=
req
.
query
;
await
this
.
service
.
verifyEmail
(
token
as
string
);
res
.
json
({
success
:
true
,
message
:
'Email verified successfully. You can now log in.'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
updateProfile
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
UpdateProfileDto
;
await
this
.
service
.
updateProfile
(
req
.
user
.
id
,
body
);
res
.
json
({
success
:
true
,
message
:
'Profile updated successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
updatePassword
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
UpdatePasswordDto
;
await
this
.
service
.
updatePassword
(
req
.
user
.
id
,
body
);
res
.
json
({
success
:
true
,
message
:
'Password updated successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
forgotPassword
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
ForgotPasswordDto
;
await
this
.
service
.
forgotPassword
(
body
);
res
.
json
({
success
:
true
,
message
:
'Password reset link sent to your email'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
resetPassword
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
ResetPasswordDto
;
await
this
.
service
.
resetPassword
(
body
);
res
.
json
({
success
:
true
,
message
:
'Password has been reset successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
resendVerification
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
body
=
req
.
body
as
ResendVerificationDto
;
await
this
.
service
.
resendVerification
(
body
);
res
.
json
({
success
:
true
,
message
:
'Verification email sent successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
}
}
src/modules/auth/auth.dto.ts
View file @
bd98f9fe
...
@@ -3,6 +3,12 @@ export interface LoginDto {
...
@@ -3,6 +3,12 @@ export interface LoginDto {
password
:
string
;
password
:
string
;
}
}
export
interface
RegisterDto
{
email
:
string
;
password
:
string
;
fullName
?:
string
;
}
export
interface
AuthTokensDto
{
export
interface
AuthTokensDto
{
accessToken
:
string
;
accessToken
:
string
;
refreshToken
:
string
;
refreshToken
:
string
;
...
@@ -10,7 +16,7 @@ export interface AuthTokensDto {
...
@@ -10,7 +16,7 @@ export interface AuthTokensDto {
export
interface
MeDto
{
export
interface
MeDto
{
id
:
string
;
id
:
string
;
email
:
string
;
email
:
string
|
null
;
fullName
:
string
|
null
;
fullName
:
string
|
null
;
role
:
string
;
role
:
string
;
isActive
:
boolean
;
isActive
:
boolean
;
...
@@ -22,8 +28,32 @@ export interface LoginResponseDto {
...
@@ -22,8 +28,32 @@ export interface LoginResponseDto {
refreshToken
:
string
;
refreshToken
:
string
;
user
:
{
user
:
{
id
:
string
;
id
:
string
;
email
:
string
;
email
:
string
|
null
;
fullName
:
string
|
null
;
fullName
:
string
|
null
;
role
:
string
;
role
:
string
;
};
};
}
}
export
interface
UpdateProfileDto
{
fullName
?:
string
;
avatarUrl
?:
string
;
phoneNumber
?:
string
;
}
export
interface
UpdatePasswordDto
{
oldPassword
?:
string
;
newPassword
:
string
;
}
export
interface
ForgotPasswordDto
{
email
:
string
;
}
export
interface
ResetPasswordDto
{
token
:
string
;
newPassword
:
string
;
}
export
interface
ResendVerificationDto
{
email
:
string
;
}
src/modules/auth/auth.repository.ts
View file @
bd98f9fe
...
@@ -2,15 +2,15 @@ import { prisma } from '../../database/prisma.client';
...
@@ -2,15 +2,15 @@ import { prisma } from '../../database/prisma.client';
export
class
AuthRepository
{
export
class
AuthRepository
{
findByEmail
(
email
:
string
)
{
findByEmail
(
email
:
string
)
{
return
prisma
.
user
.
find
Unique
({
return
prisma
.
user
.
find
First
({
where
:
{
email
},
where
:
{
email
,
deletedAt
:
null
},
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
}
}
findById
(
id
:
string
)
{
findById
(
id
:
string
)
{
return
prisma
.
user
.
find
Unique
({
return
prisma
.
user
.
find
First
({
where
:
{
id
},
where
:
{
id
,
deletedAt
:
null
},
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
}
}
...
@@ -38,4 +38,158 @@ export class AuthRepository {
...
@@ -38,4 +38,158 @@ export class AuthRepository {
where
:
{
token
},
where
:
{
token
},
});
});
}
}
async
findBySocial
(
provider
:
string
,
providerUserId
:
string
)
{
const
socialAccount
=
await
prisma
.
userSocial
.
findUnique
({
where
:
{
provider_providerUserId
:
{
provider
,
providerUserId
,
},
},
include
:
{
user
:
{
include
:
{
role
:
true
},
},
},
});
return
socialAccount
?.
user
||
null
;
}
async
createSocialUser
(
data
:
{
fullName
?:
string
;
avatarUrl
?:
string
;
roleId
:
string
;
provider
:
string
;
providerUserId
:
string
;
})
{
return
prisma
.
user
.
create
({
data
:
{
fullName
:
data
.
fullName
,
avatarUrl
:
data
.
avatarUrl
,
roleId
:
data
.
roleId
,
isActive
:
true
,
// Social users are active immediately
socialAccounts
:
{
create
:
{
provider
:
data
.
provider
,
providerUserId
:
data
.
providerUserId
,
},
},
},
include
:
{
role
:
true
},
});
}
async
createVerificationToken
(
userId
:
string
,
token
:
string
,
expiresAt
:
Date
)
{
return
prisma
.
verificationToken
.
create
({
data
:
{
userId
,
token
,
expiresAt
,
},
});
}
async
findVerificationToken
(
token
:
string
)
{
return
prisma
.
verificationToken
.
findUnique
({
where
:
{
token
},
include
:
{
user
:
true
},
});
}
async
activateUser
(
userId
:
string
)
{
return
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
isActive
:
true
},
});
}
async
deleteVerificationToken
(
tokenId
:
string
)
{
return
prisma
.
verificationToken
.
delete
({
where
:
{
id
:
tokenId
},
});
}
async
findRoleByName
(
name
:
string
)
{
return
prisma
.
role
.
findUnique
({
where
:
{
name
},
});
}
async
createUser
(
data
:
{
email
:
string
;
passwordHash
:
string
;
fullName
?:
string
;
roleId
:
string
;
isActive
:
boolean
;
})
{
return
prisma
.
user
.
create
({
data
:
{
email
:
data
.
email
,
password
:
data
.
passwordHash
,
fullName
:
data
.
fullName
,
roleId
:
data
.
roleId
,
isActive
:
data
.
isActive
,
},
});
}
async
findByPhone
(
phoneNumber
:
string
)
{
return
prisma
.
user
.
findFirst
({
where
:
{
phoneNumber
,
deletedAt
:
null
},
});
}
async
updateProfile
(
userId
:
string
,
data
:
{
fullName
?:
string
;
avatarUrl
?:
string
;
phoneNumber
?:
string
})
{
return
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
,
});
}
async
updatePassword
(
userId
:
string
,
passwordHash
:
string
)
{
return
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
password
:
passwordHash
},
});
}
async
softDelete
(
userId
:
string
,
adminId
:
string
)
{
return
prisma
.
$transaction
([
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
deletedAt
:
new
Date
(),
deletedBy
:
adminId
,
isActive
:
false
,
},
}),
prisma
.
refreshToken
.
deleteMany
({
where
:
{
userId
},
}),
]);
}
async
createPasswordResetToken
(
userId
:
string
,
token
:
string
,
expiresAt
:
Date
)
{
return
prisma
.
passwordResetToken
.
create
({
data
:
{
userId
,
token
,
expiresAt
,
},
});
}
async
findPasswordResetToken
(
token
:
string
)
{
return
prisma
.
passwordResetToken
.
findUnique
({
where
:
{
token
},
include
:
{
user
:
true
},
});
}
async
deletePasswordResetToken
(
tokenId
:
string
)
{
return
prisma
.
passwordResetToken
.
delete
({
where
:
{
id
:
tokenId
},
});
}
}
}
src/modules/auth/auth.route.ts
View file @
bd98f9fe
...
@@ -2,14 +2,21 @@ import { Router } from 'express';
...
@@ -2,14 +2,21 @@ import { Router } from 'express';
import
{
AuthController
}
from
'./auth.controller'
;
import
{
AuthController
}
from
'./auth.controller'
;
import
{
authMiddleware
}
from
'../../middlewares/auth.middleware'
;
import
{
authMiddleware
}
from
'../../middlewares/auth.middleware'
;
import
{
validate
}
from
'../../middlewares/validate.middleware'
;
import
{
validate
}
from
'../../middlewares/validate.middleware'
;
import
{
loginSchema
,
refreshSchema
,
logoutSchema
}
from
'./auth.validation'
;
import
{
loginSchema
,
refreshSchema
,
logoutSchema
,
registerSchema
,
verifyEmailSchema
,
updateProfileSchema
,
updatePasswordSchema
,
forgotPasswordSchema
,
resetPasswordSchema
,
resendVerificationSchema
}
from
'./auth.validation'
;
const
router
=
Router
();
const
router
=
Router
();
const
controller
=
new
AuthController
();
const
controller
=
new
AuthController
();
router
.
post
(
'/register'
,
validate
(
registerSchema
),
controller
.
register
);
router
.
get
(
'/verify-email'
,
validate
(
verifyEmailSchema
,
'query'
),
controller
.
verifyEmail
);
router
.
post
(
'/login'
,
validate
(
loginSchema
),
controller
.
login
);
router
.
post
(
'/login'
,
validate
(
loginSchema
),
controller
.
login
);
router
.
get
(
'/me'
,
authMiddleware
,
controller
.
me
);
router
.
get
(
'/me'
,
authMiddleware
,
controller
.
me
);
router
.
post
(
'/refresh'
,
validate
(
refreshSchema
),
controller
.
refresh
);
router
.
post
(
'/refresh'
,
validate
(
refreshSchema
),
controller
.
refresh
);
router
.
post
(
'/logout'
,
validate
(
logoutSchema
),
controller
.
logout
);
router
.
post
(
'/logout'
,
validate
(
logoutSchema
),
controller
.
logout
);
router
.
put
(
'/profile'
,
authMiddleware
,
validate
(
updateProfileSchema
),
controller
.
updateProfile
);
router
.
put
(
'/password'
,
authMiddleware
,
validate
(
updatePasswordSchema
),
controller
.
updatePassword
);
router
.
post
(
'/forgot-password'
,
validate
(
forgotPasswordSchema
),
controller
.
forgotPassword
);
router
.
post
(
'/reset-password'
,
validate
(
resetPasswordSchema
),
controller
.
resetPassword
);
router
.
post
(
'/resend-verification'
,
validate
(
resendVerificationSchema
),
controller
.
resendVerification
);
export
default
router
;
export
default
router
;
src/modules/auth/auth.service.ts
View file @
bd98f9fe
import
crypto
from
'crypto'
;
import
bcrypt
from
'bcryptjs'
;
import
bcrypt
from
'bcryptjs'
;
import
jwt
from
'jsonwebtoken'
;
import
jwt
from
'jsonwebtoken'
;
import
{
AuthRepository
}
from
'./auth.repository'
;
import
{
AuthRepository
}
from
'./auth.repository'
;
import
{
AppError
}
from
'../../common/errors/app-error'
;
import
{
AppError
}
from
'../../common/errors/app-error'
;
import
{
ERROR_CODE
}
from
'../../common/errors/error-code'
;
import
{
ERROR_CODE
}
from
'../../common/errors/error-code'
;
import
{
jwtConfig
}
from
'../../config/jwt.config'
;
import
{
jwtConfig
}
from
'../../config/jwt.config'
;
import
{
LoginDto
,
LoginResponseDto
,
AuthTokensDto
,
MeDto
}
from
'./auth.dto'
;
import
{
LoginDto
,
LoginResponseDto
,
AuthTokensDto
,
MeDto
,
RegisterDto
,
UpdateProfileDto
,
UpdatePasswordDto
,
ForgotPasswordDto
,
ResetPasswordDto
,
ResendVerificationDto
}
from
'./auth.dto'
;
import
{
MailService
}
from
'../../common/services/mail.service'
;
import
{
ROLES
}
from
'../../common/constants/role.constant'
;
export
class
AuthService
{
export
class
AuthService
{
private
readonly
repository
=
new
AuthRepository
();
private
readonly
repository
=
new
AuthRepository
();
private
readonly
mailService
=
new
MailService
();
async
login
(
data
:
LoginDto
,
metadata
?:
{
userAgent
?:
string
;
ipAddress
?:
string
}):
Promise
<
LoginResponseDto
>
{
async
login
(
data
:
LoginDto
,
metadata
?:
{
userAgent
?:
string
;
ipAddress
?:
string
}):
Promise
<
LoginResponseDto
>
{
const
{
email
,
password
}
=
data
;
const
{
email
,
password
}
=
data
;
...
@@ -21,6 +25,10 @@ export class AuthService {
...
@@ -21,6 +25,10 @@ export class AuthService {
throw
new
AppError
(
'Account is inactive'
,
403
,
ERROR_CODE
.
USER_INACTIVE
);
throw
new
AppError
(
'Account is inactive'
,
403
,
ERROR_CODE
.
USER_INACTIVE
);
}
}
if
(
!
user
.
password
)
{
throw
new
AppError
(
'Invalid credentials'
,
401
,
ERROR_CODE
.
INVALID_CREDENTIALS
);
}
const
isPasswordValid
=
await
bcrypt
.
compare
(
password
,
user
.
password
);
const
isPasswordValid
=
await
bcrypt
.
compare
(
password
,
user
.
password
);
if
(
!
isPasswordValid
)
{
if
(
!
isPasswordValid
)
{
...
@@ -122,4 +130,147 @@ export class AuthService {
...
@@ -122,4 +130,147 @@ export class AuthService {
createdAt
:
user
.
createdAt
,
createdAt
:
user
.
createdAt
,
};
};
}
}
async
register
(
data
:
RegisterDto
):
Promise
<
void
>
{
const
{
email
,
password
,
fullName
}
=
data
;
const
existing
=
await
this
.
repository
.
findByEmail
(
email
);
if
(
existing
)
{
throw
new
AppError
(
'Email already exists'
,
400
,
ERROR_CODE
.
DUPLICATE_ENTRY
);
}
const
passwordHash
=
await
bcrypt
.
hash
(
password
,
10
);
const
defaultRole
=
await
this
.
repository
.
findRoleByName
(
ROLES
.
USER
);
if
(
!
defaultRole
)
{
throw
new
AppError
(
'Default role not found'
,
500
,
ERROR_CODE
.
NOT_FOUND
);
}
const
user
=
await
this
.
repository
.
createUser
({
email
,
passwordHash
,
fullName
,
roleId
:
defaultRole
.
id
,
isActive
:
false
,
});
const
token
=
crypto
.
randomUUID
();
const
expiresAt
=
new
Date
();
expiresAt
.
setHours
(
expiresAt
.
getHours
()
+
24
);
await
this
.
repository
.
createVerificationToken
(
user
.
id
,
token
,
expiresAt
);
await
this
.
mailService
.
sendVerificationEmail
(
email
,
token
,
fullName
||
undefined
);
}
async
verifyEmail
(
token
:
string
):
Promise
<
void
>
{
const
verificationToken
=
await
this
.
repository
.
findVerificationToken
(
token
);
if
(
!
verificationToken
)
{
throw
new
AppError
(
'Invalid verification token'
,
400
,
ERROR_CODE
.
TOKEN_INVALID
);
}
if
(
verificationToken
.
expiresAt
<
new
Date
())
{
await
this
.
repository
.
deleteVerificationToken
(
verificationToken
.
id
);
throw
new
AppError
(
'Verification token has expired'
,
400
,
ERROR_CODE
.
TOKEN_EXPIRED
);
}
await
this
.
repository
.
activateUser
(
verificationToken
.
userId
);
await
this
.
repository
.
deleteVerificationToken
(
verificationToken
.
id
);
}
async
updateProfile
(
userId
:
string
,
data
:
UpdateProfileDto
):
Promise
<
void
>
{
const
user
=
await
this
.
repository
.
findById
(
userId
);
if
(
!
user
)
{
throw
new
AppError
(
'User not found'
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
if
(
data
.
phoneNumber
)
{
const
existingPhone
=
await
this
.
repository
.
findByPhone
(
data
.
phoneNumber
);
if
(
existingPhone
&&
existingPhone
.
id
!==
userId
)
{
throw
new
AppError
(
'Phone number already exists'
,
400
,
ERROR_CODE
.
DUPLICATE_ENTRY
);
}
}
await
this
.
repository
.
updateProfile
(
userId
,
data
);
}
async
updatePassword
(
userId
:
string
,
data
:
UpdatePasswordDto
):
Promise
<
void
>
{
const
user
=
await
this
.
repository
.
findById
(
userId
);
if
(
!
user
)
{
throw
new
AppError
(
'User not found'
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
if
(
user
.
password
)
{
if
(
!
data
.
oldPassword
)
{
throw
new
AppError
(
'Old password is required'
,
400
,
ERROR_CODE
.
INVALID_CREDENTIALS
);
}
const
isPasswordValid
=
await
bcrypt
.
compare
(
data
.
oldPassword
,
user
.
password
);
if
(
!
isPasswordValid
)
{
throw
new
AppError
(
'Invalid old password'
,
401
,
ERROR_CODE
.
INVALID_CREDENTIALS
);
}
}
const
passwordHash
=
await
bcrypt
.
hash
(
data
.
newPassword
,
10
);
await
this
.
repository
.
updatePassword
(
userId
,
passwordHash
);
}
async
softDeleteUser
(
userId
:
string
,
adminId
:
string
):
Promise
<
void
>
{
const
user
=
await
this
.
repository
.
findById
(
userId
);
if
(
!
user
)
{
throw
new
AppError
(
'User not found'
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
await
this
.
repository
.
softDelete
(
userId
,
adminId
);
}
async
forgotPassword
(
data
:
ForgotPasswordDto
):
Promise
<
void
>
{
const
user
=
await
this
.
repository
.
findByEmail
(
data
.
email
);
if
(
!
user
)
{
throw
new
AppError
(
'User not found'
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
const
token
=
crypto
.
randomUUID
();
const
expiresAt
=
new
Date
();
expiresAt
.
setHours
(
expiresAt
.
getHours
()
+
1
);
await
this
.
repository
.
createPasswordResetToken
(
user
.
id
,
token
,
expiresAt
);
await
this
.
mailService
.
sendPasswordResetEmail
(
user
.
email
!
,
token
,
user
.
fullName
||
undefined
);
}
async
resetPassword
(
data
:
ResetPasswordDto
):
Promise
<
void
>
{
const
resetToken
=
await
this
.
repository
.
findPasswordResetToken
(
data
.
token
);
if
(
!
resetToken
)
{
throw
new
AppError
(
'Invalid or expired reset token'
,
400
,
ERROR_CODE
.
TOKEN_INVALID
);
}
if
(
resetToken
.
expiresAt
<
new
Date
())
{
await
this
.
repository
.
deletePasswordResetToken
(
resetToken
.
id
);
throw
new
AppError
(
'Reset token has expired'
,
400
,
ERROR_CODE
.
TOKEN_EXPIRED
);
}
const
passwordHash
=
await
bcrypt
.
hash
(
data
.
newPassword
,
10
);
await
this
.
repository
.
updatePassword
(
resetToken
.
userId
,
passwordHash
);
await
this
.
repository
.
deletePasswordResetToken
(
resetToken
.
id
);
}
async
resendVerification
(
data
:
ResendVerificationDto
):
Promise
<
void
>
{
const
user
=
await
this
.
repository
.
findByEmail
(
data
.
email
);
if
(
!
user
)
{
throw
new
AppError
(
'User not found'
,
404
,
ERROR_CODE
.
NOT_FOUND
);
}
if
(
user
.
isActive
)
{
throw
new
AppError
(
'User is already verified'
,
400
,
ERROR_CODE
.
VALIDATION_ERROR
);
}
const
token
=
crypto
.
randomUUID
();
const
expiresAt
=
new
Date
();
expiresAt
.
setHours
(
expiresAt
.
getHours
()
+
24
);
await
this
.
repository
.
createVerificationToken
(
user
.
id
,
token
,
expiresAt
);
await
this
.
mailService
.
sendVerificationEmail
(
user
.
email
!
,
token
,
user
.
fullName
||
undefined
);
}
}
}
src/modules/auth/auth.validation.ts
View file @
bd98f9fe
...
@@ -6,9 +6,67 @@ export const loginSchema = z.object({
...
@@ -6,9 +6,67 @@ export const loginSchema = z.object({
});
});
export
const
refreshSchema
=
z
.
object
({
export
const
refreshSchema
=
z
.
object
({
refreshToken
:
z
.
string
().
min
(
1
,
'Refresh token is required'
),
refreshToken
:
z
.
string
().
optional
(
),
});
});
export
const
logoutSchema
=
z
.
object
({
export
const
logoutSchema
=
z
.
object
({
refreshToken
:
z
.
string
().
min
(
1
,
'Refresh token is required'
),
refreshToken
:
z
.
string
().
optional
(),
});
export
const
registerSchema
=
z
.
object
({
email
:
z
.
string
()
.
min
(
1
,
'Email is required'
)
.
email
(
'Invalid email format'
)
.
refine
((
val
)
=>
val
.
endsWith
(
'@gmail.com'
),
{
message
:
'Only @gmail.com emails are allowed'
,
}),
password
:
z
.
string
()
.
min
(
8
,
'Password must be at least 8 characters'
)
.
regex
(
/
[
a-z
]
/
,
'Password must contain at least one lowercase letter'
)
.
regex
(
/
[
A-Z
]
/
,
'Password must contain at least one uppercase letter'
)
.
regex
(
/
[
0-9
]
/
,
'Password must contain at least one number'
)
.
regex
(
/
[^
a-zA-Z0-9
]
/
,
'Password must contain at least one special character'
),
fullName
:
z
.
string
().
optional
(),
});
export
const
verifyEmailSchema
=
z
.
object
({
token
:
z
.
string
().
min
(
1
,
'Verification token is required'
),
});
export
const
updateProfileSchema
=
z
.
object
({
fullName
:
z
.
string
().
min
(
1
,
'Full name cannot be empty'
).
optional
(),
avatarUrl
:
z
.
string
().
url
(
'Invalid avatar URL format'
).
optional
().
or
(
z
.
literal
(
''
)),
phoneNumber
:
z
.
string
().
regex
(
/^
[
0-9
]{10,11}
$/
,
'Invalid phone number format (must be 10-11 digits)'
).
optional
(),
});
export
const
updatePasswordSchema
=
z
.
object
({
oldPassword
:
z
.
string
().
optional
(),
newPassword
:
z
.
string
()
.
min
(
8
,
'Password must be at least 8 characters'
)
.
regex
(
/
[
a-z
]
/
,
'Password must contain at least one lowercase letter'
)
.
regex
(
/
[
A-Z
]
/
,
'Password must contain at least one uppercase letter'
)
.
regex
(
/
[
0-9
]
/
,
'Password must contain at least one number'
)
.
regex
(
/
[^
a-zA-Z0-9
]
/
,
'Password must contain at least one special character'
),
});
export
const
forgotPasswordSchema
=
z
.
object
({
email
:
z
.
string
().
min
(
1
,
'Email is required'
).
email
(
'Invalid email format'
),
});
export
const
resetPasswordSchema
=
z
.
object
({
token
:
z
.
string
().
min
(
1
,
'Reset token is required'
),
newPassword
:
z
.
string
()
.
min
(
8
,
'Password must be at least 8 characters'
)
.
regex
(
/
[
a-z
]
/
,
'Password must contain at least one lowercase letter'
)
.
regex
(
/
[
A-Z
]
/
,
'Password must contain at least one uppercase letter'
)
.
regex
(
/
[
0-9
]
/
,
'Password must contain at least one number'
)
.
regex
(
/
[^
a-zA-Z0-9
]
/
,
'Password must contain at least one special character'
),
});
export
const
resendVerificationSchema
=
z
.
object
({
email
:
z
.
string
().
min
(
1
,
'Email is required'
).
email
(
'Invalid email format'
),
});
});
src/modules/users/user.controller.ts
View file @
bd98f9fe
...
@@ -56,4 +56,19 @@ export class UserController {
...
@@ -56,4 +56,19 @@ export class UserController {
next
(
error
);
next
(
error
);
}
}
};
};
softDelete
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
{
id
}
=
req
.
params
;
const
adminId
=
req
.
user
.
id
;
await
this
.
service
.
softDelete
(
id
,
adminId
);
res
.
json
({
success
:
true
,
message
:
'User soft-deleted successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
}
}
src/modules/users/user.repository.ts
View file @
bd98f9fe
...
@@ -16,6 +16,7 @@ export class UserRepository {
...
@@ -16,6 +16,7 @@ export class UserRepository {
}
=
query
;
}
=
query
;
const
where
:
Prisma
.
UserWhereInput
=
{
const
where
:
Prisma
.
UserWhereInput
=
{
deletedAt
:
null
,
...(
email
?
{
email
:
{
contains
:
email
,
mode
:
'insensitive'
}
}
:
{}),
...(
email
?
{
email
:
{
contains
:
email
,
mode
:
'insensitive'
}
}
:
{}),
...(
fullName
?
{
fullName
:
{
contains
:
fullName
,
mode
:
'insensitive'
}
}
:
{}),
...(
fullName
?
{
fullName
:
{
contains
:
fullName
,
mode
:
'insensitive'
}
}
:
{}),
...(
isActive
!==
undefined
?
{
isActive
}
:
{}),
...(
isActive
!==
undefined
?
{
isActive
}
:
{}),
...
@@ -42,29 +43,37 @@ export class UserRepository {
...
@@ -42,29 +43,37 @@ export class UserRepository {
}
}
findById
(
id
:
string
)
{
findById
(
id
:
string
)
{
return
prisma
.
user
.
find
Unique
({
return
prisma
.
user
.
find
First
({
where
:
{
id
},
where
:
{
id
,
deletedAt
:
null
},
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
}
}
findByEmail
(
email
:
string
)
{
findByEmail
(
email
:
string
)
{
return
prisma
.
user
.
find
Unique
({
return
prisma
.
user
.
find
First
({
where
:
{
email
},
where
:
{
email
,
deletedAt
:
null
},
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
}
}
create
(
data
:
{
create
(
data
:
{
email
:
string
;
email
?:
string
;
passwordHash
:
string
;
passwordHash
?:
string
;
fullName
?:
string
;
avatarUrl
?:
string
;
phoneNumber
?:
string
;
roleId
:
string
;
roleId
:
string
;
isActive
?:
boolean
;
})
{
})
{
return
prisma
.
user
.
create
({
return
prisma
.
user
.
create
({
data
:
{
data
:
{
email
:
data
.
email
,
email
:
data
.
email
,
password
:
data
.
passwordHash
,
password
:
data
.
passwordHash
,
fullName
:
data
.
fullName
,
avatarUrl
:
data
.
avatarUrl
,
phoneNumber
:
data
.
phoneNumber
,
roleId
:
data
.
roleId
,
roleId
:
data
.
roleId
,
isActive
:
data
.
isActive
,
},
},
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
...
@@ -77,4 +86,20 @@ export class UserRepository {
...
@@ -77,4 +86,20 @@ export class UserRepository {
include
:
{
role
:
true
},
include
:
{
role
:
true
},
});
});
}
}
async
softDelete
(
id
:
string
,
adminId
:
string
)
{
return
prisma
.
$transaction
([
prisma
.
user
.
update
({
where
:
{
id
},
data
:
{
deletedAt
:
new
Date
(),
deletedBy
:
adminId
,
isActive
:
false
,
},
}),
prisma
.
refreshToken
.
deleteMany
({
where
:
{
userId
:
id
},
}),
]);
}
}
}
src/modules/users/user.route.ts
View file @
bd98f9fe
...
@@ -4,14 +4,16 @@ import { authMiddleware } from '../../middlewares/auth.middleware';
...
@@ -4,14 +4,16 @@ import { authMiddleware } from '../../middlewares/auth.middleware';
import
{
requireRole
}
from
'../../middlewares/role.middleware'
;
import
{
requireRole
}
from
'../../middlewares/role.middleware'
;
import
{
validate
}
from
'../../middlewares/validate.middleware'
;
import
{
validate
}
from
'../../middlewares/validate.middleware'
;
import
{
createUserSchema
,
findAllUserSchema
,
updateUserSchema
}
from
'./user.validation'
;
import
{
createUserSchema
,
findAllUserSchema
,
updateUserSchema
}
from
'./user.validation'
;
import
{
ROLES
}
from
'../../common/constants/role.constant'
;
const
router
=
Router
();
const
router
=
Router
();
const
controller
=
new
UserController
();
const
controller
=
new
UserController
();
// GET /users?email=...&fullName=...&roleName=...&isActive=...&sortBy=...&order=...&page=...&limit=...
// GET /users?email=...&fullName=...&roleName=...&isActive=...&sortBy=...&order=...&page=...&limit=...
router
.
get
(
'/'
,
authMiddleware
,
requireRole
(
'ADMIN'
),
validate
(
findAllUserSchema
,
'query'
),
controller
.
findAll
);
router
.
get
(
'/'
,
authMiddleware
,
requireRole
(
ROLES
.
ADMIN
),
validate
(
findAllUserSchema
,
'query'
),
controller
.
findAll
);
router
.
get
(
'/:id'
,
authMiddleware
,
requireRole
(
'ADMIN'
),
controller
.
findById
);
router
.
get
(
'/:id'
,
authMiddleware
,
requireRole
(
ROLES
.
ADMIN
),
controller
.
findById
);
router
.
post
(
'/'
,
authMiddleware
,
requireRole
(
'ADMIN'
),
validate
(
createUserSchema
),
controller
.
create
);
router
.
post
(
'/'
,
authMiddleware
,
requireRole
(
ROLES
.
ADMIN
),
validate
(
createUserSchema
),
controller
.
create
);
router
.
put
(
'/:id'
,
authMiddleware
,
requireRole
(
'ADMIN'
),
validate
(
updateUserSchema
),
controller
.
update
);
router
.
put
(
'/:id'
,
authMiddleware
,
requireRole
(
ROLES
.
ADMIN
),
validate
(
updateUserSchema
),
controller
.
update
);
router
.
delete
(
'/:id'
,
authMiddleware
,
requireRole
(
ROLES
.
ADMIN
),
controller
.
softDelete
);
export
default
router
;
export
default
router
;
src/modules/users/user.service.ts
View file @
bd98f9fe
...
@@ -34,6 +34,7 @@ export class UserService {
...
@@ -34,6 +34,7 @@ export class UserService {
email
:
data
.
email
,
email
:
data
.
email
,
passwordHash
,
passwordHash
,
roleId
:
data
.
roleId
,
roleId
:
data
.
roleId
,
isActive
:
true
,
// Admin-created users are active by default
});
});
}
}
...
@@ -45,4 +46,9 @@ export class UserService {
...
@@ -45,4 +46,9 @@ export class UserService {
roleId
:
data
.
roleId
,
roleId
:
data
.
roleId
,
});
});
}
}
async
softDelete
(
id
:
string
,
adminId
:
string
)
{
await
this
.
findById
(
id
);
return
this
.
repository
.
softDelete
(
id
,
adminId
);
}
}
}
src/modules/users/user.validation.ts
View file @
bd98f9fe
import
{
z
}
from
'zod'
;
import
{
z
}
from
'zod'
;
import
{
ROLES
}
from
'../../common/constants/role.constant'
;
export
const
findAllUserSchema
=
z
.
object
({
export
const
findAllUserSchema
=
z
.
object
({
email
:
z
.
string
().
optional
(),
email
:
z
.
string
().
optional
(),
fullName
:
z
.
string
().
optional
(),
fullName
:
z
.
string
().
optional
(),
roleName
:
z
.
enum
([
'ADMIN'
,
'LEADER'
,
'INTERN'
]).
optional
(),
roleName
:
z
.
enum
([
ROLES
.
ADMIN
,
ROLES
.
MANAGER
,
ROLES
.
USER
]).
optional
(),
isActive
:
z
isActive
:
z
.
enum
([
'true'
,
'false'
])
.
enum
([
'true'
,
'false'
])
.
transform
((
v
)
=>
v
===
'true'
)
.
transform
((
v
)
=>
v
===
'true'
)
...
@@ -17,7 +18,13 @@ export const findAllUserSchema = z.object({
...
@@ -17,7 +18,13 @@ export const findAllUserSchema = z.object({
});
});
export
const
createUserSchema
=
z
.
object
({
export
const
createUserSchema
=
z
.
object
({
email
:
z
.
string
().
min
(
1
,
'Email is required'
).
email
(
'Invalid email format'
),
email
:
z
.
string
()
.
min
(
1
,
'Email is required'
)
.
email
(
'Invalid email format'
)
.
refine
((
val
)
=>
val
.
endsWith
(
'@gmail.com'
),
{
message
:
'Only @gmail.com emails are allowed'
,
}),
password
:
z
password
:
z
.
string
()
.
string
()
.
min
(
8
,
'Password must be at least 8 characters'
)
.
min
(
8
,
'Password must be at least 8 characters'
)
...
...
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