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
8d3bead5
Commit
8d3bead5
authored
Jul 01, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: refresh token & logout
parent
639eb99b
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
154 additions
and
4 deletions
+154
-4
migration.sql
...igrations/20260701120955_add_refresh_tokens/migration.sql
+16
-0
schema.prisma
prisma/schema.prisma
+14
-0
app-error.ts
src/common/errors/app-error.ts
+10
-3
auth.controller.ts
src/modules/auth/auth.controller.ts
+28
-0
auth.repository.ts
src/modules/auth/auth.repository.ts
+22
-0
auth.route.ts
src/modules/auth/auth.route.ts
+3
-1
auth.service.ts
src/modules/auth/auth.service.ts
+53
-0
auth.validation.ts
src/modules/auth/auth.validation.ts
+8
-0
No files found.
prisma/migrations/20260701120955_add_refresh_tokens/migration.sql
0 → 100644
View file @
8d3bead5
-- CreateTable
CREATE
TABLE
"refresh_tokens"
(
"id"
UUID
NOT
NULL
,
"token"
TEXT
NOT
NULL
,
"user_id"
UUID
NOT
NULL
,
"expires_at"
TIMESTAMP
(
3
)
NOT
NULL
,
"created_at"
TIMESTAMP
(
3
)
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
,
CONSTRAINT
"refresh_tokens_pkey"
PRIMARY
KEY
(
"id"
)
);
-- CreateIndex
CREATE
UNIQUE
INDEX
"refresh_tokens_token_key"
ON
"refresh_tokens"
(
"token"
);
-- AddForeignKey
ALTER
TABLE
"refresh_tokens"
ADD
CONSTRAINT
"refresh_tokens_user_id_fkey"
FOREIGN
KEY
(
"user_id"
)
REFERENCES
"users"
(
"id"
)
ON
DELETE
CASCADE
ON
UPDATE
CASCADE
;
prisma/schema.prisma
View file @
8d3bead5
...
...
@@ -45,6 +45,7 @@ model User {
reviewedSubmissions
TaskSubmission
[]
@
relation
(
"ReviewedSubmissions"
)
leaderEvaluations
WeeklyEvaluation
[]
@
relation
(
"LeaderEvaluations"
)
notifications
Notification
[]
refreshTokens
RefreshToken
[]
@@
map
(
"users"
)
}
...
...
@@ -277,3 +278,16 @@ model NotificationLog {
@@
map
(
"notification_logs"
)
}
//
13.
RefreshToken
model
RefreshToken
{
id
String
@
id
@
default
(
uuid
())
@
db
.
Uuid
token
String
@
unique
userId
String
@
db
.
Uuid
@
map
(
"user_id"
)
expiresAt
DateTime
@
map
(
"expires_at"
)
createdAt
DateTime
@
default
(
now
())
@
map
(
"created_at"
)
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
@@
map
(
"refresh_tokens"
)
}
src/common/errors/app-error.ts
View file @
8d3bead5
import
{
ErrorCode
}
from
'./error-code'
;
export
class
AppError
extends
Error
{
public
readonly
statusCode
:
number
;
public
readonly
code
?:
string
;
public
readonly
code
?:
ErrorCode
;
public
readonly
isOperational
:
boolean
;
constructor
(
message
:
string
,
statusCode
:
number
=
500
,
code
?:
string
)
{
constructor
(
message
:
string
,
statusCode
:
number
=
500
,
code
?:
ErrorCode
)
{
super
(
message
);
this
.
statusCode
=
statusCode
;
this
.
code
=
code
;
this
.
isOperational
=
true
;
Object
.
setPrototypeOf
(
this
,
AppError
.
prototype
);
Object
.
setPrototypeOf
(
this
,
new
.
target
.
prototype
);
if
(
Error
.
captureStackTrace
)
{
Error
.
captureStackTrace
(
this
,
this
.
constructor
);
}
}
}
src/modules/auth/auth.controller.ts
View file @
8d3bead5
...
...
@@ -30,4 +30,32 @@ export class AuthController {
next
(
error
);
}
};
refresh
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
{
refreshToken
}
=
req
.
body
;
const
result
=
await
this
.
service
.
refresh
(
refreshToken
);
res
.
json
({
success
:
true
,
data
:
result
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
logout
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
)
=>
{
try
{
const
{
refreshToken
}
=
req
.
body
;
await
this
.
service
.
logout
(
refreshToken
);
res
.
json
({
success
:
true
,
message
:
'Logged out successfully'
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
}
src/modules/auth/auth.repository.ts
View file @
8d3bead5
...
...
@@ -14,4 +14,26 @@ export class AuthRepository {
include
:
{
role
:
true
},
});
}
async
saveRefreshToken
(
userId
:
string
,
token
:
string
,
expiresAt
:
Date
)
{
return
prisma
.
refreshToken
.
create
({
data
:
{
userId
,
token
,
expiresAt
,
},
});
}
async
findRefreshToken
(
token
:
string
)
{
return
prisma
.
refreshToken
.
findUnique
({
where
:
{
token
},
});
}
async
deleteRefreshToken
(
token
:
string
)
{
return
prisma
.
refreshToken
.
deleteMany
({
where
:
{
token
},
});
}
}
src/modules/auth/auth.route.ts
View file @
8d3bead5
...
...
@@ -2,12 +2,14 @@ import { Router } from 'express';
import
{
AuthController
}
from
'./auth.controller'
;
import
{
authMiddleware
}
from
'../../middlewares/auth.middleware'
;
import
{
validate
}
from
'../../middlewares/validate.middleware'
;
import
{
loginSchema
}
from
'./auth.validation'
;
import
{
loginSchema
,
refreshSchema
,
logoutSchema
}
from
'./auth.validation'
;
const
router
=
Router
();
const
controller
=
new
AuthController
();
router
.
post
(
'/login'
,
validate
(
loginSchema
),
controller
.
login
);
router
.
get
(
'/me'
,
authMiddleware
,
controller
.
me
);
router
.
post
(
'/refresh'
,
validate
(
refreshSchema
),
controller
.
refresh
);
router
.
post
(
'/logout'
,
validate
(
logoutSchema
),
controller
.
logout
);
export
default
router
;
src/modules/auth/auth.service.ts
View file @
8d3bead5
...
...
@@ -35,6 +35,10 @@ export class AuthService {
expiresIn
:
jwtConfig
.
refreshExpiresIn
as
any
,
});
const
decoded
=
jwt
.
decode
(
refreshToken
)
as
{
exp
:
number
};
const
expiresAt
=
new
Date
(
decoded
.
exp
*
1000
);
await
this
.
repository
.
saveRefreshToken
(
user
.
id
,
refreshToken
,
expiresAt
);
return
{
accessToken
,
refreshToken
,
...
...
@@ -46,6 +50,55 @@ export class AuthService {
};
}
async
refresh
(
token
:
string
)
{
let
payload
:
any
;
try
{
payload
=
jwt
.
verify
(
token
,
jwtConfig
.
refreshSecret
);
}
catch
(
error
)
{
throw
new
AppError
(
'Invalid refresh token'
,
401
,
ERROR_CODE
.
TOKEN_INVALID
);
}
const
savedToken
=
await
this
.
repository
.
findRefreshToken
(
token
);
if
(
!
savedToken
)
{
throw
new
AppError
(
'Invalid or expired refresh token'
,
401
,
ERROR_CODE
.
TOKEN_INVALID
);
}
if
(
savedToken
.
expiresAt
<
new
Date
())
{
await
this
.
repository
.
deleteRefreshToken
(
token
);
throw
new
AppError
(
'Refresh token expired'
,
401
,
ERROR_CODE
.
TOKEN_EXPIRED
);
}
const
user
=
await
this
.
repository
.
findById
(
payload
.
id
);
if
(
!
user
||
!
user
.
isActive
)
{
throw
new
AppError
(
'User not found or inactive'
,
401
,
ERROR_CODE
.
USER_INACTIVE
);
}
const
newPayload
=
{
id
:
user
.
id
,
email
:
user
.
email
,
role
:
user
.
role
.
name
};
const
newAccessToken
=
jwt
.
sign
(
newPayload
,
jwtConfig
.
accessSecret
,
{
expiresIn
:
jwtConfig
.
accessExpiresIn
as
any
,
});
const
newRefreshToken
=
jwt
.
sign
(
newPayload
,
jwtConfig
.
refreshSecret
,
{
expiresIn
:
jwtConfig
.
refreshExpiresIn
as
any
,
});
await
this
.
repository
.
deleteRefreshToken
(
token
);
const
decoded
=
jwt
.
decode
(
newRefreshToken
)
as
{
exp
:
number
};
const
expiresAt
=
new
Date
(
decoded
.
exp
*
1000
);
await
this
.
repository
.
saveRefreshToken
(
user
.
id
,
newRefreshToken
,
expiresAt
);
return
{
accessToken
:
newAccessToken
,
refreshToken
:
newRefreshToken
,
};
}
async
logout
(
token
:
string
)
{
await
this
.
repository
.
deleteRefreshToken
(
token
);
}
async
getMe
(
userId
:
string
)
{
const
user
=
await
this
.
repository
.
findById
(
userId
);
...
...
src/modules/auth/auth.validation.ts
View file @
8d3bead5
...
...
@@ -4,3 +4,11 @@ export const loginSchema = z.object({
email
:
z
.
string
().
email
(
'Invalid email format'
),
password
:
z
.
string
().
min
(
1
,
'Password is required'
),
});
export
const
refreshSchema
=
z
.
object
({
refreshToken
:
z
.
string
().
min
(
1
,
'Refresh token is required'
),
});
export
const
logoutSchema
=
z
.
object
({
refreshToken
:
z
.
string
().
min
(
1
,
'Refresh token is required'
),
});
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