Compare commits
5 Commits
5ab072992b
...
f1f2fefdab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f2fefdab | ||
|
|
d11b0c65e0 | ||
|
|
96af8e564b | ||
|
|
42158d1fd4 | ||
|
|
f4c0bb0b76 |
@ -3,6 +3,7 @@ import { Router, RouterOutlet } from '@angular/router';
|
|||||||
import { MainMenu } from './components/main-menu/main-menu';
|
import { MainMenu } from './components/main-menu/main-menu';
|
||||||
import { AuthService } from './auth/auth.service';
|
import { AuthService } from './auth/auth.service';
|
||||||
import { AdminLayout } from './layout/admin-layout/admin-layout';
|
import { AdminLayout } from './layout/admin-layout/admin-layout';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -16,7 +17,17 @@ export class App {
|
|||||||
constructor(private authService: AuthService, private router: Router) {}
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.authService.logout();
|
// With the interceptor fixed, this is now the correct and robust way.
|
||||||
this.router.navigate(['/login']);
|
// The error from a failed server logout will propagate here.
|
||||||
|
this.authService.serverSideLogout().subscribe({
|
||||||
|
next: () => {
|
||||||
|
console.log('Server-side logout successful.');
|
||||||
|
this.authService.clientSideLogout();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Server-side logout failed, logging out client-side anyway.', err);
|
||||||
|
this.authService.clientSideLogout();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,76 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of, throwError } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly TOKEN_KEY = 'access_token';
|
private readonly ACCESS_TOKEN_KEY = 'accessToken';
|
||||||
|
private readonly REFRESH_TOKEN_KEY = 'refreshToken';
|
||||||
private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different
|
private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient, private router: Router) {}
|
||||||
|
|
||||||
login(credentials: { username: string; password: string }): Observable<any> {
|
login(credentials: { username: string; password: string }): Observable<any> {
|
||||||
return this.http.post<{ access_token: string }>(`${this.apiUrl}/login`, credentials).pipe(
|
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/login`, credentials).pipe(
|
||||||
tap((response) => this.setToken(response.access_token))
|
tap((response) => this.setTokens(response.accessToken, response.refreshToken))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void {
|
/**
|
||||||
localStorage.removeItem(this.TOKEN_KEY);
|
* Makes a best-effort call to the server to invalidate the refresh token.
|
||||||
|
*/
|
||||||
|
serverSideLogout(): Observable<any> {
|
||||||
|
return this.http.post(`${this.apiUrl}/logout`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string | null {
|
/**
|
||||||
return localStorage.getItem(this.TOKEN_KEY);
|
* Performs the client-side cleanup, removing tokens and redirecting to login.
|
||||||
|
* This is the definitive logout action from the user's perspective.
|
||||||
|
*/
|
||||||
|
clientSideLogout(): void {
|
||||||
|
console.info("clientSideLogout")
|
||||||
|
this.removeTokens();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken(): Observable<any> {
|
||||||
|
const refreshToken = this.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
// If no refresh token is present, logout and return an error.
|
||||||
|
this.clientSideLogout();
|
||||||
|
return throwError(() => new Error('No refresh token available'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {}, {
|
||||||
|
headers: { Authorization: `Bearer ${refreshToken}` }
|
||||||
|
}).pipe(
|
||||||
|
tap((response) => this.setTokens(response.accessToken, response.refreshToken))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessToken(): string | null {
|
||||||
|
return localStorage.getItem(this.ACCESS_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRefreshToken(): string | null {
|
||||||
|
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
isLoggedIn(): boolean {
|
||||||
return this.getToken() !== null;
|
return this.getAccessToken() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setToken(token: string): void {
|
private setTokens(accessToken: string, refreshToken: string): void {
|
||||||
localStorage.setItem(this.TOKEN_KEY, token);
|
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
|
||||||
|
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeTokens(): void {
|
||||||
|
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,26 +4,81 @@ import {
|
|||||||
HttpHandler,
|
HttpHandler,
|
||||||
HttpInterceptor,
|
HttpInterceptor,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
|
HttpErrorResponse,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, throwError, BehaviorSubject } from 'rxjs';
|
||||||
|
import { catchError, switchMap, filter, take, finalize } from 'rxjs/operators'; // Import finalize
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtInterceptor implements HttpInterceptor {
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
|
private isRefreshing = false;
|
||||||
|
// Initialize refreshTokenSubject with null
|
||||||
|
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
intercept(
|
intercept(
|
||||||
request: HttpRequest<any>,
|
request: HttpRequest<any>,
|
||||||
next: HttpHandler
|
next: HttpHandler
|
||||||
): Observable<HttpEvent<any>> {
|
): Observable<HttpEvent<any>> {
|
||||||
const token = this.authService.getToken();
|
if (request.url.includes('/auth/refresh')) {
|
||||||
if (token) {
|
return next.handle(request);
|
||||||
request = request.clone({
|
}
|
||||||
|
|
||||||
|
const accessToken = this.authService.getAccessToken();
|
||||||
|
if (accessToken) {
|
||||||
|
request = this.addToken(request, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle(request).pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||||
|
return this.handle401Error(request, next);
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||||
|
if (!this.isRefreshing) {
|
||||||
|
this.isRefreshing = true;
|
||||||
|
// Reset the refreshTokenSubject to null so that subsequent requests will wait
|
||||||
|
// this.refreshTokenSubject.next(null);
|
||||||
|
this.refreshTokenSubject = new BehaviorSubject<any>(null);
|
||||||
|
|
||||||
|
|
||||||
|
return this.authService.refreshToken().pipe(
|
||||||
|
switchMap((token: any) => {
|
||||||
|
this.refreshTokenSubject.next(token.accessToken);
|
||||||
|
return next.handle(this.addToken(request, token.accessToken));
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
// If refresh fails, logout the user
|
||||||
|
this.authService.clientSideLogout();
|
||||||
|
return throwError(() => err);
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
// When the refresh attempt completes, set isRefreshing to false
|
||||||
|
this.isRefreshing = false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If a refresh is already in progress, wait for it to complete
|
||||||
|
return this.refreshTokenSubject.pipe(
|
||||||
|
filter(token => token != null),
|
||||||
|
take(1),
|
||||||
|
switchMap(jwt => next.handle(this.addToken(request, jwt)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToken(request: HttpRequest<any>, token: string) {
|
||||||
|
return request.clone({
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next.handle(request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
POST http://localhost:3000/auth/login
|
POST {{apiBaseUrl}}/auth/login
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -6,10 +6,17 @@ Content-Type: application/json
|
|||||||
"password": "123456"
|
"password": "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
> {% client.global.set("auth_token", response.body.access_token); %}
|
> {% client.global.set("auth_token", response.body.accessToken); %}
|
||||||
|
|
||||||
|
|
||||||
### GET request with parameter
|
### GET request with parameter
|
||||||
GET http://localhost:3000/users
|
GET {{apiBaseUrl}}/users
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### GET request with parameter
|
||||||
|
POST {{apiBaseUrl}}/auth/logout
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Authorization: Bearer {{auth_token}}
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|||||||
5
server/http-client.env.json
Normal file
5
server/http-client.env.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"apiBaseUrl": "http://localhost:3000/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,16 @@
|
|||||||
import { Controller, Post, Body, ValidationPipe } from '@nestjs/common';
|
import {
|
||||||
import { AuthService } from './auth.service';
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
ValidationPipe,
|
||||||
|
UseGuards,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { LoginRequestDto } from './dto/login-request.dto';
|
import { LoginRequestDto } from './dto/login-request.dto';
|
||||||
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
|
import { JwtRefreshAuthGuard } from './jwt-refresh-auth.guard';
|
||||||
|
import express from 'express';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@ -10,4 +20,18 @@ export class AuthController {
|
|||||||
async login(@Body(new ValidationPipe()) body: LoginRequestDto) {
|
async login(@Body(new ValidationPipe()) body: LoginRequestDto) {
|
||||||
return await this.authService.login(body);
|
return await this.authService.login(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post('logout')
|
||||||
|
async logout(@Req() req: express.Request) {
|
||||||
|
const user = req.user as { sub: number };
|
||||||
|
return await this.authService.logout(user.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtRefreshAuthGuard)
|
||||||
|
@Post('refresh')
|
||||||
|
async refresh(@Req() req: express.Request) {
|
||||||
|
const user = req.user as { sub: number; refreshToken: string };
|
||||||
|
return await this.authService.refreshToken(user.sub, user.refreshToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,22 +6,27 @@ import { AuthService } from './auth.service';
|
|||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtRefreshTokenStrategy } from './jwt-refresh.strategy';
|
||||||
|
import { StringValue } from 'ms';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule, // <--- Import ConfigModule here
|
ConfigModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
|
// Restore the correct async registration for JwtModule
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('JWT_SECRET'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: { expiresIn: '60m' },
|
signOptions: {
|
||||||
|
expiresIn: configService.get<StringValue>('JWT_EXPIRATION_TIME'),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, JwtStrategy],
|
providers: [AuthService, JwtStrategy, JwtRefreshTokenStrategy],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { User } from '../entity/user';
|
import { User } from '../entity/user';
|
||||||
import { LoginRequest, LoginResponse } from '../types';
|
import { LoginRequest, LoginResponse } from '../types';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import type { StringValue } from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateUser(username: string, pass: string): Promise<User | null> {
|
async validateUser(username: string, pass: string): Promise<User | null> {
|
||||||
@ -34,19 +37,84 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokens = await this.getTokens(user);
|
||||||
|
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(userId: number): Promise<void> {
|
||||||
|
await this.userService.setRefreshToken(userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(
|
||||||
|
userId: number,
|
||||||
|
refreshToken: string,
|
||||||
|
): Promise<LoginResponse> {
|
||||||
|
const user = await this.userService.findOne(userId);
|
||||||
|
if (!user || !user.hashedRefreshToken) {
|
||||||
|
throw new UnauthorizedException('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTokenMatches = await bcrypt.compare(
|
||||||
|
refreshToken,
|
||||||
|
user.hashedRefreshToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!refreshTokenMatches) {
|
||||||
|
throw new UnauthorizedException('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await this.getTokens(user);
|
||||||
|
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTokens(user: User): Promise<LoginResponse> {
|
||||||
const roles: Set<string> = new Set<string>();
|
const roles: Set<string> = new Set<string>();
|
||||||
for (const group of user.groups ?? []) {
|
for (const group of user.groups ?? []) {
|
||||||
for (const role of group.roles ?? []) {
|
for (const role of group.roles ?? []) {
|
||||||
roles.add(role.name);
|
roles.add(role.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const payload = {
|
|
||||||
|
const jwtSecret = this.configService.get<string>('JWT_SECRET');
|
||||||
|
const jwtexpirationtime = this.configService.get<string>(
|
||||||
|
'JWT_EXPIRATION_TIME',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
'creating. jwt secret is: ' + jwtSecret + ',' + jwtexpirationtime,
|
||||||
|
);
|
||||||
|
|
||||||
|
// let accessToken: string, refreshToken: string;
|
||||||
|
const [accessToken, refreshToken] = await Promise.all([
|
||||||
|
this.jwtService.signAsync(
|
||||||
|
{
|
||||||
username: user.username,
|
username: user.username,
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
roles: Array.from(roles),
|
roles: Array.from(roles),
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
|
expiresIn: +this.configService.get<StringValue>('JWT_EXPIRATION_TIME')!,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
this.jwtService.signAsync(
|
||||||
|
{
|
||||||
|
sub: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||||
|
expiresIn: this.configService.get<StringValue>(
|
||||||
|
'JWT_REFRESH_EXPIRATION_TIME',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: this.jwtService.sign(payload),
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
server/src/auth/jwt-refresh-auth.guard.ts
Normal file
5
server/src/auth/jwt-refresh-auth.guard.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtRefreshAuthGuard extends AuthGuard('jwt-refresh') {}
|
||||||
47
server/src/auth/jwt-refresh.strategy.ts
Normal file
47
server/src/auth/jwt-refresh.strategy.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy, StrategyOptionsWithRequest } from 'passport-jwt';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
// Define the shape of the JWT payload
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: number;
|
||||||
|
// iat and exp are automatically added by passport-jwt
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the shape of the object returned by the validate function
|
||||||
|
interface JwtPayloadWithRefreshToken extends JwtPayload {
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtRefreshTokenStrategy extends PassportStrategy(
|
||||||
|
Strategy,
|
||||||
|
'jwt-refresh',
|
||||||
|
) {
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
|
||||||
|
passReqToCallback: true,
|
||||||
|
} as StrategyOptionsWithRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(req: Request, payload: JwtPayload): JwtPayloadWithRefreshToken {
|
||||||
|
const refreshToken = req.get('Authorization')?.replace('Bearer', '').trim();
|
||||||
|
|
||||||
|
// Ensure refreshToken is not undefined before returning
|
||||||
|
if (!refreshToken) {
|
||||||
|
// This case should be rare given the guard is used, but it's good practice
|
||||||
|
// to handle it. Depending on strictness, you might throw an error.
|
||||||
|
// For now, we'll proceed, but in a real-world scenario, logging or an error
|
||||||
|
// might be better.
|
||||||
|
return { ...payload, refreshToken: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...payload, refreshToken };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,18 +3,32 @@ import { PassportStrategy } from '@nestjs/passport';
|
|||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Role } from './role.enum';
|
import { Role } from './role.enum';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
constructor(configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET') as string,
|
// DO NOT use secretOrKey here. It causes a race condition with ConfigService.
|
||||||
|
// Instead, use secretOrKeyProvider to look up the secret dynamically
|
||||||
|
// at request time, ensuring ConfigService is ready.
|
||||||
|
secretOrKeyProvider: (
|
||||||
|
request: Request,
|
||||||
|
rawJwtToken: any,
|
||||||
|
done: (err: any, secretOrKey?: string | Buffer) => void,
|
||||||
|
) => {
|
||||||
|
const secretKey = this.configService.get<string>('JWT_SECRET');
|
||||||
|
console.info('secretKey', secretKey);
|
||||||
|
done(null, secretKey);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(payload: { sub: string; username: string; roles: Role[] }) {
|
// The payload is already validated by passport-jwt at this point,
|
||||||
|
// so we can trust its contents.
|
||||||
|
validate(payload: { sub: number; username: string; roles: Role[] }) {
|
||||||
return {
|
return {
|
||||||
userId: payload.sub,
|
userId: payload.sub,
|
||||||
username: payload.username,
|
username: payload.username,
|
||||||
|
|||||||
@ -24,4 +24,7 @@ export class User {
|
|||||||
@ManyToMany(() => UserGroup)
|
@ManyToMany(() => UserGroup)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
groups: UserGroup[];
|
groups: UserGroup[];
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
hashedRefreshToken: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddRefreshTokenToUserObject1763106308120 implements MigrationInterface {
|
||||||
|
name = 'AddRefreshTokenToUserObject1763106308120'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "hashedRefreshToken" character varying`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hashedRefreshToken"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ export interface LoginRequest{
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
access_token: string;
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -54,4 +54,20 @@ export class UserService {
|
|||||||
this.logger.log(`Removing user with id: ${id}`, 'UserService');
|
this.logger.log(`Removing user with id: ${id}`, 'UserService');
|
||||||
await this.usersRepository.delete(id);
|
await this.usersRepository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setRefreshToken(
|
||||||
|
id: number,
|
||||||
|
refreshToken: string | null,
|
||||||
|
): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Updating refresh token for user with id: ${id}`,
|
||||||
|
'UserService',
|
||||||
|
);
|
||||||
|
if (refreshToken) {
|
||||||
|
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
||||||
|
await this.usersRepository.update(id, { hashedRefreshToken });
|
||||||
|
} else {
|
||||||
|
await this.usersRepository.update(id, { hashedRefreshToken: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user