Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fd093a7
fix port numebr in back end and add end point configeration to exampl…
Jan 15, 2025
f0f729e
bug-frontend
Jan 29, 2025
90db586
rebuild authentication
Feb 4, 2025
9446e78
fix the refresh token
Feb 5, 2025
431dbc0
halfway on the authtication
Feb 12, 2025
a021725
graphql doesn't send any variables
Feb 13, 2025
898f248
complete sign up process
Feb 16, 2025
432cd11
complete sign in in landing page
Feb 16, 2025
dcbfcc8
fetch newest main
Feb 19, 2025
a0a582f
adjust sign up email already use output error message instead of block
Feb 19, 2025
ad47a97
fix the dialog error
Feb 20, 2025
255d387
fix conflict
Feb 20, 2025
4ec4221
fix all recommendation from jackson except redis and duplicated apoll…
Feb 21, 2025
d666365
fix the duplicated apollo
Feb 22, 2025
4883088
change JWT from local storage to session storage
Feb 25, 2025
04e1051
fixed the bug of the button and add log out after user login
Feb 25, 2025
f7dfa14
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 25, 2025
adb82c8
refactor(graphql): remove unused mutations and update token storage
Sma1lboy Feb 26, 2025
75c31ca
refactor(client): use LocalStore constants for token keys
Sma1lboy Feb 26, 2025
0110f4b
refactor: clean up unused imports and update storage usage in AuthPro…
Sma1lboy Feb 26, 2025
888030c
refactor(chat): remove unused imports and metadata from layout and up…
Sma1lboy Feb 26, 2025
3239710
chore(backend): remove .npmrc file
Sma1lboy Feb 26, 2025
188bcb8
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2025
4f7077e
refactor(hooks): move use-mobile hook to useIsMobile and update impor…
Sma1lboy Feb 26, 2025
1930c8f
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2025
9617407
feat(backend): improve path protection when writing file (#128)
ZHallen122 Feb 26, 2025
7cdff89
fix(backend): fix a lot small issue in backend (#127)
Sma1lboy Feb 26, 2025
1991358
ci(github): add frontend CI workflow for build and cache (#129)
Sma1lboy Feb 26, 2025
8cd0fb4
Merge branch 'main' into backup-feat-connect-frontend-backend-3
Sma1lboy Feb 26, 2025
eafd2c8
Merge branch 'main' into backup-feat-connect-frontend-backend-3
Sma1lboy Feb 26, 2025
88dfbdc
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ jobs:
./frontend/node_modules
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
3 changes: 2 additions & 1 deletion backend/.env
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
3 changes: 2 additions & 1 deletion backend/.env.development
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"
SALT_ROUNDS=123
OPENAI_BASE_URI="http://localhost:3001"
4 changes: 4 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# log files with timestamp
log-*/


# Backend
/backend/package-lock.json
3 changes: 2 additions & 1 deletion backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Imported the duplicated RefreshToken entity.

This import references the model from refresh-token/refresh-token.model.ts, but there's a duplicate definition in entities/refresh-token.entity.ts. You need to decide which implementation to use and update imports accordingly.


@Module({
imports: [
ConfigModule,
TypeOrmModule.forFeature([Role, Menu, User]),
TypeOrmModule.forFeature([Role, Menu, User, RefreshToken]),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
Expand Down
25 changes: 24 additions & 1 deletion backend/src/auth/auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { Args, Query, Resolver } from '@nestjs/graphql';
import {
Args,
Query,
Resolver,
Mutation,
Field,
ObjectType,
} from '@nestjs/graphql';
import { AuthService } from './auth.service';
import { CheckTokenInput } from './dto/check-token.input';

@ObjectType()
export class RefreshTokenResponse {
@Field()
accessToken: string;

@Field()
refreshToken: string;
}

@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
Expand All @@ -10,4 +26,11 @@ export class AuthResolver {
async checkToken(@Args('input') params: CheckTokenInput): Promise<boolean> {
return this.authService.validateToken(params);
}

@Mutation(() => RefreshTokenResponse)
async refreshToken(
@Args('refreshToken') refreshToken: string,
): Promise<RefreshTokenResponse> {
return this.authService.refreshToken(refreshToken);
}
}
96 changes: 77 additions & 19 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -30,17 +33,20 @@
private menuRepository: Repository<Menu>,
@InjectRepository(Role)
private roleRepository: Repository<Role>,
@InjectRepository(RefreshToken)
private refreshTokenRepository: Repository<RefreshToken>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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);
Expand All @@ -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) {
Expand All @@ -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> {
Expand All @@ -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) {

Check warning on line 128 in backend/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / autofix

'error' is defined but never used
return false;
}
this.jwtCacheService.removeToken(token);
return true;
}

async assignMenusToRole(roleId: string, menuIds: string[]): Promise<Role> {
Expand Down Expand Up @@ -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,
};
}
}
10 changes: 10 additions & 0 deletions backend/src/auth/dto/login-user.input.ts
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;
}
24 changes: 24 additions & 0 deletions backend/src/auth/entities/refresh-token.entity.ts
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;
}
7 changes: 6 additions & 1 deletion backend/src/auth/jwt-cache.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy {
});
}

async storeToken(token: string): Promise<void> {
/**
* The storeAccessToken method stores the access token in the cache dbds
* @param token the access token
* @returns return void
*/
async storeAccessToken(token: string): Promise<void> {
this.logger.debug(`Storing token: ${token.substring(0, 10)}...`);
return new Promise((resolve, reject) => {
this.db.run(
Expand Down
20 changes: 20 additions & 0 deletions backend/src/auth/refresh-token/refresh-token.model.ts
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;
}
4 changes: 2 additions & 2 deletions backend/src/auth/role/role.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectType, Field } from '@nestjs/graphql';
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
import {
Entity,
Expand All @@ -12,7 +12,7 @@ import { Menu } from '../menu/menu.model';
@ObjectType()
@Entity()
export class Role {
@Field()
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;

Expand Down
3 changes: 2 additions & 1 deletion backend/src/decorator/get-auth-token.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const GetUserIdFromToken = createParamDecorator(
const decodedToken: any = jwtService.decode(token);

if (!decodedToken || !decodedToken.userId) {
throw new UnauthorizedException('Invalid token');
Logger.debug('invalid token, token:' + token);
throw new UnauthorizedException('Invalid token, token:', token);
}

return decodedToken.userId;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ async function bootstrap() {

app.enableCors({
origin: '*',
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Accept',
'Authorization',
'Access-Control-Allow-Origin',
'Access-Control-Allow-Credentials',
'x-refresh-token',
],
});

Expand Down
7 changes: 3 additions & 4 deletions backend/src/user/dto/login-user.input.ts
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;
}
6 changes: 3 additions & 3 deletions backend/src/user/user.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectType, Field } from '@nestjs/graphql';
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { IsEmail } from 'class-validator';
import { Role } from 'src/auth/role/role.model';
import { SystemBaseModel } from 'src/system-base-model/system-base.model';
Expand All @@ -16,13 +16,13 @@ import {
@Entity()
@ObjectType()
export class User extends SystemBaseModel {
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;

@Field()
@Column({ unique: true })
@Column()
username: string;

@Column()
password: string;

Expand Down
Loading
Loading