Skip to content

Commit e027088

Browse files
PengyuChen01pengyuautofix-ci[bot]Sma1lboyZHallen122
authored
feat(frontend): adding home page and refreshToken in JWT (#124)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced authentication with email-based login and refresh token support. - Introduced user-friendly modals for signing in and signing up. - Added new UI elements including dynamic background gradients, textured cards, and custom icons. - New GraphQL mutations for user registration and login. - **Bug Fixes** - Improved error handling and logging for authentication processes. - **Refactor** - Streamlined authentication flows and session management. - Improved GraphQL operations to better support project and chat functionalities. - **Chores** - Updated environment configurations and dependencies. - Refined layout and styling adjustments for an improved user experience. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: pengyu <pengyuchen01@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Sma1lboy <541898146chen@gmail.com> Co-authored-by: ZHallen122 <106571949+ZHallen122@users.noreply.github.com> Co-authored-by: Jackson Chen <90215880+Sma1lboy@users.noreply.github.com> Co-authored-by: ZHallen122 <zhallen12261@gmail.com>
1 parent 9c103b9 commit e027088

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4121
-3678
lines changed

.github/workflows/frontend-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,4 @@ jobs:
6262
./frontend/node_modules
6363
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
6464
restore-keys: |
65-
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
65+
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-

backend/.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
PORT=8080
2-
JWT_SECRET="JACKSONCHENNAHEULALLEN"
2+
JWT_SECRET="JACKSONCHENNAHEULALLENPENGYU"
3+
JWT_REFRESH_SECRET="JACKSONCHENNAHEULALLENPENGYUREFRESH"
34
SALT_ROUNDS=123

backend/.env.development

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
PORT=8080
2-
JWT_SECRET="JACKSONCHENNAHEULALLEN"
2+
JWT_SECRET="JACKSONCHENNAHEULALLENPENGYU"
3+
JWT_REFRESH="JACKSONCHENNAHEULALLENPENGYUREFRESH"
34
SALT_ROUNDS=123
45
OPENAI_BASE_URI="http://localhost:3001"

backend/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
5252

5353
# log files with timestamp
5454
log-*/
55+
56+
57+
# Backend
58+
/backend/package-lock.json

backend/src/auth/auth.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { AuthService } from './auth.service';
88
import { User } from 'src/user/user.model';
99
import { AuthResolver } from './auth.resolver';
1010
import { JwtCacheService } from 'src/auth/jwt-cache.service';
11+
import { RefreshToken } from './refresh-token/refresh-token.model';
1112

1213
@Module({
1314
imports: [
1415
ConfigModule,
15-
TypeOrmModule.forFeature([Role, Menu, User]),
16+
TypeOrmModule.forFeature([Role, Menu, User, RefreshToken]),
1617
JwtModule.registerAsync({
1718
imports: [ConfigModule],
1819
useFactory: async (configService: ConfigService) => ({

backend/src/auth/auth.resolver.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
import { Args, Query, Resolver } from '@nestjs/graphql';
1+
import {
2+
Args,
3+
Query,
4+
Resolver,
5+
Mutation,
6+
Field,
7+
ObjectType,
8+
} from '@nestjs/graphql';
29
import { AuthService } from './auth.service';
310
import { CheckTokenInput } from './dto/check-token.input';
411

12+
@ObjectType()
13+
export class RefreshTokenResponse {
14+
@Field()
15+
accessToken: string;
16+
17+
@Field()
18+
refreshToken: string;
19+
}
20+
521
@Resolver()
622
export class AuthResolver {
723
constructor(private readonly authService: AuthService) {}
@@ -10,4 +26,11 @@ export class AuthResolver {
1026
async checkToken(@Args('input') params: CheckTokenInput): Promise<boolean> {
1127
return this.authService.validateToken(params);
1228
}
29+
30+
@Mutation(() => RefreshTokenResponse)
31+
async refreshToken(
32+
@Args('refreshToken') refreshToken: string,
33+
): Promise<RefreshTokenResponse> {
34+
return this.authService.refreshToken(refreshToken);
35+
}
1336
}

backend/src/auth/auth.service.ts

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import { ConfigService } from '@nestjs/config';
99
import { JwtService } from '@nestjs/jwt';
1010
import { InjectRepository } from '@nestjs/typeorm';
11-
import { compare, hash } from 'bcrypt';
1211
import { LoginUserInput } from 'src/user/dto/login-user.input';
1312
import { RegisterUserInput } from 'src/user/dto/register-user.input';
1413
import { User } from 'src/user/user.model';
@@ -17,6 +16,10 @@ import { CheckTokenInput } from './dto/check-token.input';
1716
import { JwtCacheService } from 'src/auth/jwt-cache.service';
1817
import { Menu } from './menu/menu.model';
1918
import { Role } from './role/role.model';
19+
import { RefreshToken } from './refresh-token/refresh-token.model';
20+
import { randomUUID } from 'crypto';
21+
import { compare, hash } from 'bcrypt';
22+
import { RefreshTokenResponse } from './auth.resolver';
2023

2124
@Injectable()
2225
export class AuthService {
@@ -30,17 +33,20 @@ export class AuthService {
3033
private menuRepository: Repository<Menu>,
3134
@InjectRepository(Role)
3235
private roleRepository: Repository<Role>,
36+
@InjectRepository(RefreshToken)
37+
private refreshTokenRepository: Repository<RefreshToken>,
3338
) {}
3439

3540
async register(registerUserInput: RegisterUserInput): Promise<User> {
3641
const { username, email, password } = registerUserInput;
3742

43+
// Check for existing email
3844
const existingUser = await this.userRepository.findOne({
39-
where: [{ username }, { email }],
45+
where: { email },
4046
});
4147

4248
if (existingUser) {
43-
throw new ConflictException('Username or email already exists');
49+
throw new ConflictException('Email already exists');
4450
}
4551

4652
const hashedPassword = await hash(password, 10);
@@ -53,13 +59,11 @@ export class AuthService {
5359
return this.userRepository.save(newUser);
5460
}
5561

56-
async login(
57-
loginUserInput: LoginUserInput,
58-
): Promise<{ accessToken: string }> {
59-
const { username, password } = loginUserInput;
62+
async login(loginUserInput: LoginUserInput): Promise<RefreshTokenResponse> {
63+
const { email, password } = loginUserInput;
6064

6165
const user = await this.userRepository.findOne({
62-
where: [{ username: username }],
66+
where: { email },
6367
});
6468

6569
if (!user) {
@@ -72,11 +76,32 @@ export class AuthService {
7276
throw new UnauthorizedException('Invalid credentials');
7377
}
7478

75-
const payload = { userId: user.id, username: user.username };
76-
const accessToken = this.jwtService.sign(payload);
77-
this.jwtCacheService.storeToken(accessToken);
79+
const accessToken = this.jwtService.sign(
80+
{ userId: user.id, email: user.email },
81+
{ expiresIn: '30m' },
82+
);
83+
84+
const refreshTokenEntity = await this.createRefreshToken(user);
85+
this.jwtCacheService.storeAccessToken(refreshTokenEntity.token);
86+
87+
return {
88+
accessToken,
89+
refreshToken: refreshTokenEntity.token,
90+
};
91+
}
92+
93+
private async createRefreshToken(user: User): Promise<RefreshToken> {
94+
const token = randomUUID();
95+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
96+
97+
const refreshToken = this.refreshTokenRepository.create({
98+
user,
99+
token,
100+
expiresAt: new Date(Date.now() + sevenDays), // 7 days
101+
});
78102

79-
return { accessToken };
103+
await this.refreshTokenRepository.save(refreshToken);
104+
return refreshToken;
80105
}
81106

82107
async validateToken(params: CheckTokenInput): Promise<boolean> {
@@ -89,18 +114,20 @@ export class AuthService {
89114
}
90115
}
91116
async logout(token: string): Promise<boolean> {
92-
Logger.log('logout token', token);
93117
try {
94118
await this.jwtService.verifyAsync(token);
95-
} catch (error) {
96-
return false;
97-
}
119+
const refreshToken = await this.refreshTokenRepository.findOne({
120+
where: { token },
121+
});
122+
123+
if (refreshToken) {
124+
await this.refreshTokenRepository.remove(refreshToken);
125+
}
98126

99-
if (!(await this.jwtCacheService.isTokenStored(token))) {
127+
return true;
128+
} catch (error) {
100129
return false;
101130
}
102-
this.jwtCacheService.removeToken(token);
103-
return true;
104131
}
105132

106133
async assignMenusToRole(roleId: string, menuIds: string[]): Promise<Role> {
@@ -340,4 +367,35 @@ export class AuthService {
340367
menus: userMenus,
341368
};
342369
}
370+
371+
/**
372+
* refresh access token base on refresh token.
373+
* @param refreshToken refresh token
374+
* @returns return new access token and refresh token
375+
*/
376+
async refreshToken(refreshToken: string): Promise<RefreshTokenResponse> {
377+
const existingToken = await this.refreshTokenRepository.findOne({
378+
where: { token: refreshToken },
379+
relations: ['user'],
380+
});
381+
382+
if (!existingToken || existingToken.expiresAt < new Date()) {
383+
throw new UnauthorizedException('Invalid refresh token');
384+
}
385+
386+
const accessToken = this.jwtService.sign(
387+
{
388+
userId: existingToken.user.id,
389+
email: existingToken.user.email,
390+
},
391+
{ expiresIn: '30m' },
392+
);
393+
394+
this.jwtCacheService.storeAccessToken(accessToken);
395+
396+
return {
397+
accessToken,
398+
refreshToken: refreshToken,
399+
};
400+
}
343401
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { InputType, Field } from '@nestjs/graphql';
2+
3+
@InputType()
4+
export class LoginUserInput {
5+
@Field()
6+
email: string;
7+
8+
@Field()
9+
password: string;
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
Entity,
3+
Column,
4+
PrimaryGeneratedColumn,
5+
CreateDateColumn,
6+
} from 'typeorm';
7+
8+
@Entity()
9+
export class RefreshToken {
10+
@PrimaryGeneratedColumn()
11+
id: number;
12+
13+
@Column()
14+
token: string;
15+
16+
@Column()
17+
userId: number;
18+
19+
@Column()
20+
expiresAt: Date;
21+
22+
@CreateDateColumn()
23+
createdAt: Date;
24+
}

backend/src/auth/jwt-cache.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy {
8383
});
8484
}
8585

86-
async storeToken(token: string): Promise<void> {
86+
/**
87+
* The storeAccessToken method stores the access token in the cache dbds
88+
* @param token the access token
89+
* @returns return void
90+
*/
91+
async storeAccessToken(token: string): Promise<void> {
8792
this.logger.debug(`Storing token: ${token.substring(0, 10)}...`);
8893
return new Promise((resolve, reject) => {
8994
this.db.run(

0 commit comments

Comments
 (0)