migrate to native config
This commit is contained in:
parent
be38075169
commit
f38b05d2e5
|
@ -20,6 +20,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^8.4.0",
|
"@nestjs/common": "^8.4.0",
|
||||||
|
"@nestjs/config": "^1.2.0",
|
||||||
"@nestjs/core": "^8.4.0",
|
"@nestjs/core": "^8.4.0",
|
||||||
"@nestjs/jwt": "^8.0.0",
|
"@nestjs/jwt": "^8.0.0",
|
||||||
"@nestjs/passport": "^8.2.1",
|
"@nestjs/passport": "^8.2.1",
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from './routes/api/auth/auth.module';
|
import { AuthModule } from './routes/api/auth/auth.module';
|
||||||
import { ImageModule } from './routes/image/imageroute.module';
|
import { ImageModule } from './routes/image/imageroute.module';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import Config from './env';
|
|
||||||
import { DemoManagerModule } from './managers/demo/demomanager.module';
|
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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRootAsync({
|
||||||
type: 'postgres',
|
useExisting: TypeOrmConfigService,
|
||||||
host: Config.database.host,
|
imports: [PicsurConfigModule],
|
||||||
port: Config.database.port,
|
|
||||||
username: Config.database.username,
|
|
||||||
password: Config.database.password,
|
|
||||||
database: Config.database.database,
|
|
||||||
synchronize: true,
|
|
||||||
|
|
||||||
entities: [EUserBackend, EImageBackend, ESysPreferenceBackend],
|
|
||||||
}),
|
}),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRootAsync({
|
||||||
rootPath: Config.static.frontendRoot,
|
useExisting: ServeStaticConfigService,
|
||||||
|
imports: [PicsurConfigModule],
|
||||||
}),
|
}),
|
||||||
AuthModule,
|
AuthModule,
|
||||||
ImageModule,
|
ImageModule,
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { PicsurConfigModule } from '../../config/config.module';
|
||||||
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
||||||
import { SysPreferenceService } from './syspreferencedb.service';
|
import { SysPreferenceService } from './syspreferencedb.service';
|
||||||
|
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([ESysPreferenceBackend])],
|
imports: [
|
||||||
providers: [SysPreferenceService],
|
TypeOrmModule.forFeature([ESysPreferenceBackend]),
|
||||||
|
PicsurConfigModule,
|
||||||
|
],
|
||||||
|
providers: [SysPreferenceService, SysPreferenceDefaultsService],
|
||||||
exports: [SysPreferenceService],
|
exports: [SysPreferenceService],
|
||||||
})
|
})
|
||||||
export class SysPreferenceModule {}
|
export class SysPreferenceModule {}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { validate } from 'class-validator';
|
||||||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { SysPreferenceDefaults } from '../../models/dto/syspreference.dto';
|
|
||||||
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
||||||
|
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SysPreferenceService {
|
export class SysPreferenceService {
|
||||||
|
@ -15,10 +15,11 @@ export class SysPreferenceService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ESysPreferenceBackend)
|
@InjectRepository(ESysPreferenceBackend)
|
||||||
private sysPreferenceRepository: Repository<ESysPreferenceBackend>,
|
private sysPreferenceRepository: Repository<ESysPreferenceBackend>,
|
||||||
|
private defaultsService: SysPreferenceDefaultsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async setPreference(
|
public async setPreference(
|
||||||
key: string,
|
key: SysPreferences,
|
||||||
value: string,
|
value: string,
|
||||||
): AsyncFailable<ESysPreferenceBackend> {
|
): AsyncFailable<ESysPreferenceBackend> {
|
||||||
let sysPreference = await this.validatePref(key, value);
|
let sysPreference = await this.validatePref(key, value);
|
||||||
|
@ -37,16 +38,17 @@ export class SysPreferenceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPreference(
|
public async getPreference(
|
||||||
key: string,
|
key: SysPreferences,
|
||||||
): AsyncFailable<ESysPreferenceBackend> {
|
): AsyncFailable<ESysPreferenceBackend> {
|
||||||
let sysPreference = await this.validatePref(key);
|
let sysPreference = await this.validatePref(key);
|
||||||
if (HasFailed(sysPreference)) return sysPreference;
|
if (HasFailed(sysPreference)) return sysPreference;
|
||||||
|
|
||||||
let foundSysPreference: ESysPreferenceBackend | undefined;
|
let foundSysPreference: ESysPreferenceBackend | undefined;
|
||||||
try {
|
try {
|
||||||
foundSysPreference = await this.sysPreferenceRepository.findOne({
|
foundSysPreference = await this.sysPreferenceRepository.findOne(
|
||||||
key: sysPreference.key,
|
{ key: sysPreference.key },
|
||||||
});
|
{ cache: 60000 },
|
||||||
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.warn(e);
|
this.logger.warn(e);
|
||||||
return Fail('Could not get preference');
|
return Fail('Could not get preference');
|
||||||
|
@ -55,7 +57,10 @@ export class SysPreferenceService {
|
||||||
if (!foundSysPreference) {
|
if (!foundSysPreference) {
|
||||||
return this.saveDefault(sysPreference.key);
|
return this.saveDefault(sysPreference.key);
|
||||||
} else {
|
} else {
|
||||||
foundSysPreference = plainToClass(ESysPreferenceBackend, foundSysPreference);
|
foundSysPreference = plainToClass(
|
||||||
|
ESysPreferenceBackend,
|
||||||
|
foundSysPreference,
|
||||||
|
);
|
||||||
const errors = await validate(foundSysPreference);
|
const errors = await validate(foundSysPreference);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
this.logger.warn(errors);
|
this.logger.warn(errors);
|
||||||
|
@ -69,7 +74,7 @@ export class SysPreferenceService {
|
||||||
private async saveDefault(
|
private async saveDefault(
|
||||||
key: SysPreferences,
|
key: SysPreferences,
|
||||||
): AsyncFailable<ESysPreferenceBackend> {
|
): AsyncFailable<ESysPreferenceBackend> {
|
||||||
return this.setPreference(key, SysPreferenceDefaults[key]());
|
return this.setPreference(key, this.defaultsService.defaults[key]());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validatePref(
|
private async validatePref(
|
||||||
|
|
|
@ -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',
|
||||||
|
};
|
||||||
|
}
|
15
backend/src/config/auth.config.service.ts
Normal file
15
backend/src/config/auth.config.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
35
backend/src/config/config.module.ts
Normal file
35
backend/src/config/config.module.ts
Normal 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 {}
|
10
backend/src/config/config.static.ts
Normal file
10
backend/src/config/config.static.ts
Normal 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)),
|
||||||
|
'../../',
|
||||||
|
);
|
42
backend/src/config/host.config.service.ts
Normal file
42
backend/src/config/host.config.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
15
backend/src/config/jwt.config.service.ts
Normal file
15
backend/src/config/jwt.config.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
48
backend/src/config/jwt.lateconfig.service.ts
Normal file
48
backend/src/config/jwt.lateconfig.service.ts
Normal 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],
|
||||||
|
};
|
40
backend/src/config/lateconfig.module.ts
Normal file
40
backend/src/config/lateconfig.module.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
backend/src/config/multipart.config.service.ts
Normal file
25
backend/src/config/multipart.config.service.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
36
backend/src/config/servestatic.config.service.ts
Normal file
36
backend/src/config/servestatic.config.service.ts
Normal 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(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
52
backend/src/config/typeorm.config.service.ts
Normal file
52
backend/src/config/typeorm.config.service.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
22
backend/src/decorators/decorator.ts
Normal file
22
backend/src/decorators/decorator.ts
Normal 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);
|
12
backend/src/decorators/decorators.module.ts
Normal file
12
backend/src/decorators/decorators.module.ts
Normal 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 {
|
||||||
|
}
|
|
@ -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;
|
|
||||||
},
|
|
||||||
);
|
|
72
backend/src/decorators/multipart.pipe.ts
Normal file
72
backend/src/decorators/multipart.pipe.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
44
backend/src/decorators/postfile.pipe.ts
Normal file
44
backend/src/decorators/postfile.pipe.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -9,12 +9,12 @@ import { AppModule } from './app.module';
|
||||||
import * as multipart from 'fastify-multipart';
|
import * as multipart from 'fastify-multipart';
|
||||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||||
import Config from './env';
|
import { HostConfigService } from './config/host.config.service';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const fastifyAdapter = new FastifyAdapter();
|
const fastifyAdapter = new FastifyAdapter();
|
||||||
|
|
||||||
// Todo: generic error messages
|
// TODO: generic error messages
|
||||||
fastifyAdapter.register(multipart as any);
|
fastifyAdapter.register(multipart as any);
|
||||||
|
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
|
@ -23,8 +23,15 @@ async function bootstrap() {
|
||||||
);
|
);
|
||||||
app.useGlobalFilters(new MainExceptionFilter());
|
app.useGlobalFilters(new MainExceptionFilter());
|
||||||
app.useGlobalInterceptors(new SuccessInterceptor());
|
app.useGlobalInterceptors(new SuccessInterceptor());
|
||||||
app.useGlobalPipes(new ValidationPipe({ disableErrorMessages: true, forbidUnknownValues: true }));
|
app.useGlobalPipes(
|
||||||
await app.listen(Config.main.port, Config.main.host);
|
new ValidationPipe({
|
||||||
|
disableErrorMessages: true,
|
||||||
|
forbidUnknownValues: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hostConfigService = app.get(HostConfigService);
|
||||||
|
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap().catch(console.error);
|
bootstrap().catch(console.error);
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
import { ImageDBModule } from '../../collections/imagedb/imagedb.module';
|
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';
|
import { DemoManagerService } from './demomanager.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ImageDBModule],
|
imports: [ImageDBModule, PicsurConfigModule],
|
||||||
providers: [DemoManagerService],
|
providers: [DemoManagerService],
|
||||||
})
|
})
|
||||||
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
||||||
private readonly logger = new Logger('DemoManagerModule');
|
private readonly logger = new Logger('DemoManagerModule');
|
||||||
|
|
||||||
constructor(private readonly demoManagerService: DemoManagerService) {}
|
constructor(
|
||||||
|
private readonly demoManagerService: DemoManagerService,
|
||||||
|
private hostConfigService: HostConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
private interval: NodeJS.Timeout;
|
private interval: NodeJS.Timeout;
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
if (Config.demo.enabled) {
|
if (this.hostConfigService.isDemo()) {
|
||||||
this.logger.log('Demo mode enabled');
|
this.logger.log('Demo mode enabled');
|
||||||
|
|
||||||
this.interval = setInterval(
|
this.interval = setInterval(
|
||||||
this.demoManagerService.execute.bind(this.demoManagerService),
|
this.demoManagerService.execute.bind(this.demoManagerService),
|
||||||
Config.demo.interval,
|
this.hostConfigService.getDemoInterval(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
5
backend/src/models/entities/index.ts
Normal file
5
backend/src/models/entities/index.ts
Normal 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];
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { ExistingProvider, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||||
Logger,
|
|
||||||
Module,
|
|
||||||
OnModuleInit,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { LocalAuthStrategy } from './localauth.strategy';
|
import { LocalAuthStrategy } from './localauth.strategy';
|
||||||
|
@ -10,43 +6,49 @@ import { AuthController } from './auth.controller';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
import { UsersModule } from '../../../collections/userdb/userdb.module';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UsersModule,
|
UsersModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.register({
|
SysPreferenceModule,
|
||||||
secret: Config.jwt.secret,
|
PicsurLateConfigModule,
|
||||||
signOptions: { expiresIn: Config.jwt.expiresIn },
|
JwtModule.registerAsync({
|
||||||
|
useExisting: JwtConfigService,
|
||||||
|
imports: [PicsurLateConfigModule],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, LocalAuthStrategy, JwtStrategy],
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
LocalAuthStrategy,
|
||||||
|
JwtStrategy,
|
||||||
|
JwtSecretProvider,
|
||||||
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule implements OnModuleInit {
|
export class AuthModule implements OnModuleInit {
|
||||||
private readonly logger = new Logger('AuthModule');
|
private readonly logger = new Logger('AuthModule');
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private authConfigService: AuthConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
async onModuleInit() {
|
||||||
this.checkJwtSecret();
|
await this.ensureAdminExists();
|
||||||
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.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureAdminExists() {
|
private async ensureAdminExists() {
|
||||||
const admin = Config.defaultAdmin;
|
const username = this.authConfigService.getDefaultAdminUsername();
|
||||||
this.logger.debug(`Ensuring admin user ${admin.username} exists`);
|
const password = this.authConfigService.getDefaultAdminPassword();
|
||||||
|
this.logger.debug(`Ensuring admin user "${username}" exists`);
|
||||||
|
|
||||||
await this.authService.createUser(admin.username, admin.password);
|
await this.authService.createUser(username, password);
|
||||||
await this.authService.makeAdmin(admin.username);
|
await this.authService.makeAdmin(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
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 { validate } from 'class-validator';
|
||||||
import { plainToClass } from 'class-transformer';
|
import { plainToClass } from 'class-transformer';
|
||||||
import Config from '../../../env';
|
|
||||||
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
|
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
|
||||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
private readonly logger = new Logger('JwtStrategy');
|
private readonly logger = new Logger('JwtStrategy');
|
||||||
|
|
||||||
constructor() {
|
constructor(@Inject('JWT_SECRET') private jwtSecret: string) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: Config.jwt.secret,
|
secretOrKey: jwtSecret,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ import {
|
||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} 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 { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
||||||
import { AdminGuard } from '../auth/admin.guard';
|
import { AdminGuard } from '../auth/admin.guard';
|
||||||
|
@ -21,7 +24,9 @@ export class PrefController {
|
||||||
|
|
||||||
@Get('sys/:key')
|
@Get('sys/:key')
|
||||||
async getSysPref(@Param('key') key: string) {
|
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)) {
|
if (HasFailed(returned)) {
|
||||||
console.warn(returned.getReason());
|
console.warn(returned.getReason());
|
||||||
throw new InternalServerErrorException('Could not get preference');
|
throw new InternalServerErrorException('Could not get preference');
|
||||||
|
@ -36,7 +41,10 @@ export class PrefController {
|
||||||
@Body() body: UpdateSysPreferenceRequest,
|
@Body() body: UpdateSysPreferenceRequest,
|
||||||
) {
|
) {
|
||||||
const value = body.value;
|
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)) {
|
if (HasFailed(returned)) {
|
||||||
console.warn(returned.getReason());
|
console.warn(returned.getReason());
|
||||||
throw new InternalServerErrorException('Could not set preference');
|
throw new InternalServerErrorException('Could not set preference');
|
||||||
|
|
|
@ -11,10 +11,10 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { MultiPart } from '../../decorators/multipart.decorator';
|
|
||||||
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
||||||
import { isHash } from 'class-validator';
|
import { isHash } from 'class-validator';
|
||||||
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||||
|
import { MultiPart } from '../../decorators/decorator';
|
||||||
@Controller('i')
|
@Controller('i')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
constructor(private readonly imagesService: ImageManagerService) {}
|
constructor(private readonly imagesService: ImageManagerService) {}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DecoratorsModule } from '../../decorators/decorators.module';
|
||||||
import { ImageManagerModule } from '../../managers/imagemanager/imagemanager.module';
|
import { ImageManagerModule } from '../../managers/imagemanager/imagemanager.module';
|
||||||
import { ImageController } from './imageroute.controller';
|
import { ImageController } from './imageroute.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ImageManagerModule],
|
imports: [ImageManagerModule, DecoratorsModule],
|
||||||
controllers: [ImageController],
|
controllers: [ImageController],
|
||||||
})
|
})
|
||||||
export class ImageModule {}
|
export class ImageModule {}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import tuple from '../types/tuple';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
const SysPreferencesTuple = tuple('jwt_secret');
|
const SysPreferencesTuple = tuple('jwt_secret', 'jwt_expires_in');
|
||||||
|
|
||||||
export const SysPreferences: string[] = SysPreferencesTuple;
|
export const SysPreferences: string[] = SysPreferencesTuple;
|
||||||
export type SysPreferences = typeof SysPreferencesTuple[number];
|
export type SysPreferences = typeof SysPreferencesTuple[number];
|
||||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -1409,6 +1409,16 @@
|
||||||
tslib "2.3.1"
|
tslib "2.3.1"
|
||||||
uuid "8.3.2"
|
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":
|
"@nestjs/core@^8.4.0":
|
||||||
version "8.4.0"
|
version "8.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.0.tgz#f4c1840b9b233e3985407f496b7c1e78e0169c70"
|
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"
|
domelementtype "^2.2.0"
|
||||||
domhandler "^4.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:
|
dotenv@^8.2.0:
|
||||||
version "8.6.0"
|
version "8.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
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"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
Loading…
Reference in a new issue