migrate to native config

This commit is contained in:
rubikscraft 2022-03-06 12:27:11 +01:00
parent be38075169
commit f38b05d2e5
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
32 changed files with 627 additions and 229 deletions

View file

@ -20,6 +20,7 @@
},
"dependencies": {
"@nestjs/common": "^8.4.0",
"@nestjs/config": "^1.2.0",
"@nestjs/core": "^8.4.0",
"@nestjs/jwt": "^8.0.0",
"@nestjs/passport": "^8.2.1",

View file

@ -1,30 +1,24 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './routes/api/auth/auth.module';
import { ImageModule } from './routes/image/imageroute.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import Config from './env';
import { DemoManagerModule } from './managers/demo/demomanager.module';
import { EImageBackend } from './models/entities/image.entity';
import { EUserBackend } from './models/entities/user.entity';
import { PrefModule } from './routes/api/pref/pref.module';
import { ESysPreferenceBackend } from './models/entities/syspreference.entity';
import { TypeOrmConfigService } from './config/typeorm.config.service';
import { PicsurConfigModule } from './config/config.module';
import { ServeStaticConfigService } from './config/servestatic.config.service';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: Config.database.host,
port: Config.database.port,
username: Config.database.username,
password: Config.database.password,
database: Config.database.database,
synchronize: true,
entities: [EUserBackend, EImageBackend, ESysPreferenceBackend],
TypeOrmModule.forRootAsync({
useExisting: TypeOrmConfigService,
imports: [PicsurConfigModule],
}),
ServeStaticModule.forRoot({
rootPath: Config.static.frontendRoot,
ServeStaticModule.forRootAsync({
useExisting: ServeStaticConfigService,
imports: [PicsurConfigModule],
}),
AuthModule,
ImageModule,

View file

@ -1,11 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PicsurConfigModule } from '../../config/config.module';
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
import { SysPreferenceService } from './syspreferencedb.service';
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [TypeOrmModule.forFeature([ESysPreferenceBackend])],
providers: [SysPreferenceService],
imports: [
TypeOrmModule.forFeature([ESysPreferenceBackend]),
PicsurConfigModule,
],
providers: [SysPreferenceService, SysPreferenceDefaultsService],
exports: [SysPreferenceService],
})
export class SysPreferenceModule {}

View file

@ -5,8 +5,8 @@ import { validate } from 'class-validator';
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { Repository } from 'typeorm';
import { SysPreferenceDefaults } from '../../models/dto/syspreference.dto';
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
@Injectable()
export class SysPreferenceService {
@ -15,10 +15,11 @@ export class SysPreferenceService {
constructor(
@InjectRepository(ESysPreferenceBackend)
private sysPreferenceRepository: Repository<ESysPreferenceBackend>,
private defaultsService: SysPreferenceDefaultsService,
) {}
public async setPreference(
key: string,
key: SysPreferences,
value: string,
): AsyncFailable<ESysPreferenceBackend> {
let sysPreference = await this.validatePref(key, value);
@ -37,16 +38,17 @@ export class SysPreferenceService {
}
public async getPreference(
key: string,
key: SysPreferences,
): AsyncFailable<ESysPreferenceBackend> {
let sysPreference = await this.validatePref(key);
if (HasFailed(sysPreference)) return sysPreference;
let foundSysPreference: ESysPreferenceBackend | undefined;
try {
foundSysPreference = await this.sysPreferenceRepository.findOne({
key: sysPreference.key,
});
foundSysPreference = await this.sysPreferenceRepository.findOne(
{ key: sysPreference.key },
{ cache: 60000 },
);
} catch (e: any) {
this.logger.warn(e);
return Fail('Could not get preference');
@ -55,7 +57,10 @@ export class SysPreferenceService {
if (!foundSysPreference) {
return this.saveDefault(sysPreference.key);
} else {
foundSysPreference = plainToClass(ESysPreferenceBackend, foundSysPreference);
foundSysPreference = plainToClass(
ESysPreferenceBackend,
foundSysPreference,
);
const errors = await validate(foundSysPreference);
if (errors.length > 0) {
this.logger.warn(errors);
@ -69,7 +74,7 @@ export class SysPreferenceService {
private async saveDefault(
key: SysPreferences,
): AsyncFailable<ESysPreferenceBackend> {
return this.setPreference(key, SysPreferenceDefaults[key]());
return this.setPreference(key, this.defaultsService.defaults[key]());
}
private async validatePref(

View file

@ -0,0 +1,29 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
import { generateRandomString } from 'picsur-shared/dist/util/random';
import { EnvJwtConfigService } from '../../config/jwt.config.service';
@Injectable()
export class SysPreferenceDefaultsService {
private readonly logger = new Logger('SysPreferenceDefaultsService');
constructor(private jwtConfigService: EnvJwtConfigService) {}
public readonly defaults: {
[key in SysPreferences]: () => string;
} = {
jwt_secret: () => {
const envSecret = this.jwtConfigService.getJwtSecret();
if (envSecret) {
return envSecret;
} else {
this.logger.warn(
'Since no JWT secret was provided, a random one will be generated and saved',
);
return generateRandomString(64);
}
},
jwt_expires_in: () => this.jwtConfigService.getJwtExpiresIn() ?? '7d',
};
}

View file

@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthConfigService {
constructor(private configService: ConfigService) {}
public getDefaultAdminPassword(): string {
return this.configService.get<string>('DEFAULT_ADMIN_PASSWORD', 'admin');
}
public getDefaultAdminUsername(): string {
return this.configService.get<string>('DEFAULT_ADMIN_USERNAME', 'admin');
}
}

View file

@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmConfigService } from './typeorm.config.service';
import { ServeStaticConfigService } from './servestatic.config.service';
import { HostConfigService } from './host.config.service';
import { EnvJwtConfigService } from './jwt.config.service';
import { AuthConfigService } from './auth.config.service';
import { MultipartConfigService } from './multipart.config.service';
@Module({
imports: [
ConfigModule.forRoot({
ignoreEnvVars: true,
cache: true,
}),
],
providers: [
EnvJwtConfigService,
TypeOrmConfigService,
ServeStaticConfigService,
HostConfigService,
AuthConfigService,
MultipartConfigService,
],
exports: [
ConfigModule,
EnvJwtConfigService,
TypeOrmConfigService,
ServeStaticConfigService,
HostConfigService,
AuthConfigService,
MultipartConfigService,
],
})
export class PicsurConfigModule {}

View file

@ -0,0 +1,10 @@
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
export const EnvPrefix = 'PICSUR_';
export const DefaultName = 'picsur';
export const PackageRoot = resolve(
dirname(fileURLToPath(import.meta.url)),
'../../',
);

View file

@ -0,0 +1,42 @@
import { Injectable, Logger } from '@nestjs/common';
import {
ServeStaticModuleOptions,
ServeStaticModuleOptionsFactory,
} from '@nestjs/serve-static';
import { join } from 'path';
import { EnvPrefix, PackageRoot } from './config.static';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class HostConfigService {
private readonly logger = new Logger('HostConfigService');
constructor(private configService: ConfigService) {}
public getHost(): string {
const host = this.configService.get<string>(`${EnvPrefix}HOST`, '0.0.0.0');
this.logger.debug('Host: ' + host);
return host;
}
public getPort(): number {
const port = this.configService.get<number>(`${EnvPrefix}PORT`, 8080);
this.logger.debug('Port: ' + port);
return port;
}
public isDemo() {
const enabled = this.configService.get<boolean>(`${EnvPrefix}_DEMO`, false);
this.logger.debug('Demo enabled: ' + enabled);
return enabled;
}
public getDemoInterval() {
const interval = this.configService.get<number>(
`${EnvPrefix}_DEMO_INTERVAL`,
1000 * 60 * 5,
);
this.logger.debug('Demo interval: ' + interval);
return interval;
}
}

View file

@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class EnvJwtConfigService {
constructor(private configService: ConfigService) {}
public getJwtSecret(): string | undefined {
return this.configService.get<string>('JWT_SECRET');
}
public getJwtExpiresIn(): string | undefined {
return this.configService.get<string>('JWT_EXPIRES_IN');
}
}

View file

@ -0,0 +1,48 @@
import { FactoryProvider, Injectable, Logger } from '@nestjs/common';
import { JwtModuleOptions, JwtOptionsFactory } from '@nestjs/jwt';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../collections/syspreferencesdb/syspreferencedb.service';
@Injectable()
export class JwtConfigService implements JwtOptionsFactory {
private readonly logger = new Logger('JwtConfigService');
constructor(private prefService: SysPreferenceService) {}
public async getJwtSecret(): Promise<string> {
const secret = await this.prefService.getPreference('jwt_secret');
if (HasFailed(secret)) {
throw new Error('JWT secret could not be retrieved');
}
this.logger.debug('JWT secret: ' + secret.value);
return secret.value;
}
public async getJwtExpiresIn(): Promise<string> {
const expiresIn = await this.prefService.getPreference('jwt_expires_in');
if (HasFailed(expiresIn)) {
throw new Error('JWT expiresIn could not be retrieved');
}
this.logger.debug('JWT expiresIn: ' + expiresIn.value);
return expiresIn.value;
}
public async createJwtOptions(): Promise<JwtModuleOptions> {
return {
secret: await this.getJwtSecret(),
signOptions: {
expiresIn: await this.getJwtExpiresIn(),
},
};
}
}
export const JwtSecretProvider: FactoryProvider<Promise<string>> = {
provide: 'JWT_SECRET',
useFactory: async (jwtConfigService: JwtConfigService) => {
return await jwtConfigService.getJwtSecret();
},
inject: [JwtConfigService],
};

View file

@ -0,0 +1,40 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { JwtConfigService } from './jwt.lateconfig.service';
import { SysPreferenceModule } from '../collections/syspreferencesdb/syspreferencedb.module';
import { PicsurConfigModule } from './config.module';
import { EnvJwtConfigService } from './jwt.config.service';
import { SysPreferenceService } from '../collections/syspreferencesdb/syspreferencedb.service';
// This module contains all configservices that depend on the syspref module
// The syspref module can only be used when connected to the database
// Since the syspref module requires the database config, we need this seperate
// Otherwise we will create a circular depedency
@Module({
imports: [SysPreferenceModule, PicsurConfigModule],
providers: [JwtConfigService],
exports: [JwtConfigService, PicsurConfigModule],
})
export class PicsurLateConfigModule implements OnModuleInit {
private readonly logger = new Logger('PicsurLateConfigModule');
constructor(
private envJwtConfigService: EnvJwtConfigService,
private prefService: SysPreferenceService,
) {}
async onModuleInit() {
const secret = this.envJwtConfigService.getJwtSecret();
const expiresIn = this.envJwtConfigService.getJwtExpiresIn();
if (secret === undefined) {
await this.prefService.getPreference('jwt_secret');
} else {
await this.prefService.setPreference('jwt_secret', secret);
}
if (expiresIn !== undefined) {
await this.prefService.setPreference('jwt_expires_in', expiresIn);
}
}
}

View file

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvPrefix } from './config.static';
@Injectable()
export class MultipartConfigService {
constructor(private configService: ConfigService) {}
public getMaxFileSize(): number {
return this.configService.get<number>(
`${EnvPrefix}MAX_FILE_SIZE`,
128000000,
);
}
public getLimits() {
return {
fieldNameSize: 128,
fieldSize: 1024,
fields: 16,
files: 16,
fileSize: this.getMaxFileSize(),
};
}
}

View file

@ -0,0 +1,36 @@
import { Injectable, Logger } from '@nestjs/common';
import {
ServeStaticModuleOptions,
ServeStaticModuleOptionsFactory,
} from '@nestjs/serve-static';
import { join } from 'path';
import { EnvPrefix, PackageRoot } from './config.static';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class ServeStaticConfigService
implements ServeStaticModuleOptionsFactory
{
private readonly logger = new Logger('ServeStaticConfigService');
private defaultLocation = join(PackageRoot, '../frontend/dist');
constructor(private configService: ConfigService) {}
public getStaticDirectory(): string {
const directory = this.configService.get<string>(
`${EnvPrefix}STATIC_FRONTEND_ROOT`,
this.defaultLocation,
);
this.logger.debug('Static directory: ' + directory);
return directory;
}
public createLoggerOptions(): ServeStaticModuleOptions[] {
return [
{
rootPath: this.getStaticDirectory(),
},
];
}
}

View file

@ -0,0 +1,52 @@
import { Injectable, Logger } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { EntityList } from '../models/entities';
import { ConfigService } from '@nestjs/config';
import { DefaultName, EnvPrefix } from './config.static';
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
private readonly logger = new Logger('TypeOrmConfigService');
constructor(private configService: ConfigService) {}
public getTypeOrmServerOptions() {
const varOptions = {
host: this.configService.get<string>(`${EnvPrefix}_DB_HOST`, 'localhost'),
port: this.configService.get<number>(`${EnvPrefix}_DB_PORT`, 5432),
username: this.configService.get<string>(
`${EnvPrefix}_DB_USERNAME`,
DefaultName,
),
password: this.configService.get<string>(
`${EnvPrefix}_DB_PASSWORD`,
DefaultName,
),
database: this.configService.get<string>(
`${EnvPrefix}_DB_DATABASE`,
DefaultName,
),
};
this.logger.debug('DB host: ' + varOptions.host);
this.logger.debug('DB port: ' + varOptions.port);
this.logger.debug('DB username: ' + varOptions.username);
this.logger.debug('DB password: ' + varOptions.password);
this.logger.debug('DB database: ' + varOptions.database);
return varOptions;
}
public createTypeOrmOptions(connectionName?: string): TypeOrmModuleOptions {
this.logger.debug('Creating TypeOrmOptions for: ' + connectionName);
const varOptions = this.getTypeOrmServerOptions();
return {
type: 'postgres',
synchronize: true,
entities: EntityList,
...varOptions,
};
}
}

View file

@ -0,0 +1,22 @@
import {
createParamDecorator,
ExecutionContext,
} from '@nestjs/common';
import { Newable } from 'picsur-shared/dist/types';
import { MultiPartPipe } from './multipart.pipe';
import { PostFilePipe } from './postfile.pipe';
const InjectRequest = createParamDecorator(
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => {
return {
req: ctx.switchToHttp().getRequest(),
data,
};
},
);
export const PostFile = () =>
InjectRequest(PostFilePipe);
export const MultiPart = <T extends Object>(data: Newable<T>) =>
InjectRequest(data, MultiPartPipe);

View file

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PicsurConfigModule } from '../config/config.module';
import { MultiPartPipe } from './multipart.pipe';
import { PostFilePipe } from './postfile.pipe';
@Module({
imports: [PicsurConfigModule],
providers: [MultiPartPipe, PostFilePipe],
exports: [MultiPartPipe, PostFilePipe, PicsurConfigModule],
})
export class DecoratorsModule {
}

View file

@ -1,92 +0,0 @@
import {
BadRequestException,
createParamDecorator,
ExecutionContext,
Logger,
Type,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { FastifyRequest } from 'fastify';
import { Multipart, MultipartFields, MultipartFile } from 'fastify-multipart';
import { Newable } from 'picsur-shared/dist/types';
import Config from '../env';
import { MultiPartFieldDto, MultiPartFileDto } from '../models/dto/multipart.dto';
const logger = new Logger('MultiPart');
export interface MPFile {
fieldname: string;
}
export const PostFile = createParamDecorator(
async (data: unknown, ctx: ExecutionContext) => {
const req: FastifyRequest = ctx.switchToHttp().getRequest();
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
const file = await req.file({
limits: {
...Config.uploadLimits,
files: 1,
},
});
if (file === undefined) throw new BadRequestException('Invalid file');
const allFields: Multipart[] = Object.values(file.fields).filter(
(entry) => entry,
) as any;
const files = allFields.filter((entry) => entry.file !== undefined);
if (files.length !== 1) throw new BadRequestException('Invalid file');
try {
return await files[0].toBuffer();
} catch (e) {
throw new BadRequestException('Invalid file');
}
},
);
export const MultiPart = createParamDecorator(
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => {
const req: FastifyRequest = ctx.switchToHttp().getRequest();
const dtoClass = new data();
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
let fields: MultipartFields | null = null;
try {
fields = (
await req.file({
limits: Config.uploadLimits,
})
).fields;
} catch (e) {
logger.warn(e);
}
if (!fields) throw new BadRequestException('Invalid file');
for (const key of Object.keys(fields)) {
if (Array.isArray(fields[key])) {
continue;
}
if ((fields[key] as any).value) {
(dtoClass as any)[key] = new MultiPartFieldDto(fields[key] as MultipartFile);
} else {
(dtoClass as any)[key] = new MultiPartFileDto(
fields[key] as MultipartFile,
new BadRequestException('Invalid file'),
);
}
}
const errors = await validate(dtoClass, { forbidUnknownValues: true });
if (errors.length > 0) {
logger.warn(errors);
throw new BadRequestException('Invalid file');
}
return dtoClass;
},
);

View file

@ -0,0 +1,72 @@
import {
BadRequestException,
Injectable,
Logger,
PipeTransform,
Scope,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { FastifyRequest } from 'fastify';
import { MultipartFields, MultipartFile } from 'fastify-multipart';
import { Newable } from 'picsur-shared/dist/types';
import { MultipartConfigService } from '../config/multipart.config.service';
import {
MultiPartFieldDto,
MultiPartFileDto,
} from '../models/dto/multipart.dto';
@Injectable({ scope: Scope.REQUEST })
export class MultiPartPipe implements PipeTransform {
private readonly logger = new Logger('MultiPartPipe');
constructor(private multipartConfigService: MultipartConfigService) {}
async transform<T extends Object>({
req,
data,
}: {
req: FastifyRequest;
data: Newable<T>;
}) {
const dtoClass = new data();
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
let fields: MultipartFields | null = null;
try {
fields = (
await req.file({
limits: this.multipartConfigService.getLimits(),
})
).fields;
} catch (e) {
this.logger.warn(e);
}
if (!fields) throw new BadRequestException('Invalid file');
for (const key of Object.keys(fields)) {
if (Array.isArray(fields[key])) {
continue;
}
if ((fields[key] as any).value) {
(dtoClass as any)[key] = new MultiPartFieldDto(
fields[key] as MultipartFile,
);
} else {
(dtoClass as any)[key] = new MultiPartFileDto(
fields[key] as MultipartFile,
new BadRequestException('Invalid file'),
);
}
}
const errors = await validate(dtoClass, { forbidUnknownValues: true });
if (errors.length > 0) {
this.logger.warn(errors);
throw new BadRequestException('Invalid file');
}
return dtoClass;
}
}

View file

@ -0,0 +1,44 @@
import {
BadRequestException,
Injectable,
Logger,
PipeTransform,
Scope,
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { Multipart } from 'fastify-multipart';
import { MultipartConfigService } from '../config/multipart.config.service';
@Injectable({ scope: Scope.REQUEST })
export class PostFilePipe implements PipeTransform {
private readonly logger = new Logger('PostFilePipe');
constructor(private multipartConfigService: MultipartConfigService) {}
async transform({ req }: { req: FastifyRequest }) {
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
const file = await req.file({
limits: {
...this.multipartConfigService.getLimits(),
files: 1,
},
});
if (file === undefined) throw new BadRequestException('Invalid file');
const allFields: Multipart[] = Object.values(file.fields).filter(
(entry) => entry,
) as any;
const files = allFields.filter((entry) => entry.file !== undefined);
if (files.length !== 1) throw new BadRequestException('Invalid file');
try {
return await files[0].toBuffer();
} catch (e) {
this.logger.warn(e);
throw new BadRequestException('Invalid file');
}
}
}

View file

@ -1,51 +0,0 @@
import { dirname, resolve, join } from 'path';
import { fileURLToPath } from 'url';
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../');
const Config = {
main: {
host: process.env['PICSUR_HOST'] || '0.0.0.0',
port: process.env['PICSUR_PORT'] || 8080,
},
database: {
host: process.env['PICSUR_DB_HOST'] ?? 'localhost',
port: process.env['PICSUR_DB_PORT']
? parseInt(process.env['PICSUR_DB_PORT'])
: 5432,
username: process.env['PICSUR_DB_USERNAME'] ?? 'picsur',
password: process.env['PICSUR_DB_PASSWORD'] ?? 'picsur',
database: process.env['PICSUR_DB_DATABASE'] ?? 'picsur',
},
defaultAdmin: {
username: process.env['PICSUR_ADMIN_USERNAME'] ?? 'admin',
password: process.env['PICSUR_ADMIN_PASSWORD'] ?? 'admin',
},
jwt: {
secret: process.env['PICSUR_JWT_SECRET'] ?? 'CHANGE_ME',
expiresIn: process.env['PICSUR_JWT_EXPIRES_IN'] ?? '1d',
},
uploadLimits: {
fieldNameSize: 128,
fieldSize: 1024,
fields: 16,
files: 16,
fileSize: process.env['PICSUR_MAX_FILE_SIZE']
? parseInt(process.env['PICSUR_MAX_FILE_SIZE'])
: 128000000,
},
static: {
packageRoot: packageRoot,
frontendRoot:
process.env['PICSUR_STATIC_FRONTEND_ROOT'] ??
join(packageRoot, '../frontend/dist'),
backendRoutes: ['i', 'api'],
},
demo: {
enabled: process.env['PICSUR_DEMO']?.toLowerCase() === 'true',
interval: process.env['PICSUR_DEMO_INTERVAL']
? parseInt(process.env['PICSUR_DEMO_INTERVAL'])
: 1000 * 60 * 5,
},
};
export default Config;

View file

@ -9,12 +9,12 @@ import { AppModule } from './app.module';
import * as multipart from 'fastify-multipart';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor';
import Config from './env';
import { HostConfigService } from './config/host.config.service';
async function bootstrap() {
const fastifyAdapter = new FastifyAdapter();
// Todo: generic error messages
// TODO: generic error messages
fastifyAdapter.register(multipart as any);
const app = await NestFactory.create<NestFastifyApplication>(
@ -23,8 +23,15 @@ async function bootstrap() {
);
app.useGlobalFilters(new MainExceptionFilter());
app.useGlobalInterceptors(new SuccessInterceptor());
app.useGlobalPipes(new ValidationPipe({ disableErrorMessages: true, forbidUnknownValues: true }));
await app.listen(Config.main.port, Config.main.host);
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
forbidUnknownValues: true,
}),
);
const hostConfigService = app.get(HostConfigService);
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
}
bootstrap().catch(console.error);

View file

@ -1,26 +1,30 @@
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ImageDBModule } from '../../collections/imagedb/imagedb.module';
import Config from '../../env';
import { PicsurConfigModule } from '../../config/config.module';
import { HostConfigService } from '../../config/host.config.service';
import { DemoManagerService } from './demomanager.service';
@Module({
imports: [ImageDBModule],
imports: [ImageDBModule, PicsurConfigModule],
providers: [DemoManagerService],
})
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger('DemoManagerModule');
constructor(private readonly demoManagerService: DemoManagerService) {}
constructor(
private readonly demoManagerService: DemoManagerService,
private hostConfigService: HostConfigService,
) {}
private interval: NodeJS.Timeout;
onModuleInit() {
if (Config.demo.enabled) {
if (this.hostConfigService.isDemo()) {
this.logger.log('Demo mode enabled');
this.interval = setInterval(
this.demoManagerService.execute.bind(this.demoManagerService),
Config.demo.interval,
this.hostConfigService.getDemoInterval(),
);
}
}

View file

@ -1,12 +0,0 @@
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
import { generateRandomString } from 'picsur-shared/dist/util/random';
import Config from '../../env';
export const SysPreferenceDefaults: {
[key in SysPreferences]: () => string;
} = {
jwt_secret: () => {
if (Config.jwt.secret !== 'CHANGE_ME') return Config.jwt.secret;
return generateRandomString(32);
},
};

View file

@ -0,0 +1,5 @@
import { EImageBackend } from './image.entity';
import { ESysPreferenceBackend } from './syspreference.entity';
import { EUserBackend } from './user.entity';
export const EntityList = [EImageBackend, EUserBackend, ESysPreferenceBackend];

View file

@ -1,8 +1,4 @@
import {
Logger,
Module,
OnModuleInit,
} from '@nestjs/common';
import { ExistingProvider, Logger, Module, OnModuleInit } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalAuthStrategy } from './localauth.strategy';
@ -10,43 +6,49 @@ import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../../../collections/userdb/userdb.module';
import Config from '../../../env';
import { SysPreferenceModule } from '../../../collections/syspreferencesdb/syspreferencedb.module';
import { JwtConfigService, JwtSecretProvider } from '../../../config/jwt.lateconfig.service';
import { PicsurLateConfigModule } from '../../../config/lateconfig.module';
import { AuthConfigService } from '../../../config/auth.config.service';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: Config.jwt.secret,
signOptions: { expiresIn: Config.jwt.expiresIn },
SysPreferenceModule,
PicsurLateConfigModule,
JwtModule.registerAsync({
useExisting: JwtConfigService,
imports: [PicsurLateConfigModule],
}),
],
providers: [AuthService, LocalAuthStrategy, JwtStrategy],
providers: [
AuthService,
LocalAuthStrategy,
JwtStrategy,
JwtSecretProvider,
],
controllers: [AuthController],
})
export class AuthModule implements OnModuleInit {
private readonly logger = new Logger('AuthModule');
constructor(private authService: AuthService) {}
constructor(
private authService: AuthService,
private authConfigService: AuthConfigService,
) {}
onModuleInit() {
this.checkJwtSecret();
this.ensureAdminExists();
}
private checkJwtSecret() {
if (Config.jwt.secret === 'CHANGE_ME') {
this.logger.error(
"JWT secret is not set. Please set the 'JWT_SECRET' environment variable.",
);
}
async onModuleInit() {
await this.ensureAdminExists();
}
private async ensureAdminExists() {
const admin = Config.defaultAdmin;
this.logger.debug(`Ensuring admin user ${admin.username} exists`);
const username = this.authConfigService.getDefaultAdminUsername();
const password = this.authConfigService.getDefaultAdminPassword();
this.logger.debug(`Ensuring admin user "${username}" exists`);
await this.authService.createUser(admin.username, admin.password);
await this.authService.makeAdmin(admin.username);
await this.authService.createUser(username, password);
await this.authService.makeAdmin(username);
}
}

View file

@ -1,21 +1,24 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import {
Inject,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import Config from '../../../env';
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private readonly logger = new Logger('JwtStrategy');
constructor() {
constructor(@Inject('JWT_SECRET') private jwtSecret: string) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: Config.jwt.secret,
secretOrKey: jwtSecret,
});
}

View file

@ -8,7 +8,10 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { UpdateSysPreferenceRequest } from 'picsur-shared/dist/dto/syspreferences.dto';
import {
SysPreferences,
UpdateSysPreferenceRequest,
} from 'picsur-shared/dist/dto/syspreferences.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
import { AdminGuard } from '../auth/admin.guard';
@ -21,7 +24,9 @@ export class PrefController {
@Get('sys/:key')
async getSysPref(@Param('key') key: string) {
const returned = await this.prefService.getPreference(key);
const returned = await this.prefService.getPreference(
key as SysPreferences,
);
if (HasFailed(returned)) {
console.warn(returned.getReason());
throw new InternalServerErrorException('Could not get preference');
@ -36,7 +41,10 @@ export class PrefController {
@Body() body: UpdateSysPreferenceRequest,
) {
const value = body.value;
const returned = await this.prefService.setPreference(key, value);
const returned = await this.prefService.setPreference(
key as SysPreferences,
value,
);
if (HasFailed(returned)) {
console.warn(returned.getReason());
throw new InternalServerErrorException('Could not set preference');

View file

@ -11,10 +11,10 @@ import {
} from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { isHash } from 'class-validator';
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
import { MultiPart } from '../../decorators/decorator';
@Controller('i')
export class ImageController {
constructor(private readonly imagesService: ImageManagerService) {}

View file

@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { DecoratorsModule } from '../../decorators/decorators.module';
import { ImageManagerModule } from '../../managers/imagemanager/imagemanager.module';
import { ImageController } from './imageroute.controller';
@Module({
imports: [ImageManagerModule],
imports: [ImageManagerModule, DecoratorsModule],
controllers: [ImageController],
})
export class ImageModule {}

View file

@ -3,7 +3,7 @@ import tuple from '../types/tuple';
import { randomBytes } from 'crypto';
import { IsNotEmpty } from 'class-validator';
const SysPreferencesTuple = tuple('jwt_secret');
const SysPreferencesTuple = tuple('jwt_secret', 'jwt_expires_in');
export const SysPreferences: string[] = SysPreferencesTuple;
export type SysPreferences = typeof SysPreferencesTuple[number];

View file

@ -1409,6 +1409,16 @@
tslib "2.3.1"
uuid "8.3.2"
"@nestjs/config@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-1.2.0.tgz#a4eb58390dd8145b761ee0c8e98e78c8471cabb1"
integrity sha512-GGZOj2g6EMZ23orsQqeD2Vs5E2ZrmAiB0qCGvERv+5nQmZjY4nKkisG4awQsym1uotmmzgtsd9lOiKqTIFONhA==
dependencies:
dotenv "16.0.0"
dotenv-expand "8.0.1"
lodash "4.17.21"
uuid "8.3.2"
"@nestjs/core@^8.4.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.0.tgz#f4c1840b9b233e3985407f496b7c1e78e0169c70"
@ -3298,6 +3308,16 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dotenv-expand@8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.1.tgz#332aa17c14b12e28e2e230f8d183eecc1c014fdc"
integrity sha512-j/Ih7bIERDR5PzI89Zu8ayd3tXZ6E3dbY0ljQ9Db0K87qBO8zdLsi2dIvDHMWtjC3Yxb8XixOTHAtia0fDHRpg==
dotenv@16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
@ -5164,7 +5184,7 @@ lodash.once@^4.0.0:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==