feat: Backend typing and openapi schema & codegen
This commit is contained in:
69
backend/package-lock.json
generated
69
backend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
@@ -1604,6 +1605,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
|
||||
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",
|
||||
@@ -1811,6 +1817,25 @@
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
||||
"integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz",
|
||||
@@ -1884,6 +1909,38 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz",
|
||||
"integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q=="
|
||||
},
|
||||
"node_modules/@nestjs/swagger": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz",
|
||||
"integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "^0.14.2",
|
||||
"@nestjs/mapped-types": "2.0.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"swagger-ui-dist": "5.11.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^6.0.0 || ^7.0.0",
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/static": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz",
|
||||
@@ -3043,8 +3100,7 @@
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
@@ -6754,7 +6810,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -6947,8 +7002,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
@@ -9361,6 +9415,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.11.2",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
|
||||
"integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { AuthGuard } from './auth.guard';
|
||||
|
||||
describe('AuthGuard', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new AuthGuard()).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,26 @@
|
||||
import {
|
||||
CanActivate,
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { JWT_SECRET } from '../consts';
|
||||
import { AuthUser } from './auth.service';
|
||||
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;
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private jwtService: JwtService) {}
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
@@ -19,12 +29,15 @@ export class AuthGuard implements CanActivate {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: JWT_SECRET,
|
||||
});
|
||||
// 💡 We're assigning the payload to the request object here
|
||||
// so that we can access it in our route handlers
|
||||
request['user'] = payload as AuthUser;
|
||||
const payload: AccessTokenPayload = await this.jwtService.verifyAsync(
|
||||
token,
|
||||
{
|
||||
secret: JWT_SECRET,
|
||||
},
|
||||
);
|
||||
if (payload.sub) {
|
||||
request['user'] = await this.userService.findOne(payload.sub);
|
||||
}
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,8 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
interface AccessTokenPayload {
|
||||
export interface AccessTokenPayload {
|
||||
sub: string;
|
||||
name: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -29,8 +27,6 @@ export class AuthService {
|
||||
|
||||
const payload: AccessTokenPayload = {
|
||||
sub: user.id,
|
||||
name: user.name,
|
||||
isAdmin: user.isAdmin,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -38,8 +34,3 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
}
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
|
||||
19
backend/src/generate-openapi.ts
Normal file
19
backend/src/generate-openapi.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import 'reflect-metadata';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
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);
|
||||
fs.writeFileSync('./swagger-spec.json', JSON.stringify(document));
|
||||
}
|
||||
bootstrap();
|
||||
@@ -1,10 +1,21 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import 'reflect-metadata';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
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);
|
||||
fs.writeFileSync('./swagger-spec.json', JSON.stringify(document));
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserController } from './user.controller';
|
||||
|
||||
describe('UserController', () => {
|
||||
let controller: UserController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UserController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<UserController>(UserController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -8,55 +8,65 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
import { AuthUser } from 'src/auth/auth.service';
|
||||
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';
|
||||
|
||||
@ApiTags('user')
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get()
|
||||
async getProfile(@Request() req) {
|
||||
const user = await this.userService.findOne((req.user as AuthUser).id);
|
||||
|
||||
@ApiNotFoundResponse({ description: 'User not found' })
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
async getProfile(@GetUser() user: User): Promise<UserDto> {
|
||||
if (!user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return user;
|
||||
return UserDto.fromEntity(user);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Param('id') id: string) {
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
@ApiNotFoundResponse({ description: 'User not found' })
|
||||
async findById(
|
||||
@Param('id') id: string,
|
||||
@GetUser() callerUser: User,
|
||||
): Promise<UserDto> {
|
||||
if (!callerUser.isAdmin && callerUser.id !== id) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
const user = await this.userService.findOne(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return user;
|
||||
return UserDto.fromEntity(user);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post()
|
||||
async create(
|
||||
@Body()
|
||||
userCreateDto: {
|
||||
name: string;
|
||||
password: string;
|
||||
isAdmin?: boolean;
|
||||
},
|
||||
userCreateDto: CreateUserDto,
|
||||
) {
|
||||
const canCreateAdmin = await this.userService.noPreviousAdmins();
|
||||
|
||||
return this.userService.create(
|
||||
const user = await this.userService.create(
|
||||
userCreateDto.name,
|
||||
userCreateDto.password,
|
||||
canCreateAdmin && userCreateDto.isAdmin,
|
||||
);
|
||||
|
||||
return UserDto.fromEntity(user);
|
||||
}
|
||||
}
|
||||
|
||||
21
backend/src/user/user.dto.ts
Normal file
21
backend/src/user/user.dto.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OmitType, PickType } from '@nestjs/swagger';
|
||||
import { User } from './user.entity';
|
||||
|
||||
export class UserDto extends OmitType(User, ['password'] as const) {
|
||||
static fromEntity(entity: User): UserDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
isAdmin: entity.isAdmin,
|
||||
settings: entity.settings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateUserDto extends PickType(User, [
|
||||
'name',
|
||||
'password',
|
||||
'isAdmin',
|
||||
] as const) {}
|
||||
|
||||
export class UpdateUserDto extends OmitType(User, ['id'] as const) {}
|
||||
@@ -1,6 +1,60 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
export class SonarrSettings {
|
||||
@ApiProperty({ required: true })
|
||||
apiKey: string;
|
||||
@ApiProperty({ required: true })
|
||||
baseUrl: string;
|
||||
@ApiProperty({ required: true })
|
||||
qualityProfileId: number;
|
||||
@ApiProperty({ required: true })
|
||||
rootFolderPath: string;
|
||||
@ApiProperty({ required: true })
|
||||
languageProfileId: number;
|
||||
}
|
||||
|
||||
export class RadarrSettings {
|
||||
@ApiProperty({ required: true })
|
||||
apiKey: string;
|
||||
@ApiProperty({ required: true })
|
||||
baseUrl: string;
|
||||
@ApiProperty({ required: true })
|
||||
qualityProfileId: number;
|
||||
@ApiProperty({ required: true })
|
||||
rootFolderPath: string;
|
||||
}
|
||||
|
||||
export class JellyfinSettings {
|
||||
@ApiProperty({ required: true })
|
||||
apiKey: string;
|
||||
@ApiProperty({ required: true })
|
||||
baseUrl: string;
|
||||
@ApiProperty({ required: true })
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export class Settings {
|
||||
@ApiProperty({ required: true })
|
||||
autoplayTrailers: boolean;
|
||||
@ApiProperty({ required: true })
|
||||
language: string;
|
||||
@ApiProperty({ required: true })
|
||||
animationDuration: number;
|
||||
// discover: {
|
||||
// region: string,
|
||||
// excludeLibraryItems: true,
|
||||
// includedLanguages: 'en'
|
||||
// },
|
||||
@ApiProperty({ required: true, type: SonarrSettings })
|
||||
sonarr: SonarrSettings;
|
||||
@ApiProperty({ required: true, type: RadarrSettings })
|
||||
radarr: RadarrSettings;
|
||||
@ApiProperty({ required: true, type: JellyfinSettings })
|
||||
jellyfin: JellyfinSettings;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
autoplayTrailers: true,
|
||||
language: 'en',
|
||||
animationDuration: 300,
|
||||
@@ -31,45 +85,23 @@ const DEFAULT_SETTINGS = {
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@ApiProperty({ required: true })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@Column()
|
||||
isAdmin: boolean = false;
|
||||
|
||||
@ApiProperty({ required: true, type: Settings })
|
||||
@Column('json', { default: JSON.stringify(DEFAULT_SETTINGS) })
|
||||
settings: {
|
||||
autoplayTrailers: boolean;
|
||||
language: string;
|
||||
animationDuration: number;
|
||||
// discover: {
|
||||
// region: string,
|
||||
// excludeLibraryItems: true,
|
||||
// includedLanguages: 'en'
|
||||
// },
|
||||
sonarr: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
qualityProfileId: number;
|
||||
rootFolderPath: string;
|
||||
languageProfileId: number;
|
||||
};
|
||||
radarr: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
qualityProfileId: number;
|
||||
rootFolderPath: string;
|
||||
};
|
||||
jellyfin: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userId: string;
|
||||
};
|
||||
} = DEFAULT_SETTINGS;
|
||||
settings = DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
1
backend/swagger-spec.json
Normal file
1
backend/swagger-spec.json
Normal file
@@ -0,0 +1 @@
|
||||
{"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"]}}}}
|
||||
Reference in New Issue
Block a user