add auth
This commit is contained in:
@ -7,3 +7,7 @@ DB_DATABASE=teamplanner
|
||||
|
||||
# App
|
||||
PORT=4001
|
||||
|
||||
# Keycloak
|
||||
KEYCLOAK_REALM_URL=https://auth.vigdorov.ru/realms/team-planner
|
||||
KEYCLOAK_CLIENT_ID=team-planner-frontend
|
||||
|
||||
@ -30,11 +30,15 @@
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"jwks-rsa": "^3.2.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.16.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@ -49,6 +53,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { Public } from './auth';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
@Public()
|
||||
health(): { status: string } {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { IdeasModule } from './ideas/ideas.module';
|
||||
import { AuthModule, JwtAuthGuard } from './auth';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -26,9 +28,16 @@ import { IdeasModule } from './ideas/ideas.module';
|
||||
synchronize: false,
|
||||
}),
|
||||
}),
|
||||
AuthModule,
|
||||
IdeasModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
providers: [
|
||||
AppService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
11
backend/src/auth/auth.module.ts
Normal file
11
backend/src/auth/auth.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
|
||||
providers: [JwtStrategy, JwtAuthGuard],
|
||||
exports: [JwtAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
4
backend/src/auth/decorators/public.decorator.ts
Normal file
4
backend/src/auth/decorators/public.decorator.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
4
backend/src/auth/index.ts
Normal file
4
backend/src/auth/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './auth.module';
|
||||
export * from './jwt-auth.guard';
|
||||
export * from './jwt.strategy';
|
||||
export * from './decorators/public.decorator';
|
||||
24
backend/src/auth/jwt-auth.guard.ts
Normal file
24
backend/src/auth/jwt-auth.guard.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_KEY } from './decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
51
backend/src/auth/jwt.strategy.ts
Normal file
51
backend/src/auth/jwt.strategy.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
preferred_username: string;
|
||||
email: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService) {
|
||||
const realmUrl = configService.get<string>('KEYCLOAK_REALM_URL');
|
||||
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
issuer: realmUrl,
|
||||
algorithms: ['RS256'],
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `${realmUrl}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload): AuthUser {
|
||||
return {
|
||||
userId: payload.sub,
|
||||
username: payload.preferred_username,
|
||||
email: payload.email,
|
||||
firstName: payload.given_name,
|
||||
lastName: payload.family_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './create-idea.dto';
|
||||
export * from './update-idea.dto';
|
||||
export * from './query-ideas.dto';
|
||||
export * from './reorder-ideas.dto';
|
||||
|
||||
26
backend/src/ideas/dto/reorder-ideas.dto.ts
Normal file
26
backend/src/ideas/dto/reorder-ideas.dto.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsInt,
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
ArrayMinSize,
|
||||
} from 'class-validator';
|
||||
|
||||
export class ReorderItemDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
order: number;
|
||||
}
|
||||
|
||||
export class ReorderIdeasDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ReorderItemDto)
|
||||
items: ReorderItemDto[];
|
||||
}
|
||||
@ -10,7 +10,12 @@ import {
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { IdeasService } from './ideas.service';
|
||||
import { CreateIdeaDto, UpdateIdeaDto, QueryIdeasDto } from './dto';
|
||||
import {
|
||||
CreateIdeaDto,
|
||||
UpdateIdeaDto,
|
||||
QueryIdeasDto,
|
||||
ReorderIdeasDto,
|
||||
} from './dto';
|
||||
|
||||
@Controller('ideas')
|
||||
export class IdeasController {
|
||||
@ -31,6 +36,11 @@ export class IdeasController {
|
||||
return this.ideasService.getModules();
|
||||
}
|
||||
|
||||
@Patch('reorder')
|
||||
reorder(@Body() reorderIdeasDto: ReorderIdeasDto) {
|
||||
return this.ideasService.reorder(reorderIdeasDto.items);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.ideasService.findOne(id);
|
||||
|
||||
@ -123,4 +123,12 @@ export class IdeasService {
|
||||
|
||||
return result.map((r) => r.module).filter(Boolean);
|
||||
}
|
||||
|
||||
async reorder(items: { id: string; order: number }[]): Promise<void> {
|
||||
await this.ideasRepository.manager.transaction(async (manager) => {
|
||||
for (const item of items) {
|
||||
await manager.update(Idea, item.id, { order: item.order });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user