Skip to content

Commit

Permalink
Merge pull request #22 from kir-dev/authentication
Browse files Browse the repository at this point in the history
Authentication
  • Loading branch information
berenteb authored Sep 3, 2024
2 parents 5eb86ea + 3b1c868 commit 0a8a512
Show file tree
Hide file tree
Showing 44 changed files with 1,030 additions and 50 deletions.
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# This .env file is used only when using docker compose
# When not using compose, the components load their own .env files

# postgres
POSTGRES_DB: pek-infinity
POSTGRES_USER: pek-infinity
POSTGRES_PASSWORD: pek-infinity
POSTGRES_HOST: postgres

# backend
FRONTEND_CALLBACK: http://localhost:3000/login/jwt
JWT_SECRET: secret
AUTHSCH_CLIENT_ID:
AUTHSCH_CLIENT_SECRET:
POSTGRES_PRISMA_URL: postgres://pek-infinity:pek-infinity@postgres/pek-infinity
POSTGRES_URL_NON_POOLING: postgres://pek-infinity:pek-infinity@postgres/pek-infinity

# frontend
NEXT_PUBLIC_API_URL: http://localhost:3300
NEXT_PUBLIC_PRIVATE_API_URL: http://backend:3300
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/jsLinters/eslint.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/pek-infinity.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/prettier.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ COPY --from=builder /app/backend/dist/ ./



EXPOSE 3000
EXPOSE 3300
CMD ["node", "./main.js"]
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AUTHSCH_CLIENT_ID=
AUTHSCH_CLIENT_SECRET=
FRONTEND_CALLBACK=http://localhost:3000/login/jwt
JWT_SECRET=secret
PORT=3300
46 changes: 46 additions & 0 deletions backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,45 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Ping"
/api/v4/auth/login:
get:
operationId: AuthController_login
parameters: []
responses:
"200":
description: ""
"302":
description: Redirects to the AuthSch login page.
/api/v4/auth/callback:
get:
operationId: AuthController_oauthRedirect
parameters:
- name: code
required: true
in: query
schema: {}
responses:
"200":
description: ""
"302":
description: Redirects to the frontend with the JWT in the query string.
/api/v4/auth/me:
get:
operationId: AuthController_me
parameters: []
responses:
"200":
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/UserDto"
default:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/UserDto"
info:
title: PÉK API
description: Profiles and Groups
Expand All @@ -41,6 +80,13 @@ components:
type: string
required:
- ping
UserDto:
type: object
properties:
name:
type: string
required:
- name
externalDocs:
description: Source Code (GitHub)
url: https://github.com/kir-dev/pek-infinity
8 changes: 8 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,20 @@
}
},
"dependencies": {
"@kir-dev/passport-authsch": "^2.0.3",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"@prisma/client": "^5.18.0",
"dotenv": "^16.4.5",
"env-var": "^7.5.0",
"express": "^4.19.2",
"nestjs-prisma": "^0.23.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"prisma": "^5.17.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand All @@ -60,6 +67,7 @@
"@types/express": "^4.17.21",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
Expand Down
3 changes: 2 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

import { AuthModule } from './auth/auth.module';
import { PingModule } from './ping/ping.module';

@Module({
imports: [PrismaModule.forRoot({ isGlobal: true }), PingModule],
imports: [PrismaModule.forRoot({ isGlobal: true }), PingModule, AuthModule],
controllers: [],
providers: [],
})
Expand Down
13 changes: 11 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { writeFileSync } from 'node:fs';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

import { Logger, VersioningType } from '@nestjs/common';
Expand All @@ -21,7 +21,8 @@ export async function bootstrap(): Promise<{

app
.setGlobalPrefix('api')
.enableVersioning({ type: VersioningType.URI, defaultVersion: '4' });
.enableVersioning({ type: VersioningType.URI, defaultVersion: '4' })
.enableCors();

const config = new DocumentBuilder()
.setVersion('v4')
Expand All @@ -43,6 +44,14 @@ export function writeDocument(document: OpenAPIObject): void {
const openApiLogger = new Logger('OpenApiGenerator');
const PATH = join(__dirname, '..', 'openapi.yaml');

const newDocument = yaml.stringify(document);
const currentDocument = readFileSync(PATH, { encoding: 'utf-8', flag: 'r' });

if (newDocument === currentDocument) {
openApiLogger.log('No changes in openapi.yaml');
return;
}

openApiLogger.log('Writing openapi.yaml');
writeFileSync(PATH, yaml.stringify(document), {
encoding: 'utf-8',
Expand Down
19 changes: 19 additions & 0 deletions backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { AuthController } from './auth.controller';

describe('AuthController', () => {
let controller: AuthController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();

controller = module.get<AuthController>(AuthController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
45 changes: 45 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CurrentUser } from '@kir-dev/passport-authsch';
import { Controller, Get, Redirect, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiQuery, ApiResponse } from '@nestjs/swagger';

import { FRONTEND_CALLBACK } from '@/config/environment.config';

import { AuthService } from './auth.service';
import { UserDto } from './entities/user.entity';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@UseGuards(AuthGuard('authsch'))
@Get('login')
@ApiResponse({
status: 302,
description: 'Redirects to the AuthSch login page.',
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
login() {}

@Get('callback')
@UseGuards(AuthGuard('authsch'))
@Redirect()
@ApiResponse({
status: 302,
description: 'Redirects to the frontend with the JWT in the query string.',
})
@ApiQuery({ name: 'code', required: true })
oauthRedirect(@CurrentUser() user: UserDto) {
const jwt = this.authService.login(user);
return {
url: `${FRONTEND_CALLBACK}?jwt=${jwt}`,
};
}

@Get('me')
@UseGuards(AuthGuard('jwt'))
@ApiResponse({ type: UserDto })
me(@CurrentUser() user: UserDto): UserDto {
return user;
}
}
15 changes: 15 additions & 0 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AuthSchStrategy } from './authsch.strategy';
import { JwtStrategy } from './jwt.strategy';

@Module({
providers: [AuthService, AuthSchStrategy, JwtStrategy],
controllers: [AuthController],
imports: [PassportModule, JwtModule],
})
export class AuthModule {}
19 changes: 19 additions & 0 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();

service = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
18 changes: 18 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { JWT_SECRET } from '@/config/environment.config';

import { UserDto } from './entities/user.entity';

@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}

login(user: UserDto): string {
return this.jwtService.sign(user, {
secret: JWT_SECRET,
expiresIn: '7 days',
});
}
}
31 changes: 31 additions & 0 deletions backend/src/auth/authsch.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
AuthSchProfile,
AuthSchScope,
Strategy,
} from '@kir-dev/passport-authsch';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';

import {
AUTHSCH_CLIENT_ID,
AUTHSCH_CLIENT_SECRET,
} from '@/config/environment.config';

import { UserDto } from './entities/user.entity';

@Injectable()
export class AuthSchStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientId: AUTHSCH_CLIENT_ID,
clientSecret: AUTHSCH_CLIENT_SECRET,
scopes: [AuthSchScope.PROFILE, AuthSchScope.PEK_PROFILE],
});
}

validate(userProfile: AuthSchProfile): Promise<UserDto> {
return Promise.resolve({
name: userProfile.fullName,
});
}
}
6 changes: 6 additions & 0 deletions backend/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

export class UserDto {
@ApiProperty()
name: string;
}
20 changes: 20 additions & 0 deletions backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { JWT_SECRET } from '@/config/environment.config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: JWT_SECRET,
});
}

validate(payload: unknown): unknown {
return payload;
}
}
Loading

0 comments on commit 0a8a512

Please sign in to comment.