-
Notifications
You must be signed in to change notification settings - Fork 3
feat(frontend): adding home page and refreshToken in JWT #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fd093a7
f0f729e
90db586
9446e78
431dbc0
a021725
898f248
432cd11
dcbfcc8
a0a582f
ad47a97
255d387
4ec4221
d666365
4883088
04e1051
f7dfa14
adb82c8
75c31ca
0110f4b
888030c
3239710
188bcb8
4f7077e
1930c8f
9617407
7cdff89
1991358
8cd0fb4
eafd2c8
88dfbdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| PORT=8080 | ||
| JWT_SECRET="JACKSONCHENNAHEULALLEN" | ||
| JWT_SECRET="JACKSONCHENNAHEULALLENPENGYU" | ||
| JWT_REFRESH_SECRET="JACKSONCHENNAHEULALLENPENGYUREFRESH" | ||
| SALT_ROUNDS=123 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| PORT=8080 | ||
| JWT_SECRET="JACKSONCHENNAHEULALLEN" | ||
| JWT_SECRET="JACKSONCHENNAHEULALLENPENGYU" | ||
| JWT_REFRESH="JACKSONCHENNAHEULALLENPENGYUREFRESH" | ||
Sma1lboy marked this conversation as resolved.
Show resolved
Hide resolved
Sma1lboy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| SALT_ROUNDS=123 | ||
| OPENAI_BASE_URI="http://localhost:3001" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,3 +52,7 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |
|
|
||
| # log files with timestamp | ||
| log-*/ | ||
|
|
||
|
|
||
| # Backend | ||
| /backend/package-lock.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,11 +8,12 @@ import { AuthService } from './auth.service'; | |
| import { User } from 'src/user/user.model'; | ||
| import { AuthResolver } from './auth.resolver'; | ||
| import { JwtCacheService } from 'src/auth/jwt-cache.service'; | ||
| import { RefreshToken } from './refresh-token/refresh-token.model'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imported the duplicated RefreshToken entity. This import references the model from |
||
|
|
||
| @Module({ | ||
| imports: [ | ||
| ConfigModule, | ||
| TypeOrmModule.forFeature([Role, Menu, User]), | ||
| TypeOrmModule.forFeature([Role, Menu, User, RefreshToken]), | ||
| JwtModule.registerAsync({ | ||
| imports: [ConfigModule], | ||
| useFactory: async (configService: ConfigService) => ({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,6 @@ | |
| import { ConfigService } from '@nestjs/config'; | ||
| import { JwtService } from '@nestjs/jwt'; | ||
| import { InjectRepository } from '@nestjs/typeorm'; | ||
| import { compare, hash } from 'bcrypt'; | ||
| import { LoginUserInput } from 'src/user/dto/login-user.input'; | ||
| import { RegisterUserInput } from 'src/user/dto/register-user.input'; | ||
| import { User } from 'src/user/user.model'; | ||
|
|
@@ -17,6 +16,10 @@ | |
| import { JwtCacheService } from 'src/auth/jwt-cache.service'; | ||
| import { Menu } from './menu/menu.model'; | ||
| import { Role } from './role/role.model'; | ||
| import { RefreshToken } from './refresh-token/refresh-token.model'; | ||
| import { randomUUID } from 'crypto'; | ||
| import { compare, hash } from 'bcrypt'; | ||
| import { RefreshTokenResponse } from './auth.resolver'; | ||
|
|
||
| @Injectable() | ||
| export class AuthService { | ||
|
|
@@ -30,17 +33,20 @@ | |
| private menuRepository: Repository<Menu>, | ||
| @InjectRepository(Role) | ||
| private roleRepository: Repository<Role>, | ||
| @InjectRepository(RefreshToken) | ||
| private refreshTokenRepository: Repository<RefreshToken>, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for refresh token, using redis not in memory, different then access token |
||
| ) {} | ||
|
|
||
| async register(registerUserInput: RegisterUserInput): Promise<User> { | ||
| const { username, email, password } = registerUserInput; | ||
|
|
||
| // Check for existing email | ||
| const existingUser = await this.userRepository.findOne({ | ||
| where: [{ username }, { email }], | ||
| where: { email }, | ||
| }); | ||
|
|
||
| if (existingUser) { | ||
| throw new ConflictException('Username or email already exists'); | ||
| throw new ConflictException('Email already exists'); | ||
| } | ||
|
|
||
| const hashedPassword = await hash(password, 10); | ||
|
|
@@ -53,13 +59,11 @@ | |
| return this.userRepository.save(newUser); | ||
| } | ||
|
|
||
| async login( | ||
| loginUserInput: LoginUserInput, | ||
| ): Promise<{ accessToken: string }> { | ||
| const { username, password } = loginUserInput; | ||
| async login(loginUserInput: LoginUserInput): Promise<RefreshTokenResponse> { | ||
| const { email, password } = loginUserInput; | ||
|
|
||
| const user = await this.userRepository.findOne({ | ||
| where: [{ username: username }], | ||
| where: { email }, | ||
| }); | ||
|
|
||
| if (!user) { | ||
|
|
@@ -72,11 +76,32 @@ | |
| throw new UnauthorizedException('Invalid credentials'); | ||
| } | ||
|
|
||
| const payload = { userId: user.id, username: user.username }; | ||
| const accessToken = this.jwtService.sign(payload); | ||
| this.jwtCacheService.storeToken(accessToken); | ||
| const accessToken = this.jwtService.sign( | ||
| { userId: user.id, email: user.email }, | ||
| { expiresIn: '30m' }, | ||
| ); | ||
|
|
||
| const refreshTokenEntity = await this.createRefreshToken(user); | ||
| this.jwtCacheService.storeAccessToken(refreshTokenEntity.token); | ||
|
|
||
| return { | ||
| accessToken, | ||
| refreshToken: refreshTokenEntity.token, | ||
| }; | ||
| } | ||
|
|
||
| private async createRefreshToken(user: User): Promise<RefreshToken> { | ||
| const token = randomUUID(); | ||
| const sevenDays = 7 * 24 * 60 * 60 * 1000; | ||
|
|
||
| const refreshToken = this.refreshTokenRepository.create({ | ||
| user, | ||
| token, | ||
| expiresAt: new Date(Date.now() + sevenDays), // 7 days | ||
| }); | ||
|
|
||
| return { accessToken }; | ||
| await this.refreshTokenRepository.save(refreshToken); | ||
| return refreshToken; | ||
| } | ||
|
|
||
| async validateToken(params: CheckTokenInput): Promise<boolean> { | ||
|
|
@@ -89,18 +114,20 @@ | |
| } | ||
| } | ||
| async logout(token: string): Promise<boolean> { | ||
| Logger.log('logout token', token); | ||
| try { | ||
| await this.jwtService.verifyAsync(token); | ||
| } catch (error) { | ||
| return false; | ||
| } | ||
| const refreshToken = await this.refreshTokenRepository.findOne({ | ||
| where: { token }, | ||
| }); | ||
|
|
||
| if (refreshToken) { | ||
| await this.refreshTokenRepository.remove(refreshToken); | ||
| } | ||
|
|
||
| if (!(await this.jwtCacheService.isTokenStored(token))) { | ||
| return true; | ||
| } catch (error) { | ||
| return false; | ||
| } | ||
| this.jwtCacheService.removeToken(token); | ||
| return true; | ||
| } | ||
|
|
||
| async assignMenusToRole(roleId: string, menuIds: string[]): Promise<Role> { | ||
|
|
@@ -340,4 +367,35 @@ | |
| menus: userMenus, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * refresh access token base on refresh token. | ||
| * @param refreshToken refresh token | ||
| * @returns return new access token and refresh token | ||
| */ | ||
| async refreshToken(refreshToken: string): Promise<RefreshTokenResponse> { | ||
| const existingToken = await this.refreshTokenRepository.findOne({ | ||
| where: { token: refreshToken }, | ||
| relations: ['user'], | ||
| }); | ||
|
|
||
| if (!existingToken || existingToken.expiresAt < new Date()) { | ||
| throw new UnauthorizedException('Invalid refresh token'); | ||
| } | ||
|
|
||
| const accessToken = this.jwtService.sign( | ||
| { | ||
| userId: existingToken.user.id, | ||
| email: existingToken.user.email, | ||
| }, | ||
| { expiresIn: '30m' }, | ||
| ); | ||
|
|
||
| this.jwtCacheService.storeAccessToken(accessToken); | ||
|
|
||
| return { | ||
| accessToken, | ||
| refreshToken: refreshToken, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { InputType, Field } from '@nestjs/graphql'; | ||
|
|
||
| @InputType() | ||
| export class LoginUserInput { | ||
| @Field() | ||
| email: string; | ||
|
|
||
| @Field() | ||
| password: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { | ||
| Entity, | ||
| Column, | ||
| PrimaryGeneratedColumn, | ||
| CreateDateColumn, | ||
| } from 'typeorm'; | ||
|
|
||
| @Entity() | ||
| export class RefreshToken { | ||
| @PrimaryGeneratedColumn() | ||
| id: number; | ||
|
|
||
| @Column() | ||
| token: string; | ||
|
|
||
| @Column() | ||
| userId: number; | ||
|
|
||
| @Column() | ||
| expiresAt: Date; | ||
|
|
||
| @CreateDateColumn() | ||
| createdAt: Date; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; | ||
| import { User } from '../../user/user.model'; | ||
|
|
||
| @Entity() | ||
| export class RefreshToken { | ||
| @PrimaryGeneratedColumn('uuid') | ||
| id: string; | ||
|
|
||
| @Column() | ||
| token: string; | ||
|
|
||
| @Column() | ||
| expiresAt: Date; | ||
|
|
||
| @ManyToOne(() => User, { onDelete: 'CASCADE' }) | ||
| user: User; | ||
|
|
||
| @Column() | ||
| userId: number; | ||
| } | ||
Sma1lboy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,13 @@ | ||
| import { Field, InputType } from '@nestjs/graphql'; | ||
| import { IsString, MinLength } from 'class-validator'; | ||
| import { IsEmail, MinLength } from 'class-validator'; | ||
|
|
||
| @InputType() | ||
| export class LoginUserInput { | ||
| @Field() | ||
| @IsString() | ||
| username: string; | ||
| @IsEmail() | ||
| email: string; | ||
|
|
||
| @Field() | ||
| @IsString() | ||
| @MinLength(6) | ||
| password: string; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.