feat: Authentication with backend

This commit is contained in:
Aleksi Lassila
2024-03-27 01:02:28 +02:00
parent 7318a0fa99
commit a574b718f0
21 changed files with 257 additions and 74 deletions

4
backend/.gitignore vendored
View File

@@ -1,3 +1,5 @@
swagger-spec.json
# compiled output
/dist
/node_modules
@@ -55,4 +57,4 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
*.sqlite
*.sqlite

View File

@@ -9,6 +9,7 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
@@ -1610,6 +1611,15 @@
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="
},
"node_modules/@nanogiants/nestjs-swagger-api-exception-decorator": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@nanogiants/nestjs-swagger-api-exception-decorator/-/nestjs-swagger-api-exception-decorator-1.6.11.tgz",
"integrity": "sha512-F2Jvj52BDFvKo0I5LFj+kSjwLQecqrs+ibDWokq6Xkod/wrT6gxGia1H/z7ENGk9XwwXfQL9rZt4W/+Vwp0ZhQ==",
"peerDependencies": {
"@nestjs/common": "^7.6.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
"@nestjs/swagger": "^4.8.1 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@nestjs/cli": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",

View File

@@ -17,9 +17,11 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"openapi:schema": "ts-node src/generate-openapi.ts"
},
"dependencies": {
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",

View File

@@ -1,22 +1,33 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
UseGuards,
Request,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { SignInDto } from '../user/user.dto';
import {
ApiOkResponse,
ApiProperty,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
export class SignInResponse {
@ApiProperty()
accessToken: string;
}
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post()
async signIn(@Body() signInDto: { name: string; password: string }) {
@ApiOkResponse({ description: 'User found', type: SignInResponse })
@ApiException(() => UnauthorizedException)
async signIn(@Body() signInDto: SignInDto) {
const { token } = await this.authService.signIn(
signInDto.name,
signInDto.password,
@@ -25,10 +36,4 @@ export class AuthController {
accessToken: token,
};
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}

View File

@@ -11,9 +11,12 @@ import { AccessTokenPayload } from './auth.service';
import { User } from '../user/user.entity';
import { UserService } from '../user/user.service';
export const GetUser = createParamDecorator((data, req): User => {
return req.user;
});
export const GetUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): User => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
@Injectable()
export class AuthGuard implements CanActivate {

View File

@@ -3,7 +3,7 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { JWT_SECRET } from 'src/consts';
import { JWT_SECRET } from '../consts';
@Module({
imports: [

View File

@@ -6,14 +6,13 @@ import * as fs from 'fs';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
const config = new DocumentBuilder().build();
const document = SwaggerModule.createDocument(app, config, {
deepScanRoutes: true,
});
SwaggerModule.setup('openapi', app, document);
SwaggerModule.setup('openapi', app, document, {});
fs.writeFileSync('./swagger-spec.json', JSON.stringify(document));
}
bootstrap();

View File

@@ -7,7 +7,7 @@ import * as fs from 'fs';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableCors();
const config = new DocumentBuilder().build();
const document = SwaggerModule.createDocument(app, config, {

View File

@@ -14,6 +14,7 @@ import { AuthGuard, GetUser } from '../auth/auth.guard';
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { CreateUserDto, UserDto } from './user.dto';
import { User } from './user.entity';
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
@ApiTags('user')
@Controller('user')
@@ -22,9 +23,11 @@ export class UserController {
@UseGuards(AuthGuard)
@Get()
@ApiNotFoundResponse({ description: 'User not found' })
@ApiOkResponse({ description: 'User found', type: UserDto })
@ApiException(() => NotFoundException, { description: 'User not found' })
async getProfile(@GetUser() user: User): Promise<UserDto> {
console.log(user);
if (!user) {
throw new NotFoundException();
}
@@ -35,7 +38,7 @@ export class UserController {
@UseGuards(AuthGuard)
@Get(':id')
@ApiOkResponse({ description: 'User found', type: UserDto })
@ApiNotFoundResponse({ description: 'User not found' })
@ApiException(() => NotFoundException, { description: 'User not found' })
async findById(
@Param('id') id: string,
@GetUser() callerUser: User,

View File

@@ -19,3 +19,5 @@ export class CreateUserDto extends PickType(User, [
] as const) {}
export class UpdateUserDto extends OmitType(User, ['id'] as const) {}
export class SignInDto extends PickType(User, ['name', 'password'] as const) {}

View File

@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { userProviders } from './user.providers';
import { DatabaseModule } from 'src/database/database.module';
import { UserController } from './user.controller';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule],

View File

@@ -1,6 +1,6 @@
import { DATA_SOURCE } from 'src/database/database.providers';
import { DataSource } from 'typeorm';
import { User } from './user.entity';
import { DATA_SOURCE } from '../database/database.providers';
export const USER_REPOSITORY = 'USER_REPOSITORY';

View File

@@ -1 +0,0 @@
{"openapi":"3.0.0","paths":{"/api/user":{"get":{"operationId":"UserController_getProfile","parameters":[],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDto"}}}},"404":{"description":"User not found"}},"tags":["user"]},"post":{"operationId":"UserController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"200":{"description":""}},"tags":["user"]}},"/api/user/{id}":{"get":{"operationId":"UserController_findById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDto"}}}},"404":{"description":"User not found"}},"tags":["user"]}},"/api/auth":{"post":{"operationId":"AuthController_signIn","parameters":[],"responses":{"200":{"description":""}}}},"/api/auth/profile":{"get":{"operationId":"AuthController_getProfile","parameters":[],"responses":{"200":{"description":""}}}},"/api":{"get":{"operationId":"AppController_getHello","parameters":[],"responses":{"200":{"description":""}}}}},"info":{"title":"","description":"","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"schemas":{"SonarrSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"qualityProfileId":{"type":"number"},"rootFolderPath":{"type":"string"},"languageProfileId":{"type":"number"}},"required":["apiKey","baseUrl","qualityProfileId","rootFolderPath","languageProfileId"]},"RadarrSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"qualityProfileId":{"type":"number"},"rootFolderPath":{"type":"string"}},"required":["apiKey","baseUrl","qualityProfileId","rootFolderPath"]},"JellyfinSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"userId":{"type":"string"}},"required":["apiKey","baseUrl","userId"]},"Settings":{"type":"object","properties":{"autoplayTrailers":{"type":"boolean"},"language":{"type":"string"},"animationDuration":{"type":"number"},"sonarr":{"$ref":"#/components/schemas/SonarrSettings"},"radarr":{"$ref":"#/components/schemas/RadarrSettings"},"jellyfin":{"$ref":"#/components/schemas/JellyfinSettings"}},"required":["autoplayTrailers","language","animationDuration","sonarr","radarr","jellyfin"]},"UserDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"isAdmin":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/Settings"}},"required":["id","name","isAdmin","settings"]},"CreateUserDto":{"type":"object","properties":{"name":{"type":"string"},"password":{"type":"string"},"isAdmin":{"type":"boolean"}},"required":["name","password","isAdmin"]}}}}