Merge branch 'dev'
This commit is contained in:
commit
26ed0e9ea5
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
|
@ -2,8 +2,8 @@ nodeLinker: node-modules
|
|||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
spec: '@yarnpkg/plugin-version'
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
spec: '@yarnpkg/plugin-workspace-tools'
|
||||
|
||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
||||
|
|
18
README.md
18
README.md
|
@ -19,7 +19,7 @@ But it does function, so feel free to give it a try.
|
|||
|
||||
## Demo
|
||||
|
||||
You can view a live demo here: <https://picsur.rubikscraft.nl/>
|
||||
You can view a live demo here: <https://picsur.org/>
|
||||
|
||||
The images are deleted every five minutes, and the maximum filesize is 16MB. But it should give you an indication of how it works.
|
||||
|
||||
|
@ -57,9 +57,9 @@ Every featured marked here should work in the latest release.
|
|||
- [x] Proper DB migrations
|
||||
- [x] Show own images in list
|
||||
- [x] Correct previews on chats
|
||||
- [X] Expiring images
|
||||
- [X] ShareX endpoint
|
||||
- [X] Arm64 image
|
||||
- [x] Expiring images
|
||||
- [x] ShareX endpoint
|
||||
- [x] ARM64 and AMD64 Docker image
|
||||
|
||||
- [ ] White mode
|
||||
- [ ] Public gallery
|
||||
|
@ -74,6 +74,12 @@ If you like this project, don't forget to give it a star. It tells me that I'm n
|
|||
|
||||
## Faq
|
||||
|
||||
### Is this project maintained?
|
||||
|
||||
Yes it still is. If I were to stop maintaining it, I would archive the repository.
|
||||
|
||||
However I do not have a lot of time on my hands, so updates are not always as frequent as I would like them to be.
|
||||
|
||||
### Why do my images dissapear of the public instance?
|
||||
|
||||
The public instance is only a demo, and therefore only keeps images for 5 minutes. This is to prevent the server from running out of disk space, and to prevent people from using it to host questionable images.
|
||||
|
@ -150,6 +156,10 @@ volumes:
|
|||
picsur-data:
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
- @awg13 for donating 5$
|
||||
|
||||
## Api
|
||||
|
||||
Here is a usually up to date documentation of the api:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "picsur-backend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"description": "Backend for Picsur",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/rubikscraft/Picsur",
|
||||
|
@ -22,65 +22,69 @@
|
|||
"purge": "rm -rf dist && rm -rf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/helmet": "^10.0.1",
|
||||
"@fastify/multipart": "^7.2.0",
|
||||
"@fastify/static": "^6.5.0",
|
||||
"@nestjs/common": "^9.1.4",
|
||||
"@fastify/helmet": "^10.1.0",
|
||||
"@fastify/multipart": "^7.3.0",
|
||||
"@fastify/reply-from": "^8.3.1",
|
||||
"@fastify/static": "^6.6.0",
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.1.4",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-fastify": "^9.1.4",
|
||||
"@nestjs/platform-fastify": "^9.2.1",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
"@nestjs/serve-static": "^3.0.0",
|
||||
"@nestjs/throttler": "^3.1.0",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bmp-img": "^1.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"file-type": "^18.0.0",
|
||||
"is-docker": "^3.0.0",
|
||||
"ms": "^2.1.3",
|
||||
"node-fetch": "^3.2.10",
|
||||
"p-timeout": "^6.0.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"pg": "^8.8.0",
|
||||
"picsur-shared": "*",
|
||||
"posix.js": "^0.1.1",
|
||||
"qoi-img": "^2.1.0",
|
||||
"qoi-img": "^2.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.7",
|
||||
"sharp": "^0.31.1",
|
||||
"rxjs": "^7.8.0",
|
||||
"sharp": "^0.31.3",
|
||||
"stream-parser": "^0.3.1",
|
||||
"thunks": "^4.9.6",
|
||||
"typeorm": "0.3.10",
|
||||
"zod": "^3.19.1"
|
||||
"typeorm": "0.3.11",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.4",
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@nestjs/testing": "^9.1.4",
|
||||
"@nestjs/testing": "^9.2.1",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.8.4",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@types/passport-strategy": "^0.2.35",
|
||||
"@types/sharp": "^0.31.0",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint": "^8.25.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "4.8.4"
|
||||
"tsconfig-paths": "^4.1.1",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import cors from 'cors';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import { EarlyConfigModule } from './config/early/early-config.module';
|
||||
import { ServeStaticConfigService } from './config/early/serve-static.config.service';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { PicsurLayersModule } from './layers/PicsurLayers.module';
|
||||
import { PicsurLoggerModule } from './logger/logger.module';
|
||||
import { AuthManagerModule } from './managers/auth/auth.module';
|
||||
import { DemoManagerModule } from './managers/demo/demo.module';
|
||||
import { UsageManagerModule } from './managers/usage/usage.module';
|
||||
import { PicsurRoutesModule } from './routes/routes.module';
|
||||
|
||||
const mainCorsConfig = cors({
|
||||
|
@ -41,10 +44,13 @@ const imageCorsOverride = (
|
|||
useExisting: ServeStaticConfigService,
|
||||
imports: [EarlyConfigModule],
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
DatabaseModule,
|
||||
AuthManagerModule,
|
||||
UsageManagerModule,
|
||||
DemoManagerModule,
|
||||
PicsurRoutesModule,
|
||||
PicsurLayersModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { FindResult } from 'picsur-shared/dist/types/find-result';
|
|||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { Repository } from 'typeorm';
|
||||
import { EApiKeyBackend } from '../../database/entities/apikey.entity';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyDbService {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/image.entity';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/images/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/images/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
import { ImageDBService } from './image-db.service';
|
||||
import { ImageFileDBService } from './image-file-db.service';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
|||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { In, LessThan, Repository } from 'typeorm';
|
||||
import { EImageBackend } from '../../database/entities/image.entity';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ImageDBService {
|
||||
|
@ -83,6 +83,14 @@ export class ImageDBService {
|
|||
}
|
||||
}
|
||||
|
||||
public async count(): AsyncFailable<number> {
|
||||
try {
|
||||
return await this.imageRepo.count();
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async update(
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { LessThan, Repository } from 'typeorm';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/image-file.entity';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/images/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/images/image-file.entity';
|
||||
|
||||
const A_DAY_IN_SECONDS = 24 * 60 * 60;
|
||||
|
||||
|
@ -57,6 +57,40 @@ export class ImageFileDBService {
|
|||
}
|
||||
}
|
||||
|
||||
public async migrateFile(
|
||||
imageId: string,
|
||||
sourceVariant: ImageEntryVariant,
|
||||
targetVariant: ImageEntryVariant,
|
||||
): AsyncFailable<EImageFileBackend> {
|
||||
try {
|
||||
const sourceFile = await this.getFile(imageId, sourceVariant);
|
||||
if (HasFailed(sourceFile)) return sourceFile;
|
||||
|
||||
sourceFile.variant = targetVariant;
|
||||
return await this.imageFileRepo.save(sourceFile);
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteFile(
|
||||
imageId: string,
|
||||
variant: ImageEntryVariant,
|
||||
): AsyncFailable<EImageFileBackend> {
|
||||
try {
|
||||
const found = await this.imageFileRepo.findOne({
|
||||
where: { image_id: imageId, variant: variant },
|
||||
});
|
||||
|
||||
if (!found) return Fail(FT.NotFound, 'Image not found');
|
||||
|
||||
await this.imageFileRepo.delete({ image_id: imageId, variant: variant });
|
||||
return found;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
// This is useful because you dont have to pull the whole image file
|
||||
public async getFileTypes(
|
||||
imageId: string,
|
||||
|
@ -129,7 +163,7 @@ export class ImageFileDBService {
|
|||
): AsyncFailable<number> {
|
||||
try {
|
||||
const result = await this.imageDerivativeRepo.delete({
|
||||
last_read: LessThan(new Date()),
|
||||
last_read: LessThan(new Date(Date.now() - olderThanSeconds * 1000)),
|
||||
});
|
||||
|
||||
return result.affected ?? 0;
|
||||
|
|
|
@ -17,8 +17,8 @@ type EnumValue<E> = E[keyof E];
|
|||
type PrefValueTypeType<E extends Enum> = {
|
||||
[key in EnumValue<E>]: PrefValueTypeStrings;
|
||||
};
|
||||
type EncodedPref = {
|
||||
key: string;
|
||||
type EncodedPref<E extends Enum> = {
|
||||
key: EnumValue<E>;
|
||||
value: string;
|
||||
};
|
||||
|
||||
|
@ -32,7 +32,7 @@ export class PreferenceCommonService {
|
|||
// E is either the SysPreference or the UsrPreference enum
|
||||
// the pref value types is the object containing the type of each key in E
|
||||
public DecodePref<E extends Enum>(
|
||||
preference: EncodedPref,
|
||||
preference: EncodedPref<E>,
|
||||
prefType: E,
|
||||
prefValueTypes: PrefValueTypeType<E>,
|
||||
): Failable<DecodedPref> {
|
||||
|
@ -69,7 +69,7 @@ export class PreferenceCommonService {
|
|||
value: PrefValueType,
|
||||
prefType: E,
|
||||
prefValueTypes: PrefValueTypeType<E>,
|
||||
): AsyncFailable<EncodedPref> {
|
||||
): AsyncFailable<EncodedPref<E>> {
|
||||
const validatedKey = this.validatePrefKey(key, prefType);
|
||||
if (HasFailed(validatedKey)) return validatedKey;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EarlyConfigModule } from '../../config/early/early-config.module';
|
||||
import { ESysPreferenceBackend } from '../../database/entities/sys-preference.entity';
|
||||
import { EUsrPreferenceBackend } from '../../database/entities/usr-preference.entity';
|
||||
import { ESysPreferenceBackend } from '../../database/entities/system/sys-preference.entity';
|
||||
import { EUsrPreferenceBackend } from '../../database/entities/system/usr-preference.entity';
|
||||
import { PreferenceCommonService } from './preference-common.service';
|
||||
import { PreferenceDefaultsService } from './preference-defaults.service';
|
||||
import { SysPreferenceDbService } from './sys-preference-db.service';
|
||||
|
|
|
@ -15,15 +15,17 @@ export class PreferenceDefaultsService {
|
|||
|
||||
constructor(private readonly jwtConfigService: EarlyJwtConfigService) {}
|
||||
|
||||
public readonly usrDefaults: {
|
||||
[key in UsrPreference]: () => PrefValueType;
|
||||
private readonly usrDefaults: {
|
||||
[key in UsrPreference]: (() => PrefValueType) | PrefValueType;
|
||||
} = {
|
||||
[UsrPreference.KeepOriginal]: () => false,
|
||||
[UsrPreference.KeepOriginal]: false,
|
||||
};
|
||||
|
||||
public readonly sysDefaults: {
|
||||
[key in SysPreference]: () => PrefValueType;
|
||||
private readonly sysDefaults: {
|
||||
[key in SysPreference]: (() => PrefValueType) | PrefValueType;
|
||||
} = {
|
||||
[SysPreference.HostOverride]: '',
|
||||
|
||||
[SysPreference.JwtSecret]: () => {
|
||||
const envSecret = this.jwtConfigService.getJwtSecret();
|
||||
if (envSecret) {
|
||||
|
@ -37,13 +39,36 @@ export class PreferenceDefaultsService {
|
|||
},
|
||||
[SysPreference.JwtExpiresIn]: () =>
|
||||
this.jwtConfigService.getJwtExpiresIn() ?? '7d',
|
||||
[SysPreference.BCryptStrength]: () => 12,
|
||||
[SysPreference.BCryptStrength]: 10,
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: () => '7d',
|
||||
[SysPreference.SaveDerivatives]: () => true,
|
||||
[SysPreference.AllowEditing]: () => true,
|
||||
[SysPreference.RemoveDerivativesAfter]: '7d',
|
||||
[SysPreference.AllowEditing]: true,
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: () => '10s',
|
||||
[SysPreference.ConversionMemoryLimit]: () => 512,
|
||||
[SysPreference.ConversionTimeLimit]: '15s',
|
||||
[SysPreference.ConversionMemoryLimit]: 512,
|
||||
|
||||
[SysPreference.EnableTracking]: false,
|
||||
[SysPreference.TrackingUrl]: '',
|
||||
[SysPreference.TrackingId]: '',
|
||||
|
||||
[SysPreference.EnableTelemetry]: true,
|
||||
};
|
||||
|
||||
public getSysDefault(pref: SysPreference): PrefValueType {
|
||||
const value = this.sysDefaults[pref];
|
||||
if (typeof value === 'function') {
|
||||
return value();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public getUsrDefault(pref: UsrPreference): PrefValueType {
|
||||
const value = this.usrDefaults[pref];
|
||||
if (typeof value === 'function') {
|
||||
return value();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,18 @@ import {
|
|||
PrefValueType,
|
||||
PrefValueTypeStrings,
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import {
|
||||
SysPreference,
|
||||
SysPreferenceList,
|
||||
SysPreferenceValidators,
|
||||
SysPreferenceValueTypes,
|
||||
} from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
ESysPreferenceBackend,
|
||||
ESysPreferenceSchema,
|
||||
} from '../../database/entities/sys-preference.entity';
|
||||
import {
|
||||
SysPreferenceList,
|
||||
SysPreferenceValueTypes,
|
||||
} from '../../models/constants/syspreferences.const';
|
||||
} from '../../database/entities/system/sys-preference.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { PreferenceCommonService } from './preference-common.service';
|
||||
import { PreferenceDefaultsService } from './preference-defaults.service';
|
||||
|
@ -85,7 +86,7 @@ export class SysPreferenceDbService {
|
|||
|
||||
// Return
|
||||
return this.prefCommon.DecodePref(
|
||||
result.data,
|
||||
result.data as any,
|
||||
SysPreference,
|
||||
SysPreferenceValueTypes,
|
||||
);
|
||||
|
@ -136,7 +137,7 @@ export class SysPreferenceDbService {
|
|||
private async saveDefault(
|
||||
key: SysPreference, // Force enum here because we dont validate
|
||||
): AsyncFailable<DecodedSysPref> {
|
||||
return this.setPreference(key, this.defaultsService.sysDefaults[key]());
|
||||
return this.setPreference(key, this.defaultsService.getSysDefault(key));
|
||||
}
|
||||
|
||||
private async encodeSysPref(
|
||||
|
@ -151,6 +152,12 @@ export class SysPreferenceDbService {
|
|||
);
|
||||
if (HasFailed(validated)) return validated;
|
||||
|
||||
const valueValidated =
|
||||
SysPreferenceValidators[key as SysPreference].safeParse(value);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
let verifySysPreference = new ESysPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
verifySysPreference.value = validated.value;
|
||||
|
|
|
@ -5,17 +5,18 @@ import {
|
|||
PrefValueType,
|
||||
PrefValueTypeStrings,
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import {
|
||||
UsrPreference,
|
||||
UsrPreferenceList,
|
||||
UsrPreferenceValidators,
|
||||
UsrPreferenceValueTypes,
|
||||
} from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
EUsrPreferenceBackend,
|
||||
EUsrPreferenceSchema,
|
||||
} from '../../database/entities/usr-preference.entity';
|
||||
import {
|
||||
UsrPreferenceList,
|
||||
UsrPreferenceValueTypes,
|
||||
} from '../../models/constants/usrpreferences.const';
|
||||
} from '../../database/entities/system/usr-preference.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { PreferenceCommonService } from './preference-common.service';
|
||||
import { PreferenceDefaultsService } from './preference-defaults.service';
|
||||
|
@ -91,7 +92,7 @@ export class UsrPreferenceDbService {
|
|||
|
||||
// Return
|
||||
const unpacked = this.prefCommon.DecodePref(
|
||||
result.data,
|
||||
result.data as any,
|
||||
UsrPreference,
|
||||
UsrPreferenceValueTypes,
|
||||
);
|
||||
|
@ -175,7 +176,7 @@ export class UsrPreferenceDbService {
|
|||
return this.setPreference(
|
||||
userid,
|
||||
key,
|
||||
this.defaultsService.usrDefaults[key](),
|
||||
this.defaultsService.getUsrDefault(key),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -192,6 +193,12 @@ export class UsrPreferenceDbService {
|
|||
);
|
||||
if (HasFailed(validated)) return validated;
|
||||
|
||||
const valueValidated =
|
||||
UsrPreferenceValidators[key as UsrPreference].safeParse(value);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
let verifySysPreference = new EUsrPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
verifySysPreference.value = validated.value;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { EarlyConfigModule } from '../../config/early/early-config.module';
|
||||
import { HostConfigService } from '../../config/early/host.config.service';
|
||||
import { ERoleBackend } from '../../database/entities/role.entity';
|
||||
import { ERoleBackend } from '../../database/entities/users/role.entity';
|
||||
import {
|
||||
ImmutableRolesList,
|
||||
SystemRoleDefaults,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from 'picsur-shared/dist/types';
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { ERoleBackend } from '../../database/entities/role.entity';
|
||||
import { ERoleBackend } from '../../database/entities/users/role.entity';
|
||||
import { Permissions } from '../../models/constants/permissions.const';
|
||||
import {
|
||||
ImmutableRolesList,
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ESystemStateBackend } from '../../database/entities/system/system-state.entity';
|
||||
import { SystemStateDbService } from './system-state-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ESystemStateBackend])],
|
||||
providers: [SystemStateDbService],
|
||||
exports: [SystemStateDbService],
|
||||
})
|
||||
export class SystemStateDbModule {}
|
|
@ -0,0 +1,42 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ESystemStateBackend } from '../../database/entities/system/system-state.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SystemStateDbService {
|
||||
private readonly logger = new Logger(SystemStateDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ESystemStateBackend)
|
||||
private readonly stateRepo: Repository<ESystemStateBackend>,
|
||||
) {}
|
||||
|
||||
async get(key: string): AsyncFailable<string | null> {
|
||||
try {
|
||||
const state = await this.stateRepo.findOne({ where: { key } });
|
||||
return state?.value ?? null;
|
||||
} catch (err) {
|
||||
return Fail(FT.Database, err);
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: string): AsyncFailable<true> {
|
||||
try {
|
||||
await this.stateRepo.save({ key, value });
|
||||
return true;
|
||||
} catch (err) {
|
||||
return Fail(FT.Database, err);
|
||||
}
|
||||
}
|
||||
|
||||
async clear(key: string): AsyncFailable<true> {
|
||||
try {
|
||||
await this.stateRepo.delete({ key });
|
||||
return true;
|
||||
} catch (err) {
|
||||
return Fail(FT.Database, err);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { HasFailed } from 'picsur-shared/dist/types';
|
|||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { AuthConfigService } from '../../config/early/auth.config.service';
|
||||
import { EarlyConfigModule } from '../../config/early/early-config.module';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
import { PreferenceDbModule } from '../preference-db/preference-db.module';
|
||||
import { RoleDbModule } from '../role-db/role-db.module';
|
||||
import { UserDbService } from './user-db.service';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { Repository } from 'typeorm';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
import { Permissions } from '../../models/constants/permissions.const';
|
||||
import {
|
||||
DefaultRolesList,
|
||||
|
@ -263,6 +263,14 @@ export class UserDbService {
|
|||
}
|
||||
}
|
||||
|
||||
public async count(): AsyncFailable<number> {
|
||||
try {
|
||||
return await this.usersRepository.count();
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async exists(username: string): Promise<boolean> {
|
||||
return HasSuccess(await this.findByUsername(username));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export const ReportUrl = 'https://metrics.picsur.org';
|
||||
export const ReportInterval = 1000 * 60 * 60;
|
||||
export const EnvPrefix = 'PICSUR_';
|
||||
export const DefaultName = 'picsur';
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AuthConfigService } from './auth.config.service';
|
|||
import { EarlyJwtConfigService } from './early-jwt.config.service';
|
||||
import { HostConfigService } from './host.config.service';
|
||||
import { MultipartConfigService } from './multipart.config.service';
|
||||
import { RedisConfigService } from './redis.config.service';
|
||||
import { ServeStaticConfigService } from './serve-static.config.service';
|
||||
import { TypeOrmConfigService } from './type-orm.config.service';
|
||||
|
||||
|
@ -21,6 +22,7 @@ import { TypeOrmConfigService } from './type-orm.config.service';
|
|||
HostConfigService,
|
||||
AuthConfigService,
|
||||
MultipartConfigService,
|
||||
RedisConfigService,
|
||||
],
|
||||
exports: [
|
||||
ConfigModule,
|
||||
|
@ -30,6 +32,7 @@ import { TypeOrmConfigService } from './type-orm.config.service';
|
|||
HostConfigService,
|
||||
AuthConfigService,
|
||||
MultipartConfigService,
|
||||
RedisConfigService,
|
||||
],
|
||||
})
|
||||
export class EarlyConfigModule {}
|
||||
|
|
|
@ -16,8 +16,15 @@ export class HostConfigService {
|
|||
this.logger.log('Verbose: ' + this.isVerbose());
|
||||
this.logger.log('Host: ' + this.getHost());
|
||||
this.logger.log('Port: ' + this.getPort());
|
||||
this.logger.log('Demo: ' + this.isDemo());
|
||||
this.logger.log('Demo Interval: ' + this.getDemoInterval() / 1000 + 's');
|
||||
|
||||
if (this.isDemo()) {
|
||||
this.logger.log('Running in demo mode');
|
||||
this.logger.log('Demo Interval: ' + this.getDemoInterval() / 1000 + 's');
|
||||
}
|
||||
|
||||
if (!this.isTelemetry()) {
|
||||
this.logger.log('Telemetry disabled');
|
||||
}
|
||||
}
|
||||
|
||||
public getHost(): string {
|
||||
|
@ -47,6 +54,10 @@ export class HostConfigService {
|
|||
return ParseBool(this.configService.get(`${EnvPrefix}VERBOSE`), false);
|
||||
}
|
||||
|
||||
public isTelemetry() {
|
||||
return ParseBool(this.configService.get(`${EnvPrefix}TELEMETRY`), true);
|
||||
}
|
||||
|
||||
public getVersion() {
|
||||
return ParseString(this.configService.get(`npm_package_version`), '0.0.0');
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ export class MultipartConfigService {
|
|||
);
|
||||
}
|
||||
|
||||
public getLimits() {
|
||||
public getLimits(fileLimit?: number) {
|
||||
return {
|
||||
fieldNameSize: 128,
|
||||
fieldSize: 1024,
|
||||
fields: 16,
|
||||
files: 16,
|
||||
fields: 20,
|
||||
files: fileLimit ?? 20,
|
||||
fileSize: this.getMaxFileSize(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ParseString } from 'picsur-shared/dist/util/parse-simple';
|
||||
import { EnvPrefix } from '../config.static';
|
||||
|
||||
@Injectable()
|
||||
export class RedisConfigService {
|
||||
private readonly logger = new Logger(RedisConfigService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.logger.log('Redis URL: ' + this.getRedisUrl());
|
||||
}
|
||||
|
||||
public getRedisUrl(): string {
|
||||
return ParseString(
|
||||
this.configService.get(`${EnvPrefix}REDIS_URL`),
|
||||
'redis://localhost:6379',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
|
||||
@Injectable()
|
||||
export class InfoConfigService {
|
||||
private readonly logger = new Logger(InfoConfigService.name);
|
||||
|
||||
constructor(private readonly prefService: SysPreferenceDbService) {}
|
||||
|
||||
public async getHostnameOverride(): Promise<string | undefined> {
|
||||
const hostname = await this.prefService.getStringPreference(
|
||||
SysPreference.HostOverride,
|
||||
);
|
||||
if (HasFailed(hostname)) {
|
||||
hostname.print(this.logger);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hostname === '') return undefined;
|
||||
|
||||
return hostname;
|
||||
}
|
||||
}
|
|
@ -32,8 +32,8 @@ export class JwtConfigService implements JwtOptionsFactory {
|
|||
await this.prefService.getStringPreference('jwt_expires_in'),
|
||||
);
|
||||
|
||||
let milliseconds = ms(expiresIn);
|
||||
if (milliseconds === undefined) {
|
||||
let milliseconds = ms(expiresIn as any);
|
||||
if (isNaN(milliseconds)) {
|
||||
milliseconds = 1000 * 60 * 60 * 24; // 1 day
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ import { PreferenceDbModule } from '../../collections/preference-db/preference-d
|
|||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { EarlyConfigModule } from '../early/early-config.module';
|
||||
import { EarlyJwtConfigService } from '../early/early-jwt.config.service';
|
||||
import { InfoConfigService } from './info.config.service';
|
||||
import { JwtConfigService } from './jwt.config.service';
|
||||
import { UsageConfigService } from './usage.config.service';
|
||||
|
||||
// This module contains all configservices that depend on the syspref module
|
||||
// The syspref module can only be used when connected to the database
|
||||
|
@ -11,9 +13,14 @@ import { JwtConfigService } from './jwt.config.service';
|
|||
// Otherwise we will create a circular depedency
|
||||
|
||||
@Module({
|
||||
imports: [PreferenceDbModule, EarlyConfigModule],
|
||||
providers: [JwtConfigService],
|
||||
exports: [JwtConfigService, EarlyConfigModule],
|
||||
imports: [EarlyConfigModule, PreferenceDbModule],
|
||||
providers: [JwtConfigService, InfoConfigService, UsageConfigService],
|
||||
exports: [
|
||||
EarlyConfigModule,
|
||||
JwtConfigService,
|
||||
InfoConfigService,
|
||||
UsageConfigService,
|
||||
],
|
||||
})
|
||||
export class LateConfigModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(LateConfigModule.name);
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { URLRegex, UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { ReportInterval, ReportUrl } from '../config.static';
|
||||
|
||||
@Injectable()
|
||||
export class UsageConfigService {
|
||||
constructor(private readonly sysPref: SysPreferenceDbService) {}
|
||||
|
||||
async getTrackingUrl(): AsyncFailable<string | null> {
|
||||
const trackingUrl = await this.sysPref.getStringPreference(
|
||||
SysPreference.TrackingUrl,
|
||||
);
|
||||
if (HasFailed(trackingUrl)) return trackingUrl;
|
||||
|
||||
if (trackingUrl === '') return null;
|
||||
|
||||
if (!URLRegex.test(trackingUrl)) {
|
||||
return Fail(FT.UsrValidation, undefined, 'Invalid tracking URL');
|
||||
}
|
||||
|
||||
return trackingUrl;
|
||||
}
|
||||
|
||||
async getTrackingID(): AsyncFailable<string | null> {
|
||||
const trackingID = await this.sysPref.getStringPreference(
|
||||
SysPreference.TrackingId,
|
||||
);
|
||||
if (HasFailed(trackingID)) return trackingID;
|
||||
|
||||
if (trackingID === '') return null;
|
||||
|
||||
if (!UUIDRegex.test(trackingID)) {
|
||||
return Fail(FT.UsrValidation, undefined, 'Invalid tracking ID');
|
||||
}
|
||||
|
||||
return trackingID;
|
||||
}
|
||||
|
||||
async getMetricsEnabled(): AsyncFailable<boolean> {
|
||||
return this.sysPref.getBooleanPreference(SysPreference.EnableTelemetry);
|
||||
}
|
||||
|
||||
async getMetricsInterval(): Promise<number> {
|
||||
return ReportInterval;
|
||||
}
|
||||
|
||||
async getMetricsUrl(): Promise<string> {
|
||||
return ReportUrl;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import {
|
|||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
import { EUserBackend } from './user.entity';
|
||||
import { EUserBackend } from './users/user.entity';
|
||||
|
||||
const OverriddenEApiKeySchema = EApiKeySchema.omit({ user: true }).merge(
|
||||
z.object({
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { EApiKeyBackend } from './apikey.entity';
|
||||
import { EImageDerivativeBackend } from './image-derivative.entity';
|
||||
import { EImageFileBackend } from './image-file.entity';
|
||||
import { EImageBackend } from './image.entity';
|
||||
import { ERoleBackend } from './role.entity';
|
||||
import { ESysPreferenceBackend } from './sys-preference.entity';
|
||||
import { EUserBackend } from './user.entity';
|
||||
import { EUsrPreferenceBackend } from './usr-preference.entity';
|
||||
import { EImageDerivativeBackend } from './images/image-derivative.entity';
|
||||
import { EImageFileBackend } from './images/image-file.entity';
|
||||
import { EImageBackend } from './images/image.entity';
|
||||
import { ESysPreferenceBackend } from './system/sys-preference.entity';
|
||||
import { ESystemStateBackend } from './system/system-state.entity';
|
||||
import { EUsrPreferenceBackend } from './system/usr-preference.entity';
|
||||
import { ERoleBackend } from './users/role.entity';
|
||||
import { EUserBackend } from './users/user.entity';
|
||||
|
||||
export const EntityList = [
|
||||
EImageBackend,
|
||||
|
@ -16,4 +17,5 @@ export const EntityList = [
|
|||
ESysPreferenceBackend,
|
||||
EUsrPreferenceBackend,
|
||||
EApiKeyBackend,
|
||||
ESystemStateBackend,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class ESystemStateBackend {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id?: string;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false, unique: true })
|
||||
key: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
value: string;
|
||||
}
|
|
@ -9,7 +9,7 @@ import {
|
|||
Unique,
|
||||
} from 'typeorm';
|
||||
import z from 'zod';
|
||||
import { EUserBackend } from './user.entity';
|
||||
import { EUserBackend } from '../users/user.entity';
|
||||
|
||||
export const EUsrPreferenceSchema = z.object({
|
||||
id: IsEntityID().optional(),
|
|
@ -1,6 +1,6 @@
|
|||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import type { Permissions } from '../../models/constants/permissions.const';
|
||||
import type { Permissions } from '../../../models/constants/permissions.const';
|
||||
|
||||
@Entity()
|
||||
export class ERoleBackend implements ERole {
|
|
@ -7,8 +7,8 @@ import {
|
|||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
import { EApiKeyBackend } from './apikey.entity';
|
||||
import { EUsrPreferenceBackend } from './usr-preference.entity';
|
||||
import { EApiKeyBackend } from '../apikey.entity';
|
||||
import { EUsrPreferenceBackend } from '../system/usr-preference.entity';
|
||||
|
||||
// Different data for public and private
|
||||
const OverriddenEUserSchema = EUserSchema.omit({ hashedPassword: true }).merge(
|
|
@ -0,0 +1,16 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class V050A1672154027079 implements MigrationInterface {
|
||||
name = 'V050A1672154027079'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "e_system_state_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_f11f1605928b497b24f4b3ecc1f" UNIQUE ("key"), CONSTRAINT "PK_097ea165dadc8c14237481afd64" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f11f1605928b497b24f4b3ecc1" ON "e_system_state_backend" ("key") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f11f1605928b497b24f4b3ecc1"`);
|
||||
await queryRunner.query(`DROP TABLE "e_system_state_backend"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import { V040A1662314197741 } from './1662314197741-V_0_4_0_a';
|
|||
import { V040B1662485374471 } from './1662485374471-V_0_4_0_b';
|
||||
import { V040C1662535484200 } from './1662535484200-V_0_4_0_c';
|
||||
import { V040D1662728275448 } from './1662728275448-V_0_4_0_d';
|
||||
import { V050A1672154027079 } from './1672154027079-V_0_5_0_a';
|
||||
|
||||
export const MigrationList: Function[] = [
|
||||
V030A1661692206479,
|
||||
|
@ -12,4 +13,5 @@ export const MigrationList: Function[] = [
|
|||
V040B1662485374471,
|
||||
V040C1662535484200,
|
||||
V040D1662728275448,
|
||||
V050A1672154027079,
|
||||
];
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EarlyConfigModule } from '../config/early/early-config.module';
|
||||
import { ImageIdPipe } from './image-id/image-id.pipe';
|
||||
import { MultiPartPipe } from './multipart/multipart.pipe';
|
||||
import { PostFilePipe } from './multipart/postfile.pipe';
|
||||
import { MultiPartPipe } from './multipart/postfiles.pipe';
|
||||
|
||||
@Module({
|
||||
imports: [EarlyConfigModule],
|
||||
|
|
|
@ -3,6 +3,9 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|||
// Since pipes dont have direct access to the request object, we need this decorator to inject it
|
||||
export const InjectRequest = createParamDecorator(
|
||||
async (data: any, ctx: ExecutionContext) => {
|
||||
return ctx.switchToHttp().getRequest();
|
||||
return {
|
||||
data: data,
|
||||
request: ctx.switchToHttp().getRequest(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { InjectRequest } from './inject-request.decorator';
|
||||
import { MultiPartPipe } from './multipart.pipe';
|
||||
import { PostFilePipe } from './postfile.pipe';
|
||||
import { MultiPartPipe } from './postfiles.pipe';
|
||||
|
||||
export const PostFile = () => InjectRequest(PostFilePipe);
|
||||
|
||||
export const MultiPart = () => InjectRequest(MultiPartPipe);
|
||||
export const PostFiles = (maxFiles?: number) =>
|
||||
InjectRequest(maxFiles, MultiPartPipe);
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import { MultipartFields, MultipartFile } from '@fastify/multipart';
|
||||
import {
|
||||
ArgumentMetadata,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
||||
import { MultipartConfigService } from '../../config/early/multipart.config.service';
|
||||
import {
|
||||
CreateMultiPartFieldDto,
|
||||
CreateMultiPartFileDto,
|
||||
} from '../../models/dto/multipart.dto';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class MultiPartPipe implements PipeTransform {
|
||||
private readonly logger = new Logger(MultiPartPipe.name);
|
||||
|
||||
constructor(
|
||||
private readonly multipartConfigService: MultipartConfigService,
|
||||
) {}
|
||||
|
||||
async transform<T extends Object>(
|
||||
req: FastifyRequest,
|
||||
metadata: ArgumentMetadata,
|
||||
) {
|
||||
let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
|
||||
if (!zodSchema) {
|
||||
this.logger.error('Invalid scheme on multipart body');
|
||||
throw Fail(FT.Internal, 'Invalid scheme on backend');
|
||||
}
|
||||
|
||||
let multipartData = {};
|
||||
if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
|
||||
// Fetch all fields from the request
|
||||
let fields: MultipartFields | null = null;
|
||||
try {
|
||||
fields =
|
||||
(
|
||||
await req.file({
|
||||
limits: this.multipartConfigService.getLimits(),
|
||||
})
|
||||
)?.fields ?? null;
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
}
|
||||
if (!fields) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
|
||||
// Loop over every formfield that was sent
|
||||
for (const key of Object.keys(fields)) {
|
||||
// Ignore duplicate fields
|
||||
if (Array.isArray(fields[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the value property to differentiate between a field and a file
|
||||
// And then put the value into the correct property on the validatable class
|
||||
if ((fields[key] as any).value) {
|
||||
(multipartData as any)[key] = CreateMultiPartFieldDto(
|
||||
fields[key] as MultipartFile,
|
||||
);
|
||||
} else {
|
||||
const file = await CreateMultiPartFileDto(fields[key] as MultipartFile);
|
||||
if (HasFailed(file)) throw file;
|
||||
(multipartData as any)[key] = file;
|
||||
}
|
||||
}
|
||||
|
||||
// Now validate the class we made, if any properties were invalid, it will error here
|
||||
const result = zodSchema.safeParse(multipartData);
|
||||
if (!result.success) {
|
||||
this.logger.warn(result.error);
|
||||
throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Multipart, MultipartFields, MultipartFile } from '@fastify/multipart';
|
||||
import { Multipart, MultipartFile } from '@fastify/multipart';
|
||||
import { Injectable, Logger, PipeTransform, Scope } from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
|
@ -12,11 +12,11 @@ export class PostFilePipe implements PipeTransform {
|
|||
private readonly multipartConfigService: MultipartConfigService,
|
||||
) {}
|
||||
|
||||
async transform({ req }: { req: FastifyRequest }) {
|
||||
if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
async transform({ request, data }: { data: any; request: FastifyRequest }) {
|
||||
if (!request.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
|
||||
// Only one file is allowed
|
||||
const file = await req.file({
|
||||
const file = await request.file({
|
||||
limits: {
|
||||
...this.multipartConfigService.getLimits(),
|
||||
files: 1,
|
||||
|
@ -30,13 +30,15 @@ export class PostFilePipe implements PipeTransform {
|
|||
) as any;
|
||||
|
||||
// Remove non-file fields
|
||||
const files: MultipartFile[] = allFields.filter((entry) => (entry as any).file !== undefined) as MultipartFile[];
|
||||
const files: MultipartFile[] = allFields.filter(
|
||||
(entry) => (entry as any).file !== undefined,
|
||||
) as MultipartFile[];
|
||||
|
||||
if (files.length !== 1) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
|
||||
// Return a buffer of the file
|
||||
try {
|
||||
return await files[0]?.toBuffer();
|
||||
return await files[0].toBuffer();
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
throw Fail(FT.Internal, 'Invalid file');
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { MultipartFile } from '@fastify/multipart';
|
||||
import {
|
||||
ArgumentMetadata,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { MultipartConfigService } from '../../config/early/multipart.config.service';
|
||||
|
||||
export type FileIterator = AsyncIterableIterator<MultipartFile>;
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class MultiPartPipe implements PipeTransform {
|
||||
private readonly logger = new Logger(MultiPartPipe.name);
|
||||
|
||||
constructor(
|
||||
private readonly multipartConfigService: MultipartConfigService,
|
||||
) {}
|
||||
|
||||
async transform<T extends Object>(
|
||||
{ request, data }: { data: any; request: FastifyRequest },
|
||||
metadata: ArgumentMetadata,
|
||||
) {
|
||||
const filesLimit = typeof data === 'number' ? data : undefined;
|
||||
|
||||
if (!request.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid files');
|
||||
|
||||
const files = request.files({
|
||||
limits: this.multipartConfigService.getLimits(filesLimit),
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
}
|
|
@ -16,3 +16,7 @@ export function Returns<N extends Object>(
|
|||
): ReturnsMethodDecorator<N> {
|
||||
return SetMetadata('returns', newable);
|
||||
}
|
||||
|
||||
export function ReturnsAnything(): ReturnsMethodDecorator<any> {
|
||||
return SetMetadata('noreturns', true);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { MainExceptionFilter } from './exception/exception.filter';
|
||||
import { SuccessInterceptor } from './success/success.interceptor';
|
||||
import { PicsurThrottlerGuard } from './throttler/PicsurThrottler.guard';
|
||||
import { ZodValidationPipe } from './validate/zod-validator.pipe';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ThrottlerModule.forRoot({
|
||||
ttl: 60,
|
||||
limit: 60,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
PicsurThrottlerGuard,
|
||||
MainExceptionFilter,
|
||||
SuccessInterceptor,
|
||||
ZodValidationPipe,
|
||||
],
|
||||
exports: [
|
||||
PicsurThrottlerGuard,
|
||||
MainExceptionFilter,
|
||||
SuccessInterceptor,
|
||||
ZodValidationPipe,
|
||||
],
|
||||
})
|
||||
export class PicsurLayersModule {}
|
|
@ -39,23 +39,7 @@ export class MainExceptionFilter implements ExceptionFilter {
|
|||
const status = exception.getCode();
|
||||
const type = exception.getType();
|
||||
|
||||
const message = exception.getReason();
|
||||
const logmessage =
|
||||
message +
|
||||
(exception.getDebugMessage() ? ' - ' + exception.getDebugMessage() : '');
|
||||
|
||||
if (exception.isImportant()) {
|
||||
MainExceptionFilter.logger.error(
|
||||
`${traceString} ${exception.getName()}: ${logmessage}`,
|
||||
);
|
||||
if (exception.getStack()) {
|
||||
MainExceptionFilter.logger.debug(exception.getStack());
|
||||
}
|
||||
} else {
|
||||
MainExceptionFilter.logger.warn(
|
||||
`${traceString} ${exception.getName()}: ${logmessage}`,
|
||||
);
|
||||
}
|
||||
exception.print(MainExceptionFilter.logger, { prefix: traceString });
|
||||
|
||||
const toSend: ApiErrorResponse = {
|
||||
success: false,
|
||||
|
@ -65,7 +49,7 @@ export class MainExceptionFilter implements ExceptionFilter {
|
|||
|
||||
data: {
|
||||
type,
|
||||
message,
|
||||
message: exception.getReason(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -46,10 +46,29 @@ export class SuccessInterceptor<T> implements NestInterceptor {
|
|||
return data;
|
||||
}
|
||||
}),
|
||||
map((data) => {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const response = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const traceString = `(${request.ip} -> ${request.method} ${request.url})`;
|
||||
|
||||
this.logger.verbose(
|
||||
`Handled ${traceString} with ${response.statusCode} in ${Math.ceil(
|
||||
response.getResponseTime(),
|
||||
)}ms`,
|
||||
SuccessInterceptor.name,
|
||||
);
|
||||
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private validate(context: ExecutionContext, data: unknown): unknown {
|
||||
const canReturnAnything =
|
||||
(this.reflector.get('noreturns', context.getHandler()) ?? false) === true;
|
||||
|
||||
if (canReturnAnything) return data;
|
||||
|
||||
const schemaStatic = this.reflector.get<ZodDtoStatic>(
|
||||
'returns',
|
||||
context.getHandler(),
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
|
||||
@Injectable()
|
||||
export class PicsurThrottlerGuard extends ThrottlerGuard {
|
||||
protected override throwThrottlingException(context: ExecutionContext): void {
|
||||
throw Fail(FT.RateLimit);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import fastifyHelmet from '@fastify/helmet';
|
||||
import multipart from '@fastify/multipart';
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
import fastifyReplyFrom from '@fastify/reply-from';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { AppModule } from './app.module';
|
||||
import { UserDbService } from './collections/user-db/user-db.service';
|
||||
import { HostConfigService } from './config/early/host.config.service';
|
||||
import { MainExceptionFilter } from './layers/exception/exception.filter';
|
||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||
import { PicsurThrottlerGuard } from './layers/throttler/PicsurThrottler.guard';
|
||||
import { ZodValidationPipe } from './layers/validate/zod-validator.pipe';
|
||||
import { PicsurLoggerService } from './logger/logger.service';
|
||||
import { MainAuthGuard } from './managers/auth/guards/main.guard';
|
||||
|
@ -19,10 +20,18 @@ async function bootstrap() {
|
|||
const isProduction = process.env['PICSUR_PRODUCTION'] !== undefined;
|
||||
|
||||
// Create fasify
|
||||
const fastifyAdapter = new FastifyAdapter();
|
||||
const fastifyAdapter = new FastifyAdapter({
|
||||
trustProxy: [
|
||||
'127.0.0.0/8',
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
],
|
||||
});
|
||||
// TODO: generic error messages
|
||||
await fastifyAdapter.register(multipart as any);
|
||||
await fastifyAdapter.register(fastifyHelmet as any, HelmetOptions);
|
||||
await fastifyAdapter.register(fastifyReplyFrom as any);
|
||||
|
||||
// Create nest app
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
|
@ -30,20 +39,19 @@ async function bootstrap() {
|
|||
fastifyAdapter,
|
||||
{
|
||||
bufferLogs: isProduction,
|
||||
autoFlushLogs: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Configure logger
|
||||
app.useLogger(app.get(PicsurLoggerService));
|
||||
|
||||
app.flushLogs();
|
||||
|
||||
app.useGlobalFilters(new MainExceptionFilter());
|
||||
app.useGlobalInterceptors(new SuccessInterceptor(app.get(Reflector)));
|
||||
app.useGlobalPipes(new ZodValidationPipe());
|
||||
app.useGlobalGuards(
|
||||
new MainAuthGuard(app.get(Reflector), app.get(UserDbService)),
|
||||
);
|
||||
app.useGlobalFilters(app.get(MainExceptionFilter));
|
||||
app.useGlobalInterceptors(app.get(SuccessInterceptor));
|
||||
app.useGlobalPipes(app.get(ZodValidationPipe));
|
||||
|
||||
app.useGlobalGuards(app.get(PicsurThrottlerGuard), app.get(MainAuthGuard));
|
||||
|
||||
// Start app
|
||||
const hostConfigService = app.get(HostConfigService);
|
||||
|
|
|
@ -13,7 +13,9 @@ import { AuthManagerService } from './auth.service';
|
|||
import { ApiKeyStrategy } from './guards/apikey.strategy';
|
||||
import { GuestStrategy } from './guards/guest.strategy';
|
||||
import { JwtStrategy } from './guards/jwt.strategy';
|
||||
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||
import { LocalAuthStrategy } from './guards/local-auth.strategy';
|
||||
import { MainAuthGuard } from './guards/main.guard';
|
||||
import { GuestService } from './guest.service';
|
||||
|
||||
@Module({
|
||||
|
@ -30,13 +32,15 @@ import { GuestService } from './guest.service';
|
|||
],
|
||||
providers: [
|
||||
AuthManagerService,
|
||||
GuestService,
|
||||
JwtSecretProvider,
|
||||
LocalAuthStrategy,
|
||||
JwtStrategy,
|
||||
GuestStrategy,
|
||||
JwtSecretProvider,
|
||||
ApiKeyStrategy,
|
||||
GuestService,
|
||||
LocalAuthGuard,
|
||||
MainAuthGuard,
|
||||
],
|
||||
exports: [UserDbModule, AuthManagerService],
|
||||
exports: [UserDbModule, AuthManagerService, LocalAuthGuard, MainAuthGuard],
|
||||
})
|
||||
export class AuthManagerModule {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UserDbService } from '../../collections/user-db/user-db.service';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GuestService {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { ImageDBModule } from '../../collections/image-db/image-db.module';
|
||||
import { RoleDbModule } from '../../collections/role-db/role-db.module';
|
||||
import { EarlyConfigModule } from '../../config/early/early-config.module';
|
||||
|
@ -9,13 +10,13 @@ import { DemoManagerService } from './demo.service';
|
|||
imports: [ImageDBModule, EarlyConfigModule, RoleDbModule],
|
||||
providers: [DemoManagerService],
|
||||
})
|
||||
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
||||
export class DemoManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(DemoManagerModule.name);
|
||||
private interval: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private readonly demoManagerService: DemoManagerService,
|
||||
private readonly hostConfigService: HostConfigService,
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
|
@ -27,14 +28,12 @@ export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
|||
|
||||
private async setupDemoMode() {
|
||||
this.demoManagerService.setupRoles();
|
||||
this.interval = setInterval(
|
||||
|
||||
const interval = setInterval(
|
||||
// Run demoManagerService.execute() every interval
|
||||
this.demoManagerService.execute.bind(this.demoManagerService),
|
||||
this.hostConfigService.getDemoInterval(),
|
||||
);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.interval) clearInterval(this.interval);
|
||||
this.schedulerRegistry.addInterval('demo', interval);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,8 @@ export class ImageConverterService {
|
|||
if (HasFailed(memLimit) || HasFailed(timeLimit)) {
|
||||
return Fail(FT.Internal, 'Failed to get conversion limits');
|
||||
}
|
||||
const timeLimitMS = ms(timeLimit);
|
||||
let timeLimitMS = ms(timeLimit as any);
|
||||
if (isNaN(timeLimitMS) || timeLimitMS === 0) timeLimitMS = 15 * 1000; // 15 seconds
|
||||
|
||||
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit);
|
||||
const sharpOptions: SharpOptions = {
|
||||
|
@ -121,16 +122,4 @@ export class ImageConverterService {
|
|||
filetype: targetFiletype.identifier,
|
||||
};
|
||||
}
|
||||
|
||||
private async convertAnimation(
|
||||
image: Buffer,
|
||||
targetFiletype: FileType,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageResult> {
|
||||
// Apng and gif are stored as is for now
|
||||
return {
|
||||
image: image,
|
||||
filetype: targetFiletype.identifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import ms from 'ms';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
|
@ -18,11 +19,10 @@ import { ImageManagerService } from './image.service';
|
|||
ImageProcessorService,
|
||||
ImageConverterService,
|
||||
],
|
||||
exports: [ImageManagerService],
|
||||
exports: [ImageManagerService, ImageConverterService],
|
||||
})
|
||||
export class ImageManagerModule implements OnModuleInit, OnModuleDestroy {
|
||||
export class ImageManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(ImageManagerModule.name);
|
||||
private interval: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private readonly prefManager: SysPreferenceDbService,
|
||||
|
@ -31,14 +31,10 @@ export class ImageManagerModule implements OnModuleInit, OnModuleDestroy {
|
|||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.interval = setInterval(
|
||||
// Run demoManagerService.execute() every interval
|
||||
this.imageManagerCron.bind(this),
|
||||
1000 * 60,
|
||||
);
|
||||
await this.imageManagerCron();
|
||||
}
|
||||
|
||||
@Interval(1000 * 60)
|
||||
private async imageManagerCron() {
|
||||
await this.cleanupDerivatives();
|
||||
await this.cleanupExpired();
|
||||
|
@ -53,31 +49,30 @@ export class ImageManagerModule implements OnModuleInit, OnModuleDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const after_ms = ms(remove_derivatives_after);
|
||||
if (after_ms === 0) {
|
||||
let after_ms = ms(remove_derivatives_after as any);
|
||||
if (isNaN(after_ms) || after_ms === 0) {
|
||||
this.logger.log('remove_derivatives_after is 0, skipping cron');
|
||||
return;
|
||||
}
|
||||
if (after_ms < 60000) after_ms = 60000;
|
||||
|
||||
const result = await this.imageFileDB.cleanupDerivatives(after_ms / 1000);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.warn(result.print());
|
||||
result.print(this.logger);
|
||||
}
|
||||
|
||||
this.logger.log(`Cleaned up ${result} derivatives`);
|
||||
if (result > 0) this.logger.log(`Cleaned up ${result} derivatives`);
|
||||
}
|
||||
|
||||
private async cleanupExpired() {
|
||||
const cleanedUp = await this.imageDB.cleanupExpired();
|
||||
|
||||
if (HasFailed(cleanedUp)) {
|
||||
this.logger.warn(cleanedUp.print());
|
||||
cleanedUp.print(this.logger);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Cleaned up ${cleanedUp} expired images`);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.interval) clearInterval(this.interval);
|
||||
if (cleanedUp > 0)
|
||||
this.logger.log(`Cleaned up ${cleanedUp} expired images`);
|
||||
}
|
||||
}
|
|
@ -19,9 +19,9 @@ import { ImageDBService } from '../../collections/image-db/image-db.service';
|
|||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { UsrPreferenceDbService } from '../../collections/preference-db/usr-preference-db.service';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/image.entity';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/images/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/images/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageProcessorService } from './image-processor.service';
|
||||
|
@ -145,19 +145,15 @@ export class ImageManagerService {
|
|||
|
||||
const converted_key = this.getConvertHash({ mime: fileType, ...options });
|
||||
|
||||
const [save_derivatives, allow_editing] = await Promise.all([
|
||||
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
|
||||
this.sysPref.getBooleanPreference(SysPreference.AllowEditing),
|
||||
]);
|
||||
if (HasFailed(save_derivatives)) return save_derivatives;
|
||||
const allow_editing = await this.sysPref.getBooleanPreference(
|
||||
SysPreference.AllowEditing,
|
||||
);
|
||||
if (HasFailed(allow_editing)) return allow_editing;
|
||||
|
||||
return MutexFallBack(
|
||||
converted_key,
|
||||
() => {
|
||||
if (save_derivatives)
|
||||
return this.imageFilesService.getDerivative(imageId, converted_key);
|
||||
else return Promise.resolve(null);
|
||||
return this.imageFilesService.getDerivative(imageId, converted_key);
|
||||
},
|
||||
async () => {
|
||||
const masterImage = await this.getMaster(imageId);
|
||||
|
@ -181,21 +177,12 @@ export class ImageManagerService {
|
|||
} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
|
||||
if (save_derivatives) {
|
||||
return await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
converted_key,
|
||||
convertResult.filetype,
|
||||
convertResult.image,
|
||||
);
|
||||
} else {
|
||||
const derivative = new EImageDerivativeBackend();
|
||||
derivative.filetype = convertResult.filetype;
|
||||
derivative.data = convertResult.image;
|
||||
derivative.image_id = imageId;
|
||||
derivative.key = converted_key;
|
||||
return derivative;
|
||||
}
|
||||
return await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
converted_key,
|
||||
convertResult.filetype,
|
||||
convertResult.image,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -256,7 +243,7 @@ export class ImageManagerService {
|
|||
|
||||
let mime: string | undefined;
|
||||
if (filetypeResult === undefined) {
|
||||
if (IsQOI(image)) mime = 'image/qoi';
|
||||
if (IsQOI(image)) mime = 'image/x-qoi';
|
||||
} else {
|
||||
mime = filetypeResult.mime;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { ImageDBModule } from '../../collections/image-db/image-db.module';
|
||||
import { SystemStateDbModule } from '../../collections/system-state-db/system-state-db.module';
|
||||
import { UserDbModule } from '../../collections/user-db/user-db.module';
|
||||
import { LateConfigModule } from '../../config/late/late-config.module';
|
||||
import { UsageConfigService } from '../../config/late/usage.config.service';
|
||||
import { UsageService } from './usage.service';
|
||||
|
||||
@Module({
|
||||
imports: [LateConfigModule, SystemStateDbModule, ImageDBModule, UserDbModule],
|
||||
providers: [UsageService],
|
||||
exports: [UsageService],
|
||||
})
|
||||
export class UsageManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(UsageManagerModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly usageService: UsageService,
|
||||
private readonly usageConfigService: UsageConfigService,
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
if (!(await this.usageConfigService.getMetricsEnabled())) {
|
||||
this.logger.log('Telemetry is disabled');
|
||||
}
|
||||
|
||||
const interval = setInterval(
|
||||
this.cronJob.bind(this),
|
||||
await this.usageConfigService.getMetricsInterval(),
|
||||
);
|
||||
this.schedulerRegistry.addInterval('usage', interval);
|
||||
|
||||
this.cronJob();
|
||||
}
|
||||
|
||||
private cronJob() {
|
||||
this.usageService.execute().catch((err) => {
|
||||
this.logger.warn(err);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import isDocker from 'is-docker';
|
||||
import fetch from 'node-fetch';
|
||||
import os from 'os';
|
||||
import { FallbackIfFailed, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { SystemStateDbService } from '../../collections/system-state-db/system-state-db.service';
|
||||
import { UserDbService } from '../../collections/user-db/user-db.service';
|
||||
import { HostConfigService } from '../../config/early/host.config.service';
|
||||
import { UsageConfigService } from '../../config/late/usage.config.service';
|
||||
|
||||
interface UsageData {
|
||||
id?: string;
|
||||
|
||||
uptime: number;
|
||||
version: string;
|
||||
demo_active: boolean;
|
||||
|
||||
users: number;
|
||||
images: number;
|
||||
|
||||
architecture: string;
|
||||
cpu_count: number;
|
||||
ram_total: number;
|
||||
hostname: string;
|
||||
|
||||
is_docker: boolean;
|
||||
is_production: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UsageService {
|
||||
private readonly logger = new Logger(UsageService.name);
|
||||
|
||||
constructor(
|
||||
private readonly systemState: SystemStateDbService,
|
||||
private readonly hostConfig: HostConfigService,
|
||||
private readonly usageConfig: UsageConfigService,
|
||||
private readonly userRepo: UserDbService,
|
||||
private readonly imageRepo: ImageDBService,
|
||||
) {}
|
||||
|
||||
public async execute() {
|
||||
if (!(await this.usageConfig.getMetricsEnabled())) return;
|
||||
|
||||
const id = await this.getSystemID();
|
||||
|
||||
if (id === null) {
|
||||
await this.sendInitialData();
|
||||
} else {
|
||||
await this.sendUpdateData(id);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendInitialData() {
|
||||
const url =
|
||||
(await this.usageConfig.getMetricsUrl()) + '/api/install/create';
|
||||
|
||||
const result: any = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(await this.collectData()),
|
||||
}).then((res) => res.json());
|
||||
|
||||
const id = result?.data?.id;
|
||||
if (typeof id !== 'string')
|
||||
return this.logger.warn(
|
||||
'Invalid response when sending initial data: ' + JSON.stringify(result),
|
||||
);
|
||||
if (!UUIDRegex.test(id))
|
||||
return this.logger.warn('Invalid system ID: ' + id);
|
||||
|
||||
await this.setSystemID(id);
|
||||
}
|
||||
|
||||
private async sendUpdateData(id: string) {
|
||||
const url =
|
||||
(await this.usageConfig.getMetricsUrl()) + '/api/install/update';
|
||||
|
||||
const body = await this.collectData();
|
||||
body.id = id;
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
const data: any = await result.json();
|
||||
|
||||
if (data?.type === 'notfound') {
|
||||
this.logger.warn('System ID not found, clearing');
|
||||
await this.clearSystemID();
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'Failed to send update data: ' + JSON.stringify(await result.json()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getSystemID(): Promise<string | null> {
|
||||
const result = await this.systemState.get('systemID');
|
||||
if (HasFailed(result)) {
|
||||
this.logger.warn(result);
|
||||
return null;
|
||||
}
|
||||
if (result === null) return null;
|
||||
if (UUIDRegex.test(result)) return result;
|
||||
this.logger.warn('Invalid system ID');
|
||||
return null;
|
||||
}
|
||||
|
||||
private async setSystemID(id: string) {
|
||||
if (!UUIDRegex.test(id)) {
|
||||
return this.logger.warn('Invalid system ID');
|
||||
}
|
||||
const result = await this.systemState.set('systemID', id);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.warn(result);
|
||||
}
|
||||
}
|
||||
|
||||
private async clearSystemID() {
|
||||
const result = await this.systemState.clear('systemID');
|
||||
if (HasFailed(result)) {
|
||||
this.logger.warn(result);
|
||||
}
|
||||
}
|
||||
|
||||
private async collectData(): Promise<UsageData> {
|
||||
const users = FallbackIfFailed(await this.userRepo.count(), 0, this.logger);
|
||||
const images = FallbackIfFailed(
|
||||
await this.imageRepo.count(),
|
||||
0,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
const data: UsageData = {
|
||||
uptime: Math.floor(process.uptime()),
|
||||
version: this.hostConfig.getVersion(),
|
||||
demo_active: this.hostConfig.isDemo(),
|
||||
users,
|
||||
images,
|
||||
architecture: process.arch,
|
||||
cpu_count: os.cpus().length,
|
||||
ram_total: Math.floor(os.totalmem() / 1024 / 1024),
|
||||
hostname: os.hostname(),
|
||||
is_docker: isDocker(),
|
||||
is_production: this.hostConfig.isProduction(),
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
|
||||
export type SysPreferences = SysPreference[];
|
||||
export const SysPreferenceList: string[] = Object.values(SysPreference);
|
||||
|
||||
// Syspref Value types
|
||||
export const SysPreferenceValueTypes: {
|
||||
[key in SysPreference]: PrefValueTypeStrings;
|
||||
} = {
|
||||
[SysPreference.JwtSecret]: 'string',
|
||||
[SysPreference.JwtExpiresIn]: 'string',
|
||||
[SysPreference.BCryptStrength]: 'number',
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: 'string',
|
||||
[SysPreference.SaveDerivatives]: 'boolean',
|
||||
[SysPreference.AllowEditing]: 'boolean',
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: 'string',
|
||||
[SysPreference.ConversionMemoryLimit]: 'number',
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import { PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
|
||||
export type UsrPreferences = UsrPreference[];
|
||||
export const UsrPreferenceList: string[] = Object.values(UsrPreference);
|
||||
|
||||
// Syspref Value types
|
||||
export const UsrPreferenceValueTypes: {
|
||||
[key in UsrPreference]: PrefValueTypeStrings;
|
||||
} = {
|
||||
[UsrPreference.KeepOriginal]: 'boolean',
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
import { createZodDto } from 'picsur-shared/dist/util/create-zod-dto';
|
||||
import { z } from 'zod';
|
||||
import { MultiPartFileDtoSchema } from './multipart.dto';
|
||||
|
||||
// A validation class for form based file upload of an image
|
||||
export const ImageUploadDtoSchema = z.object({
|
||||
image: MultiPartFileDtoSchema,
|
||||
});
|
||||
export class ImageUploadDto extends createZodDto(ImageUploadDtoSchema) {}
|
|
@ -1,48 +0,0 @@
|
|||
import { MultipartFile } from '@fastify/multipart';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MultiPartFileDtoSchema = z.object({
|
||||
fieldname: z.string(),
|
||||
encoding: z.string(),
|
||||
filename: z.string(),
|
||||
mimetype: z.string(),
|
||||
buffer: z.any(),
|
||||
file: z.any(),
|
||||
});
|
||||
export type MultiPartFileDto = z.infer<typeof MultiPartFileDtoSchema>;
|
||||
|
||||
export async function CreateMultiPartFileDto(
|
||||
file: MultipartFile,
|
||||
): AsyncFailable<MultiPartFileDto> {
|
||||
try {
|
||||
const buffer = await file.toBuffer();
|
||||
return {
|
||||
fieldname: file.fieldname,
|
||||
encoding: file.encoding,
|
||||
filename: file.filename,
|
||||
mimetype: file.mimetype,
|
||||
buffer,
|
||||
file: file.file,
|
||||
};
|
||||
} catch (e) {
|
||||
return Fail(FT.Internal, e);
|
||||
}
|
||||
}
|
||||
|
||||
export const MultiPartFieldDtoSchema = z.object({
|
||||
fieldname: z.string(),
|
||||
encoding: z.string(),
|
||||
value: z.string(),
|
||||
});
|
||||
export type MultiPartFieldDto = z.infer<typeof MultiPartFieldDtoSchema>;
|
||||
|
||||
export function CreateMultiPartFieldDto(
|
||||
file: MultipartFile,
|
||||
): MultiPartFieldDto {
|
||||
return {
|
||||
fieldname: file.fieldname,
|
||||
encoding: file.encoding,
|
||||
value: (file as any).value,
|
||||
};
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
|
||||
export function EUserBackend2EUser(eUser: EUserBackend): EUser {
|
||||
if (eUser.hashed_password === undefined) return eUser as EUser;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ExperimentModule } from './experiment/experiment.module';
|
|||
import { InfoModule } from './info/info.module';
|
||||
import { PrefModule } from './pref/pref.module';
|
||||
import { RolesApiModule } from './roles/roles.module';
|
||||
import { UsageApiModule } from './usage/usage.module';
|
||||
import { UserApiModule } from './user/user.module';
|
||||
|
||||
@Module({
|
||||
|
@ -14,6 +15,7 @@ import { UserApiModule } from './user/user.module';
|
|||
InfoModule,
|
||||
RolesApiModule,
|
||||
ApiKeysModule,
|
||||
UsageApiModule,
|
||||
],
|
||||
})
|
||||
export class PicsurApiModule {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyDeleteRequest,
|
||||
|
@ -53,6 +54,7 @@ export class ApiKeysController {
|
|||
|
||||
@Post('create')
|
||||
@Returns(ApiKeyCreateResponse)
|
||||
@Throttle(10)
|
||||
async createApiKey(
|
||||
@ReqUserID() userID: string,
|
||||
): Promise<ApiKeyCreateResponse> {
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
import { Controller, Get, Request, Response } from '@nestjs/common';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import { UserInfoResponse } from 'picsur-shared/dist/dto/api/user-manage.dto';
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { Returns } from '../../../decorators/returns.decorator';
|
||||
import type AuthFastifyRequest from '../../../models/interfaces/authrequest.dto';
|
||||
@Controller('api/experiment')
|
||||
@NoPermissions()
|
||||
export class ExperimentController {
|
||||
constructor() {}
|
||||
|
||||
@Get()
|
||||
@Returns(UserInfoResponse)
|
||||
async testRoute(
|
||||
@Request() req: AuthFastifyRequest,
|
||||
@Response({ passthrough: true }) res: FastifyReply,
|
||||
): Promise<UserInfoResponse> {
|
||||
res.header('Location', '/error/delete-success');
|
||||
res.code(302);
|
||||
return req.user;
|
||||
}
|
||||
// @Get()
|
||||
// @Returns(UserInfoResponse)
|
||||
// async testRoute(
|
||||
// @Request() req: AuthFastifyRequest,
|
||||
// @Response({ passthrough: true }) res: FastifyReply,
|
||||
// ): Promise<UserInfoResponse> {
|
||||
// res.header('Location', '/error/delete-success');
|
||||
// res.code(302);
|
||||
// return req.user;
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -10,7 +10,11 @@ import {
|
|||
SupportedAnimFileTypes,
|
||||
SupportedImageFileTypes,
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { TrackingState } from 'picsur-shared/dist/dto/tracking-state.enum';
|
||||
import { FallbackIfFailed } from 'picsur-shared/dist/types';
|
||||
import { HostConfigService } from '../../../config/early/host.config.service';
|
||||
import { InfoConfigService } from '../../../config/late/info.config.service';
|
||||
import { UsageConfigService } from '../../../config/late/usage.config.service';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { Returns } from '../../../decorators/returns.decorator';
|
||||
import { PermissionsList } from '../../../models/constants/permissions.const';
|
||||
|
@ -18,15 +22,29 @@ import { PermissionsList } from '../../../models/constants/permissions.const';
|
|||
@Controller('api/info')
|
||||
@NoPermissions()
|
||||
export class InfoController {
|
||||
constructor(private readonly hostConfig: HostConfigService) {}
|
||||
constructor(
|
||||
private readonly hostConfig: HostConfigService,
|
||||
private readonly infoConfig: InfoConfigService,
|
||||
private readonly usageService: UsageConfigService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Returns(InfoResponse)
|
||||
async getInfo(): Promise<InfoResponse> {
|
||||
const trackingID =
|
||||
FallbackIfFailed(await this.usageService.getTrackingID(), null) ??
|
||||
undefined;
|
||||
const hostOverride = await this.infoConfig.getHostnameOverride();
|
||||
|
||||
return {
|
||||
demo: this.hostConfig.isDemo(),
|
||||
production: this.hostConfig.isProduction(),
|
||||
version: this.hostConfig.getVersion(),
|
||||
host_override: hostOverride,
|
||||
tracking: {
|
||||
id: trackingID,
|
||||
state: TrackingState.Detailed,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EarlyConfigModule } from '../../../config/early/early-config.module';
|
||||
import { LateConfigModule } from '../../../config/late/late-config.module';
|
||||
import { InfoController } from './info.controller';
|
||||
|
||||
@Module({
|
||||
imports: [EarlyConfigModule],
|
||||
imports: [LateConfigModule],
|
||||
controllers: [InfoController],
|
||||
})
|
||||
export class InfoModule {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Get, Logger, Param, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
GetPreferenceResponse,
|
||||
MultiplePreferencesResponse,
|
||||
|
@ -20,6 +21,7 @@ export class SysPrefController {
|
|||
|
||||
@Get()
|
||||
@Returns(MultiplePreferencesResponse)
|
||||
@Throttle(20)
|
||||
async getAllSysPrefs(): Promise<MultiplePreferencesResponse> {
|
||||
const prefs = ThrowIfFailed(await this.prefService.getAllPreferences());
|
||||
|
||||
|
@ -39,6 +41,7 @@ export class SysPrefController {
|
|||
|
||||
@Post(':key')
|
||||
@Returns(UpdatePreferenceResponse)
|
||||
@Throttle(30)
|
||||
async setSysPref(
|
||||
@Param('key') key: string,
|
||||
@Body() body: UpdatePreferenceRequest,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Get, Logger, Param, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
GetPreferenceResponse,
|
||||
MultiplePreferencesResponse,
|
||||
|
@ -21,7 +22,8 @@ export class UsrPrefController {
|
|||
|
||||
@Get()
|
||||
@Returns(MultiplePreferencesResponse)
|
||||
async getAllSysPrefs(
|
||||
@Throttle(20)
|
||||
async getAllUsrPrefs(
|
||||
@ReqUserID() userid: string,
|
||||
): Promise<MultiplePreferencesResponse> {
|
||||
const prefs = ThrowIfFailed(
|
||||
|
@ -36,7 +38,7 @@ export class UsrPrefController {
|
|||
|
||||
@Get(':key')
|
||||
@Returns(GetPreferenceResponse)
|
||||
async getSysPref(
|
||||
async getUsrPref(
|
||||
@Param('key') key: string,
|
||||
@ReqUserID() userid: string,
|
||||
): Promise<GetPreferenceResponse> {
|
||||
|
@ -49,7 +51,8 @@ export class UsrPrefController {
|
|||
|
||||
@Post(':key')
|
||||
@Returns(UpdatePreferenceResponse)
|
||||
async setSysPref(
|
||||
@Throttle(30)
|
||||
async setUsrPref(
|
||||
@Param('key') key: string,
|
||||
@ReqUserID() userid: string,
|
||||
@Body() body: UpdatePreferenceRequest,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Get, Logger, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
RoleCreateRequest,
|
||||
RoleCreateResponse,
|
||||
|
@ -56,6 +57,7 @@ export class RolesController {
|
|||
|
||||
@Post('update')
|
||||
@Returns(RoleUpdateResponse)
|
||||
@Throttle(20)
|
||||
async updateRole(
|
||||
@Body() body: RoleUpdateRequest,
|
||||
): Promise<RoleUpdateResponse> {
|
||||
|
@ -73,6 +75,7 @@ export class RolesController {
|
|||
|
||||
@Post('create')
|
||||
@Returns(RoleCreateResponse)
|
||||
@Throttle(10)
|
||||
async createRole(
|
||||
@Body() role: RoleCreateRequest,
|
||||
): Promise<RoleCreateResponse> {
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { Controller, Logger, Post, Req, Res } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { UsageConfigService } from '../../../config/late/usage.config.service';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { ReturnsAnything } from '../../../decorators/returns.decorator';
|
||||
|
||||
@Controller('api/usage')
|
||||
@NoPermissions()
|
||||
export class UsageController {
|
||||
private readonly logger = new Logger(UsageController.name);
|
||||
|
||||
constructor(private readonly usageService: UsageConfigService) {}
|
||||
|
||||
@Post(['report', 'report/*'])
|
||||
@ReturnsAnything()
|
||||
@Throttle(120)
|
||||
async deleteRole(
|
||||
@Req() req: FastifyRequest,
|
||||
@Res({
|
||||
passthrough: true,
|
||||
})
|
||||
res: FastifyReply,
|
||||
) {
|
||||
const trackingUrl = ThrowIfFailed(await this.usageService.getTrackingUrl());
|
||||
|
||||
if (trackingUrl === null) {
|
||||
throw Fail(FT.NotFound, undefined, 'Tracking URL not set');
|
||||
}
|
||||
|
||||
await res.from(`${trackingUrl}/api`, {
|
||||
rewriteRequestHeaders(request, headers) {
|
||||
const req = request as any as FastifyRequest;
|
||||
|
||||
// remove cookies
|
||||
delete headers.cookie;
|
||||
|
||||
// Add real ip, this should not work, but ackee uses a bad ip resolver
|
||||
// So we might aswell use it
|
||||
headers['X-Forwarded-For'] = req.ip;
|
||||
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { LateConfigModule } from '../../../config/late/late-config.module';
|
||||
import { UsageController } from './usage.controller';
|
||||
|
||||
@Module({
|
||||
imports: [LateConfigModule],
|
||||
controllers: [UsageController],
|
||||
})
|
||||
export class UsageApiModule {}
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Get, Logger, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
GetSpecialUsersResponse,
|
||||
UserCreateRequest,
|
||||
|
@ -46,6 +47,7 @@ export class UserAdminController {
|
|||
|
||||
@Post('create')
|
||||
@Returns(UserCreateResponse)
|
||||
@Throttle(10)
|
||||
async register(
|
||||
@Body() create: UserCreateRequest,
|
||||
): Promise<UserCreateResponse> {
|
||||
|
@ -78,6 +80,7 @@ export class UserAdminController {
|
|||
|
||||
@Post('update')
|
||||
@Returns(UserUpdateResponse)
|
||||
@Throttle(20)
|
||||
async setPermissions(
|
||||
@Body() body: UserUpdateRequest,
|
||||
): Promise<UserUpdateResponse> {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Get, Logger, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import {
|
||||
UserCheckNameRequest,
|
||||
UserCheckNameResponse,
|
||||
|
@ -34,6 +35,7 @@ export class UserController {
|
|||
@Post('login')
|
||||
@Returns(UserLoginResponse)
|
||||
@UseLocalAuth(Permission.UserLogin)
|
||||
@Throttle(30, 300)
|
||||
async login(@ReqUser() user: EUser): Promise<UserLoginResponse> {
|
||||
const jwt_token = ThrowIfFailed(await this.authService.createToken(user));
|
||||
|
||||
|
@ -43,6 +45,7 @@ export class UserController {
|
|||
@Post('register')
|
||||
@Returns(UserRegisterResponse)
|
||||
@RequiredPermissions(Permission.UserRegister)
|
||||
@Throttle(5, 300)
|
||||
async register(
|
||||
@Body() register: UserRegisterRequest,
|
||||
): Promise<UserRegisterResponse> {
|
||||
|
@ -56,6 +59,7 @@ export class UserController {
|
|||
@Post('checkname')
|
||||
@Returns(UserCheckNameResponse)
|
||||
@RequiredPermissions(Permission.UserRegister)
|
||||
@Throttle(20)
|
||||
async checkName(
|
||||
@Body() checkName: UserCheckNameRequest,
|
||||
): Promise<UserCheckNameResponse> {
|
||||
|
@ -67,6 +71,7 @@ export class UserController {
|
|||
@Get('me')
|
||||
@Returns(UserMeResponse)
|
||||
@RequiredPermissions(Permission.UserKeepLogin)
|
||||
@Throttle(10)
|
||||
async me(@ReqUserID() userid: string): Promise<UserMeResponse> {
|
||||
const backenduser = ThrowIfFailed(await this.usersService.findOne(userid));
|
||||
|
||||
|
@ -81,6 +86,7 @@ export class UserController {
|
|||
@Get('me/permissions')
|
||||
@Returns(UserMePermissionsResponse)
|
||||
@NoPermissions()
|
||||
@Throttle(20)
|
||||
async refresh(
|
||||
@ReqUserID() userid: string,
|
||||
): Promise<UserMePermissionsResponse> {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Post,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import {
|
||||
ImageDeleteRequest,
|
||||
|
@ -20,8 +21,9 @@ import {
|
|||
ImageUploadResponse,
|
||||
} from 'picsur-shared/dist/dto/api/image-manage.dto';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
|
||||
import { HasFailed, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { MultiPart } from '../../decorators/multipart/multipart.decorator';
|
||||
import { Fail, FT, HasFailed, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { PostFiles } from '../../decorators/multipart/multipart.decorator';
|
||||
import type { FileIterator } from '../../decorators/multipart/postfiles.pipe';
|
||||
import {
|
||||
HasPermission,
|
||||
RequiredPermissions,
|
||||
|
@ -29,7 +31,7 @@ import {
|
|||
import { ReqUserID } from '../../decorators/request-user.decorator';
|
||||
import { Returns } from '../../decorators/returns.decorator';
|
||||
import { ImageManagerService } from '../../managers/image/image.service';
|
||||
import { ImageUploadDto } from '../../models/dto/image-upload.dto';
|
||||
import { GetNextAsync } from '../../util/iterator';
|
||||
@Controller('api/image')
|
||||
@RequiredPermissions(Permission.ImageUpload)
|
||||
export class ImageManageController {
|
||||
|
@ -39,16 +41,26 @@ export class ImageManageController {
|
|||
|
||||
@Post('upload')
|
||||
@Returns(ImageUploadResponse)
|
||||
@Throttle(20)
|
||||
async uploadImage(
|
||||
@MultiPart() multipart: ImageUploadDto,
|
||||
@PostFiles(1) multipart: FileIterator,
|
||||
@ReqUserID() userid: string,
|
||||
@HasPermission(Permission.ImageDeleteKey) withDeleteKey: boolean,
|
||||
): Promise<ImageUploadResponse> {
|
||||
const file = ThrowIfFailed(await GetNextAsync(multipart));
|
||||
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await file.toBuffer();
|
||||
} catch (e) {
|
||||
throw Fail(FT.Internal, e);
|
||||
}
|
||||
|
||||
const image = ThrowIfFailed(
|
||||
await this.imagesService.upload(
|
||||
userid,
|
||||
multipart.image.filename,
|
||||
multipart.image.buffer,
|
||||
file.filename,
|
||||
buffer,
|
||||
withDeleteKey,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Controller, Get, Head, Logger, Query, Res } from '@nestjs/common';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import {
|
||||
ImageMetaResponse,
|
||||
|
@ -21,6 +22,7 @@ import { BrandMessageType, GetBrandMessage } from '../../util/branding';
|
|||
// This is the only controller with CORS enabled
|
||||
@Controller('i')
|
||||
@RequiredPermissions(Permission.ImageView)
|
||||
@SkipThrottle()
|
||||
export class ImageController {
|
||||
private readonly logger = new Logger(ImageController.name);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { UserDbModule } from '../../collections/user-db/user-db.module';
|
||||
import { DecoratorsModule } from '../../decorators/decorators.module';
|
||||
import { ImageManagerModule } from '../../managers/image/image.module';
|
||||
import { ImageManagerModule } from '../../managers/image/image-manager.module';
|
||||
import { ImageManageController } from './image-manage.controller';
|
||||
import { ImageController } from './image.controller';
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
|
||||
export async function GetNextAsync<T>(
|
||||
iterator: AsyncIterableIterator<T>,
|
||||
): AsyncFailable<T> {
|
||||
const { done, value } = await iterator.next();
|
||||
if (done) return Fail(FT.BadRequest);
|
||||
return value;
|
||||
}
|
|
@ -41,7 +41,9 @@
|
|||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ngx-auto-unsubscribe-decorator",
|
||||
"moment"
|
||||
"moment",
|
||||
"platform",
|
||||
"form-data"
|
||||
],
|
||||
"optimization": true,
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
|
@ -54,7 +56,7 @@
|
|||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kb",
|
||||
"maximumWarning": "1mb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
|
@ -102,6 +104,5 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "picsur-frontend"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import webpack from 'webpack';
|
||||
// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
|
||||
export default {
|
||||
plugins: [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/)],
|
||||
plugins: [
|
||||
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
||||
// new BundleAnalyzerPlugin(),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "picsur-frontend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"description": "Frontend for Picsur",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/rubikscraft/Picsur",
|
||||
|
@ -14,30 +14,34 @@
|
|||
"purge": "rm -rf dist && rm -rf node_modules && rm -rf .angular"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^14.0.1",
|
||||
"@angular-devkit/build-angular": "14.2.5",
|
||||
"@angular/animations": "^14.2.5",
|
||||
"@angular/cdk": "^14.2.4",
|
||||
"@angular/cli": "^14.2.5",
|
||||
"@angular/common": "^14.2.5",
|
||||
"@angular/compiler": "^14.2.5",
|
||||
"@angular/compiler-cli": "^14.2.5",
|
||||
"@angular/core": "^14.2.5",
|
||||
"@angular/forms": "^14.2.5",
|
||||
"@angular/material": "^14.2.4",
|
||||
"@angular/platform-browser": "^14.2.5",
|
||||
"@angular/platform-browser-dynamic": "^14.2.5",
|
||||
"@angular/router": "^14.2.5",
|
||||
"@angular-builders/custom-webpack": "^15.0.0",
|
||||
"@angular-devkit/build-angular": "^15.0.4",
|
||||
"@angular/animations": "^15.0.4",
|
||||
"@angular/cdk": "^15.0.3",
|
||||
"@angular/cli": "^15.0.4",
|
||||
"@angular/common": "^15.0.4",
|
||||
"@angular/compiler": "^15.0.4",
|
||||
"@angular/compiler-cli": "^15.0.4",
|
||||
"@angular/core": "^15.0.4",
|
||||
"@angular/forms": "^15.0.4",
|
||||
"@angular/material": "^15.0.3",
|
||||
"@angular/platform-browser": "^15.0.4",
|
||||
"@angular/platform-browser-dynamic": "^15.0.4",
|
||||
"@angular/router": "^15.0.4",
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@fontsource/material-icons-outlined": "^4.5.4",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@ng-web-apis/common": "^2.0.1",
|
||||
"@ng-web-apis/common": "^2.1.0",
|
||||
"@ng-web-apis/resize-observer": "^2.0.0",
|
||||
"@ngui/common": "^1.0.0",
|
||||
"@types/node": "^18.8.4",
|
||||
"@types/ackee-tracker": "^5.0.2",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/resize-observer-browser": "^0.1.7",
|
||||
"@types/validator": "^13.7.7",
|
||||
"bootstrap": "^5.2.2",
|
||||
"@types/validator": "^13.7.10",
|
||||
"ackee-tracker": "^5.1.0",
|
||||
"axios": "^1.2.1",
|
||||
"bootstrap": "^5.2.3",
|
||||
"caniuse-lite": "^1.0.30001441",
|
||||
"fuse.js": "^6.6.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"moment": "^2.29.4",
|
||||
|
@ -47,10 +51,11 @@
|
|||
"ngx-moment": "^6.0.2",
|
||||
"picsur-shared": "*",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "~7.5.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "4.8.4",
|
||||
"zod": "^3.19.1",
|
||||
"zone.js": "~0.11.8"
|
||||
"rxjs": "^7.8.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"zod": "^3.20.2",
|
||||
"zone.js": "^0.12.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<app-header
|
||||
[enableHamburger]="hasSidebar && !isDesktop"
|
||||
(onHamburgerClick)="sidebar.toggle()"
|
||||
[loading]="loading"
|
||||
></app-header>
|
||||
|
||||
<mat-sidenav-container class="grow-full">
|
||||
|
|
|
@ -5,11 +5,13 @@ import {
|
|||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationStart,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import { RouteTransitionAnimations } from './app.animation';
|
||||
import { PRouteData } from './models/dto/picsur-routes.dto';
|
||||
import { UsageService } from './services/usage/usage.service';
|
||||
import { BootstrapService } from './util/bootstrap.service';
|
||||
|
||||
@Component({
|
||||
|
@ -23,6 +25,9 @@ export class AppComponent implements OnInit {
|
|||
|
||||
@ViewChild(MatSidenav) sidebar: MatSidenav;
|
||||
|
||||
loading: boolean = false;
|
||||
private loadingTimeout: number | null = null;
|
||||
|
||||
wrapContentWithContainer: boolean = true;
|
||||
sidebarPortal: Portal<any> | undefined = undefined;
|
||||
|
||||
|
@ -33,7 +38,10 @@ export class AppComponent implements OnInit {
|
|||
private readonly router: Router,
|
||||
private readonly activatedRoute: ActivatedRoute,
|
||||
private readonly bootstrapService: BootstrapService,
|
||||
) {}
|
||||
usageService: UsageService,
|
||||
) {
|
||||
usageService;
|
||||
}
|
||||
|
||||
public getRouteAnimData() {
|
||||
// Everyone is doing shit with the activated route
|
||||
|
@ -50,6 +58,12 @@ export class AppComponent implements OnInit {
|
|||
@AutoUnsubscribe()
|
||||
private subscribeRouter() {
|
||||
return this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
this.loadingStart();
|
||||
}
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.loadingEnd();
|
||||
}
|
||||
if (event instanceof NavigationEnd) this.onNavigationEnd(event);
|
||||
if (event instanceof NavigationError) this.onNavigationError(event);
|
||||
});
|
||||
|
@ -83,6 +97,21 @@ export class AppComponent implements OnInit {
|
|||
this.updateSidebar();
|
||||
}
|
||||
|
||||
private loadingStart() {
|
||||
if (this.loadingTimeout !== null) clearTimeout(this.loadingTimeout);
|
||||
|
||||
this.loadingTimeout = window.setTimeout(() => {
|
||||
this.loading = true;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private loadingEnd() {
|
||||
if (this.loadingTimeout !== null) clearTimeout(this.loadingTimeout);
|
||||
this.loadingTimeout = null;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private updateSidebar() {
|
||||
if (!this.sidebar) return;
|
||||
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { PRoutes } from './models/dto/picsur-routes.dto';
|
||||
import { ErrorsRouteModule } from './routes/errors/errors.module';
|
||||
import { ImagesRouteModule } from './routes/images/images.module';
|
||||
import { ProcessingRouteModule } from './routes/processing/processing.module';
|
||||
import { SettingsRouteModule } from './routes/settings/settings.module';
|
||||
import { UploadRouteModule } from './routes/upload/upload.module';
|
||||
import { UserRouteModule } from './routes/user/user.module';
|
||||
import { ViewRouteModule } from './routes/view/view.module';
|
||||
|
||||
const routes: PRoutes = [
|
||||
{
|
||||
|
@ -17,31 +10,38 @@ const routes: PRoutes = [
|
|||
},
|
||||
{
|
||||
path: 'upload',
|
||||
loadChildren: () => UploadRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/upload/upload.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'processing',
|
||||
loadChildren: () => ProcessingRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/processing/processing.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'view',
|
||||
loadChildren: () => ViewRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/view/view.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
loadChildren: () => UserRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/user/user.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'images',
|
||||
loadChildren: () => ImagesRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/images/images.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadChildren: () => SettingsRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/settings/settings.module').then((m) => m.default),
|
||||
},
|
||||
{
|
||||
path: 'error',
|
||||
loadChildren: () => ErrorsRouteModule,
|
||||
loadChildren: () =>
|
||||
import('./routes/errors/errors.module').then((m) => m.default),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-form-field
|
||||
[appearance]="appearance"
|
||||
[color]="color"
|
||||
[subscriptSizing]="subscriptSizing"
|
||||
>
|
||||
<mat-label>{{ label }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import {
|
||||
MatFormFieldAppearance,
|
||||
SubscriptSizing,
|
||||
} from '@angular/material/form-field';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Logger } from 'src/app/services/logger/logger.service';
|
||||
import { ErrorService } from 'src/app/util/error-manager/error.service';
|
||||
|
@ -19,6 +23,10 @@ export class CopyFieldComponent {
|
|||
@Input() showHideButton: boolean = false;
|
||||
@Input() hidden: boolean = false;
|
||||
|
||||
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
|
||||
@Input() appearance: MatFormFieldAppearance = 'outline';
|
||||
@Input() subscriptSizing: SubscriptSizing = 'fixed';
|
||||
|
||||
@Output('copy') onCopy = new EventEmitter<string>();
|
||||
@Output('hide') onHide = new EventEmitter<boolean>();
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Component, Input } from '@angular/core';
|
|||
export class FabComponent {
|
||||
@Input('aria-label') ariaLabel: string = 'Floating Action Button';
|
||||
@Input() icon: string = 'add';
|
||||
@Input() color: string = 'accent';
|
||||
@Input() color: string = 'primary';
|
||||
@Input('tooltip') tooltip: string;
|
||||
@Input() onClick: () => void = () => {};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Directive, Host, Optional } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatMiniFabButton } from '@angular/material/button';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
|
||||
@Directive({
|
||||
|
@ -8,9 +8,9 @@ import { MatTooltip } from '@angular/material/tooltip';
|
|||
export class SpeedDialOptionDirective {
|
||||
constructor(
|
||||
@Host() @Optional() tooltip?: MatTooltip,
|
||||
@Host() @Optional() button?: MatButton,
|
||||
@Host() @Optional() button?: MatMiniFabButton,
|
||||
) {
|
||||
if (tooltip) tooltip.position = 'left';
|
||||
if (button) button.color = 'primary';
|
||||
if (button) button.color = 'accent';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class SpeedDialComponent {
|
|||
|
||||
@Input('icon') icon: string = 'add';
|
||||
@Input('icon-hover') iconHover: string = 'close';
|
||||
@Input('color') color: string = 'accent';
|
||||
@Input('color') color: string = 'primary';
|
||||
@Input('open-on-hover') openOnHover: boolean = false;
|
||||
@Input('tooltip') tooltip: string;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ footer {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 16px;
|
||||
margin-top: 1em;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
<mat-toolbar color="primary">
|
||||
<mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="loading"
|
||||
class="loading-bar"
|
||||
mode="indeterminate"
|
||||
color="accent"
|
||||
></mat-progress-bar>
|
||||
|
||||
<button
|
||||
*ngIf="_enableHamburger"
|
||||
class="me-3"
|
||||
|
@ -56,9 +63,9 @@
|
|||
|
||||
<mat-menu #menu="matMenu" xPosition="before">
|
||||
<ng-template matMenuContent>
|
||||
<span mat-menu-item disabled>
|
||||
<div class="centered">
|
||||
<h2>{{ user?.username }}</h2>
|
||||
<span>
|
||||
<div class="menu-username center-horizontally">
|
||||
<h2 class="mat-typography">{{ user?.username }}</h2>
|
||||
</div>
|
||||
</span>
|
||||
<button *ngIf="canUpload" mat-menu-item (click)="doImages()">
|
||||
|
|
|
@ -28,8 +28,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.mat-menu-item[disabled] {
|
||||
color: inherit;
|
||||
.menu-username {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
@ -54,3 +55,10 @@ mat-toolbar {
|
|||
box-shadow: 0px 2px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ export class HeaderComponent implements OnInit {
|
|||
public _enableHamburger: boolean = true;
|
||||
@Output('onHamburgerClick') onHamburgerClick = new EventEmitter<void>();
|
||||
|
||||
@Input('loading') public loading: boolean = false;
|
||||
|
||||
private currentUser: EUser | null = null;
|
||||
|
||||
public canLogIn: boolean = false;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
@ -16,6 +17,7 @@ import { HeaderComponent } from './header.component';
|
|||
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
RouterModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
|
|
|
@ -17,5 +17,4 @@
|
|||
(nguiInview)="onInview($event)"
|
||||
(nguiOutview)="onOutview($event)"
|
||||
*ngIf="state === 'loading'"
|
||||
color="accent"
|
||||
></mat-spinner>
|
||||
|
|
|
@ -85,7 +85,7 @@ export class PicsurImgComponent implements OnChanges {
|
|||
|
||||
this.state = PicsurImgState.Canvas;
|
||||
} else {
|
||||
const result = await this.apiService.getBuffer(url);
|
||||
const result = await this.apiService.getBuffer(url).result;
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
const img = this.img.nativeElement;
|
||||
|
@ -99,12 +99,12 @@ export class PicsurImgComponent implements OnChanges {
|
|||
}
|
||||
|
||||
private async getFileType(url: string): AsyncFailable<FileType> {
|
||||
const response = await this.apiService.head(url);
|
||||
const response = await this.apiService.head(url).result;
|
||||
if (HasFailed(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const mimeHeader = response.get('content-type') ?? '';
|
||||
const mimeHeader = response['content-type'] ?? '';
|
||||
const mime = mimeHeader.split(';')[0];
|
||||
|
||||
return ParseMime2FileType(mime);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue