Compare commits
133 commits
Author | SHA1 | Date | |
---|---|---|---|
1e9dad9ac2 | |||
441e7d2af1 | |||
5195f1f5d8 | |||
b0c53850db | |||
e2de37a6dd | |||
b8db72bac4 | |||
3b6244461e | |||
2a11f162b3 | |||
e09f661cdf | |||
409c3af475 | |||
18bee2e0cd | |||
5e6c12ce6d | |||
cea5443309 | |||
0e1146f578 | |||
b7b5d7015e | |||
c65b7ca066 | |||
10036f1269 | |||
dadc954564 | |||
86737ebe6a | |||
949b5c95c7 | |||
8bc13b106d | |||
767256d2c4 | |||
86179356a4 | |||
f794c724c0 | |||
221d7c1072 | |||
26ed0e9ea5 | |||
a0984efd67 | |||
5d0ecbbd31 | |||
40bd2349a3 | |||
5a6a366f9d | |||
adc58476ff | |||
8999e69f26 | |||
2f4c74b8da | |||
9bf2dfd6fc | |||
05fabea82b | |||
4566fa947d | |||
844d11b9ca | |||
72b6b2a3d7 | |||
9845fae599 | |||
f6f94a9e01 | |||
7b6ffb5010 | |||
78ff25034c | |||
a750d65baa | |||
286333f598 | |||
d65ac16943 | |||
f69e455996 | |||
dac43896ce | |||
145ff6973f | |||
3b50cdeb1e | |||
7dc3a198e4 | |||
b769ec9c8e | |||
58ff75c728 | |||
9cdf909994 | |||
72264bd88f | |||
7cec8eb816 | |||
96641e3c60 | |||
d7b4bc19e0 | |||
7c9bf1ff7e | |||
35328d0433 | |||
d41530d40c | |||
9fcd83427b | |||
523dcf1f2c | |||
6a17cd12a5 | |||
00621cf637 | |||
ff2386c161 | |||
9e5178db8b | |||
e6b70d0b1c | |||
0dd0c44113 | |||
95fd0b9aa7 | |||
9792cedb94 | |||
ef255d6e66 | |||
c866522888 | |||
539f4bb042 | |||
941e0deb0a | |||
6621a167e7 | |||
74eb9a2503 | |||
b3d7cc1546 | |||
bd86a8c336 | |||
e0e804d27d | |||
08af514758 | |||
a19d0bab25 | |||
03fec5f832 | |||
422b4a73c4 | |||
32ee928b6c | |||
887b80aee8 | |||
864758f296 | |||
2976d746de | |||
c57ae98f2c | |||
e96c24a669 | |||
838cb237af | |||
7f5a6b1f7a | |||
485ca2d3ff | |||
f602e71520 | |||
27b5fdeae1 | |||
94763e1e41 | |||
9580ccc928 | |||
5878f0ad1d | |||
ec3e58d1b2 | |||
92e44aea66 | |||
8ffb06c059 | |||
6b0504ec9c | |||
a91363962a | |||
0813ed0cbc | |||
bfbc6fc8e3 | |||
47dd528778 | |||
833a89426a | |||
db7b02b629 | |||
c68360c81f | |||
a7981ce8ad | |||
caa18ea3bd | |||
482ab2bfb6 | |||
7eb203555c | |||
8fe5833036 | |||
b92e9c4f98 | |||
94b10929df | |||
aa61e12856 | |||
fc7bf915dc | |||
66342025d9 | |||
2519afae6a | |||
b23e5c6660 | |||
c8c0443940 | |||
7962adb5ca | |||
83cc652b52 | |||
3a1f279d32 | |||
1fdbe9edc7 | |||
654671ba4c | |||
69c405c7cc | |||
d1419e67cf | |||
a7f19b424a | |||
c99cff55e1 | |||
0e2388f808 | |||
ba7f1db412 | |||
ea5a14483f |
|
@ -1,24 +1,22 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
ignorePatterns: ['.eslintrc.cjs', 'dist', '*.exclude.*'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
root: true,
|
||||
};
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: caramelfur
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,5 +10,4 @@ yarn-error.log
|
|||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
|
||||
temp
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
.angular
|
||||
.yarn
|
||||
|
|
|
@ -3,3 +3,6 @@ endOfLine: lf
|
|||
singleQuote: true
|
||||
tabWidth: 2
|
||||
trailingComma: all
|
||||
|
||||
plugins:
|
||||
- prettier-plugin-sh
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"vsicons.presets.angular": true,
|
||||
"skipRefreshExplorerOnWindowFocus": true,
|
||||
"angular.log": "verbose"
|
||||
"angular.log": "verbose",
|
||||
"discord.enabled": true
|
||||
}
|
||||
|
|
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
4
.yarn/versions/6db1bf03.yml
vendored
Normal file
4
.yarn/versions/6db1bf03.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
4
.yarn/versions/8df55c81.yml
vendored
Normal file
4
.yarn/versions/8df55c81.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
||||
- picsur-shared
|
4
.yarn/versions/9e39fdb4.yml
vendored
Normal file
4
.yarn/versions/9e39fdb4.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
4
.yarn/versions/a9a7dc82.yml
vendored
Normal file
4
.yarn/versions/a9a7dc82.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
4
.yarn/versions/ccff8772.yml
vendored
Normal file
4
.yarn/versions/ccff8772.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
4
.yarn/versions/e0bbb8ad.yml
vendored
Normal file
4
.yarn/versions/e0bbb8ad.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
undecided:
|
||||
- root-workspace-0b6124
|
||||
- picsur-backend
|
||||
- picsur-frontend
|
|
@ -2,6 +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'
|
||||
|
||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
||||
|
|
4
LICENSE
4
LICENSE
|
@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively
|
|||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
Picsur - An easy to use, selfhostable image sharing service like Imgur
|
||||
Copyright (C) 2022 Caramel <picsur@caramelfur.dev>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
|
106
README.md
106
README.md
|
@ -1,4 +1,4 @@
|
|||
<img align="left" width="100" height="100" src="branding/logo/picsur.svg"/>
|
||||
<img align="left" width="100" height="100" style="border-radius: 15%" src="branding/logo/picsur.svg"/>
|
||||
|
||||
<a href="https://discord.gg/GPZNwV3VKE">
|
||||
<img align="right" style="margin: 5px" src="https://img.shields.io/discord/986634827337965638?color=454FBF&label=Chat%20on%20Discord"/>
|
||||
|
@ -6,9 +6,7 @@
|
|||
|
||||
# Picsur
|
||||
|
||||
<br>
|
||||
|
||||
> Totally not an imgur clone
|
||||
> Totally not an Imgur clone
|
||||
|
||||
I couldn't really find any open source project that allowed you to easily host images. So I decided to create one.
|
||||
|
||||
|
@ -21,16 +19,16 @@ 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.
|
||||
|
||||
## Features
|
||||
|
||||
Here is a list of done features, and what is planned.
|
||||
For a more detailed list, you can always visit [the project](https://github.com/rubikscraft/Picsur/projects/1).
|
||||
For a more detailed list, you can always visit [the project](https://github.com/CaramelFur/Picsur/projects/1).
|
||||
|
||||
Right now, not every done feature here is available in the current release. But these will all be available with the next one.
|
||||
Every featured marked here should work in the latest release.
|
||||
|
||||
- [x] Uploading and viewing images
|
||||
- [x] Anonymous uploads
|
||||
|
@ -39,27 +37,81 @@ Right now, not every done feature here is available in the current release. But
|
|||
- [x] Proper CORS restrictions
|
||||
- [x] Exif stripping
|
||||
- [x] Ability to keep original
|
||||
- [x] Support for [QOI format](https://qoiformat.org/)
|
||||
- [x] Support for many formats
|
||||
- QOI
|
||||
- JPG
|
||||
- PNG
|
||||
- WEBP (animated supported)
|
||||
- TIFF
|
||||
- BMP
|
||||
- GIF (animated supported)
|
||||
- [x] Convert images
|
||||
- [x] Resize images
|
||||
- [x] Apply filters
|
||||
- [x] Edit images
|
||||
- Resize
|
||||
- Rotate
|
||||
- Flip
|
||||
- Strip transparency
|
||||
- Negative
|
||||
- Greyscale
|
||||
- [x] Deletable images
|
||||
- [x] Proper DB migrations
|
||||
- [x] Show own images in list
|
||||
- [x] Correct previews on chats
|
||||
- [x] Expiring images
|
||||
- [x] ShareX endpoint
|
||||
- [x] ARM64 and AMD64 Docker image
|
||||
|
||||
- [ ] Correct previews on chats
|
||||
- [ ] Expiring images
|
||||
- [ ] White mode
|
||||
- [ ] ShareX endpoint
|
||||
- [ ] Arm64 image
|
||||
- [ ] Public gallery
|
||||
- [ ] Albums
|
||||
|
||||
## Bugs
|
||||
|
||||
If you encounter any bugs or oddities, please open an issue [here](https://github.com/rubikscraft/Picsur/issues). Cause without feedback I'll never know they exists.
|
||||
If you encounter any bugs or oddities, please open an issue [here](https://github.com/CaramelFur/Picsur/issues). Cause without feedback I'll never know they exists.
|
||||
|
||||
## Star
|
||||
|
||||
If you like this project, don't forget to give it a star. It tells me that I'm not wasting my time on something that people don't like.
|
||||
|
||||
## Running
|
||||
## 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.
|
||||
|
||||
If you wish to keep your images, you will have to host your own instance.
|
||||
|
||||
### How do I allow users to register their own accounts?
|
||||
|
||||
By default, users can't register their own accounts. This is to prevent users from accidentally allowing anyone to upload to their instance.
|
||||
|
||||
If you want to allow this you can though. To change this you go to `settings -> roles -> guest -> edit`, and then give the guest role the `Register` permission. Upon saving the role, the register button will appear on the login page.
|
||||
|
||||
### I want to keep my original image files, how?
|
||||
|
||||
By default, Picsur will not keep your original image files. Since for most purposes this is not needed, and it saves disk space.
|
||||
|
||||
If you want to enable this however, you can do so by going to `settings -> general`, and then enabling the `Keep original` option. Upon saving the settings, the original files will be kept.
|
||||
|
||||
Do keep in mind here, that the exif data will NOT be removed from the original image. So make sure you do not accidentally share sensitive data.
|
||||
|
||||
### This service says its supports the QOI format, what is this?
|
||||
|
||||
QOI is a new lossless image format that is designed to be very fast to encode and decode. All while still offering good compression ratios. This is the primary format the server will store images in when uploaded.
|
||||
|
||||
You can [read more about QOI here](https://qoiformat.org/).
|
||||
|
||||
### What is the default admin login?
|
||||
|
||||
The default username is `admin`, and the default password is set from the `PICSUR_ADMIN_PASSWORD` environment variable.
|
||||
|
||||
## Running your own instance
|
||||
|
||||
You easily run this service yourself via Docker. Here is an example docker-compose file:
|
||||
|
||||
|
@ -67,7 +119,7 @@ You easily run this service yourself via Docker. Here is an example docker-compo
|
|||
version: '3'
|
||||
services:
|
||||
picsur:
|
||||
image: ghcr.io/rubikscraft/picsur:latest
|
||||
image: ghcr.io/caramelfur/picsur:latest
|
||||
container_name: picsur
|
||||
ports:
|
||||
- '8080:8080'
|
||||
|
@ -81,16 +133,23 @@ services:
|
|||
# PICSUR_DB_PASSWORD: picsur
|
||||
# PICSUR_DB_DATABASE: picsur
|
||||
|
||||
## The default username is admin, this is not modifyable
|
||||
# PICSUR_ADMIN_PASSWORD: picsur
|
||||
|
||||
## Optional, random secret will be generated if not set
|
||||
# PICSUR_JWT_SECRET: CHANGE_ME
|
||||
# PICSUR_JWT_EXPIRY: 1d
|
||||
# PICSUR_JWT_EXPIRY: 7d
|
||||
|
||||
## Maximum accepted size for uploads in bytes
|
||||
# PICSUR_MAX_FILE_SIZE: 128000000
|
||||
## No need to touch this, unless you use a custom frontend
|
||||
# PICSUR_STATIC_FRONTEND_ROOT: "/picsur/frontend/dist"
|
||||
|
||||
## Warning: Verbose mode might log sensitive data
|
||||
# PICSUR_VERBOSE: "true"
|
||||
restart: unless-stopped
|
||||
picsur_postgres:
|
||||
image: postgres:11-alpine
|
||||
image: postgres:14-alpine
|
||||
container_name: picsur_postgres
|
||||
environment:
|
||||
POSTGRES_DB: picsur
|
||||
|
@ -103,8 +162,15 @@ volumes:
|
|||
picsur-data:
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
- @chennin for monthly donating 4$
|
||||
- @awg13 for donating 5$
|
||||
|
||||
## Api
|
||||
|
||||
Here is a usually up to date documentation of the api:
|
||||
|
||||
[![Run in Postman](https://run.pstmn.io/button.svg)](https://www.postman.com/rubikscraft-team/workspace/picsur/collection/1841871-78e559b6-4f39-4092-87c3-92fa29547d03)
|
||||
[![Run in Postman](https://run.pstmn.io/button.svg)](https://www.postman.com/caramel-team/workspace/picsur/collection/1841871-78e559b6-4f39-4092-87c3-92fa29547d03)
|
||||
|
||||
If you wish to build your own frontend or app for picsur, this will surely come in handy. Also take a look at the `./shared` folder in the source code, as it contains typescript schema definitions for the api.
|
||||
|
|
9
backend/.eslintrc.cjs
Normal file
9
backend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['../.eslintrc.cjs'],
|
||||
root: false,
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
|
@ -1,87 +1,85 @@
|
|||
{
|
||||
"name": "picsur-backend",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Backend for Picsur",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/rubikscraft/Picsur",
|
||||
"author": "Rubikscraft <contact@rubikscraft.nl>",
|
||||
"repository": "https://github.com/caramelfur/Picsur",
|
||||
"author": "Caramel <picsur@caramelfur.dev>",
|
||||
"type": "module",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"start": "nest start --exec \"node --es-module-specifier-resolution=node\"",
|
||||
"start:dev": "yarn clean && nest start --watch --exec \"node --es-module-specifier-resolution=node\"",
|
||||
"start:debug": "nest start --debug --watch --exec \"node --es-module-specifier-resolution=node\"",
|
||||
"start:prod": "node --es-module-specifier-resolution=node dist/main",
|
||||
"start": "nest start --exec \"node --experimental-loader=extensionless\"",
|
||||
"start:dev": "yarn clean && nest start --watch --exec \"node --experimental-loader=extensionless\"",
|
||||
"start:debug": "nest start --debug --watch --exec \"node --experimental-loader=extensionless\"",
|
||||
"start:prod": "node --experimental-loader=extensionless dist/main",
|
||||
"typeorm": "typeorm-ts-node-esm",
|
||||
"migrate": "yarn typeorm migration:generate -d ./src/datasource.ts",
|
||||
"migrate": "PICSUR_PRODUCTION=\"true\" yarn typeorm migration:generate -d ./src/datasource.ts",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"purge": "rm -rf dist && rm -rf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/helmet": "^9.1.0",
|
||||
"@fastify/multipart": "^7.1.0",
|
||||
"@fastify/static": "^6.5.0",
|
||||
"@nestjs/common": "^9.0.11",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.0.11",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-fastify": "^9.0.11",
|
||||
"@nestjs/serve-static": "^3.0.0",
|
||||
"@fastify/helmet": "^10.1.1",
|
||||
"@fastify/multipart": "^7.6.1",
|
||||
"@fastify/reply-from": "^9.3.0",
|
||||
"@fastify/static": "^6.10.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^2.3.4",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.1.0",
|
||||
"@nestjs/passport": "^9.0.3",
|
||||
"@nestjs/platform-fastify": "^10.0.0",
|
||||
"@nestjs/schedule": "^3.0.0",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@nestjs/throttler": "^4.0.0",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bmp-img": "^1.1.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bmp-img": "^1.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"fastify-static": "^4.7.0",
|
||||
"file-type": "^18.0.0",
|
||||
"ms": "^2.1.3",
|
||||
"p-timeout": "^6.0.0",
|
||||
"extensionless": "^1.4.5",
|
||||
"file-type": "^18.5.0",
|
||||
"is-docker": "^3.0.0",
|
||||
"ms": "2.1.3",
|
||||
"node-fetch": "^3.3.1",
|
||||
"p-timeout": "^6.1.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"pg": "^8.8.0",
|
||||
"pg": "^8.11.0",
|
||||
"picsur-shared": "*",
|
||||
"posix.js": "^0.1.1",
|
||||
"qoi-img": "^1.1.0",
|
||||
"qoi-img": "^2.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"sharp": "^0.30.7",
|
||||
"rimraf": "^5.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.32.1",
|
||||
"stream-parser": "^0.3.1",
|
||||
"thunks": "^4.9.6",
|
||||
"typeorm": "0.3.7",
|
||||
"zod": "^3.18.0"
|
||||
"typeorm": "0.3.16",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.1",
|
||||
"@nestjs/testing": "^9.0.11",
|
||||
"@nestjs/cli": "^10.0.1",
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@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.7.13",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/passport-strategy": "^0.2.35",
|
||||
"@types/sharp": "^0.30.5",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.35.1",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.8",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.3.1",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "4.8.2",
|
||||
"webpack": "^5.74.0"
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
@ -27,7 +30,7 @@ const imageCorsConfig = cors({
|
|||
const imageCorsOverride = (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Function,
|
||||
next: () => void,
|
||||
) => {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
|
||||
|
@ -41,10 +44,13 @@ const imageCorsOverride = (
|
|||
useExisting: ServeStaticConfigService,
|
||||
imports: [EarlyConfigModule],
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
DatabaseModule,
|
||||
AuthManagerModule,
|
||||
UsageManagerModule,
|
||||
DemoManagerModule,
|
||||
PicsurRoutesModule,
|
||||
PicsurLayersModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
|
11
backend/src/collections/apikey-db/apikey-db.module.ts
Normal file
11
backend/src/collections/apikey-db/apikey-db.module.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EApiKeyBackend } from '../../database/entities/apikey.entity';
|
||||
import { ApiKeyDbService } from './apikey-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EApiKeyBackend])],
|
||||
providers: [ApiKeyDbService],
|
||||
exports: [ApiKeyDbService],
|
||||
})
|
||||
export class ApiKeyDbModule {}
|
160
backend/src/collections/apikey-db/apikey-db.service.ts
Normal file
160
backend/src/collections/apikey-db/apikey-db.service.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
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/users/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyDbService {
|
||||
private readonly logger = new Logger(ApiKeyDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EApiKeyBackend)
|
||||
private readonly apikeyRepo: Repository<EApiKeyBackend>,
|
||||
) {}
|
||||
|
||||
async createApiKey(userid: string): AsyncFailable<EApiKeyBackend<string>> {
|
||||
const apikey = new EApiKeyBackend<string>();
|
||||
apikey.user = userid;
|
||||
apikey.created = new Date();
|
||||
// YYYY-MM-DD- followed by a random number
|
||||
apikey.name =
|
||||
new Date().toISOString().slice(0, 10) +
|
||||
'_' +
|
||||
Math.round(Math.random() * 100);
|
||||
apikey.key = generateRandomString(32); // Might collide, probably not
|
||||
|
||||
/*
|
||||
And yes it might be more secure here to sha256 the key, to ensure that they are not leaked upon db breach
|
||||
But this would mean that the user has to keep track of it themselves, and it makes many other things less smooth
|
||||
So just foking protect ya database, and we'll be fine
|
||||
*/
|
||||
|
||||
try {
|
||||
return this.apikeyRepo.save(apikey);
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EApiKeyBackend<string>> {
|
||||
try {
|
||||
const apikey = await this.apikeyRepo.findOne({
|
||||
where: {
|
||||
user:
|
||||
userid !== undefined
|
||||
? // This is stupid, but typeorm do typeorm
|
||||
({ id: userid } as any)
|
||||
: undefined,
|
||||
id,
|
||||
},
|
||||
loadRelationIds: true,
|
||||
});
|
||||
if (!apikey) return Fail(FT.NotFound, 'API key not found');
|
||||
return apikey as EApiKeyBackend<string>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async findMany(
|
||||
count: number,
|
||||
page: number,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<FindResult<EApiKeyBackend<string>>> {
|
||||
if (count < 1 || page < 0) return Fail(FT.UsrValidation, 'Invalid page');
|
||||
if (count > 100) return Fail(FT.UsrValidation, 'Too many results');
|
||||
|
||||
try {
|
||||
const [apikeys, amount] = await this.apikeyRepo.findAndCount({
|
||||
where: {
|
||||
user:
|
||||
userid !== undefined
|
||||
? // This is stupid, but typeorm do typeorm
|
||||
({ id: userid } as any)
|
||||
: undefined,
|
||||
},
|
||||
order: { created: 'DESC' },
|
||||
skip: count * page,
|
||||
take: count,
|
||||
loadRelationIds: true,
|
||||
});
|
||||
|
||||
return {
|
||||
results: apikeys as EApiKeyBackend<string>[],
|
||||
total: amount,
|
||||
page,
|
||||
pages: Math.ceil(amount / count),
|
||||
};
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async updateApiKey(
|
||||
id: string,
|
||||
name: string,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EApiKeyBackend<string>> {
|
||||
const apikey = await this.findOne(id, userid);
|
||||
if (HasFailed(apikey)) return apikey;
|
||||
|
||||
try {
|
||||
apikey.name = name;
|
||||
|
||||
return this.apikeyRepo.save(apikey);
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteApiKey(
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EApiKeyBackend<string>> {
|
||||
const apikeyToDelete = await this.findOne(id, userid);
|
||||
if (HasFailed(apikeyToDelete)) return apikeyToDelete;
|
||||
|
||||
const apiKeyCopy = { ...apikeyToDelete };
|
||||
try {
|
||||
await this.apikeyRepo.remove(apikeyToDelete);
|
||||
return apiKeyCopy as EApiKeyBackend<string>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(key: string): AsyncFailable<EApiKeyBackend<EUserBackend>> {
|
||||
try {
|
||||
const apikey = await this.apikeyRepo.findOne({
|
||||
where: { key },
|
||||
relations: ['user'],
|
||||
});
|
||||
if (!apikey) return Fail(FT.NotFound, 'API key not found');
|
||||
|
||||
this.updateLastUsed(apikey);
|
||||
|
||||
return apikey as EApiKeyBackend<EUserBackend>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLastUsed(apikey: EApiKeyBackend) {
|
||||
(async () => {
|
||||
apikey.last_used = new Date();
|
||||
this.apikeyRepo.save(apikey);
|
||||
})().catch(this.logger.error.bind(this.logger));
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { In, Repository } from '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 { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { In, LessThan, Repository } from 'typeorm';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ImageDBService {
|
||||
constructor(
|
||||
@InjectRepository(EImageBackend)
|
||||
private readonly imageRepo: Repository<EImageBackend>,
|
||||
|
||||
@InjectRepository(EImageFileBackend)
|
||||
private readonly imageFileRepo: Repository<EImageFileBackend>,
|
||||
|
||||
@InjectRepository(EImageDerivativeBackend)
|
||||
private readonly imageDerivativeRepo: Repository<EImageDerivativeBackend>,
|
||||
) {}
|
||||
|
||||
public async create(userid: string): AsyncFailable<EImageBackend> {
|
||||
public async create(
|
||||
userid: string,
|
||||
filename: string,
|
||||
withDeleteKey: boolean,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
let imageEntity = new EImageBackend();
|
||||
imageEntity.user_id = userid;
|
||||
imageEntity.created = new Date();
|
||||
imageEntity.file_name = filename;
|
||||
if (withDeleteKey) imageEntity.delete_key = generateRandomString(32);
|
||||
|
||||
try {
|
||||
imageEntity = await this.imageRepo.save(imageEntity, { reload: true });
|
||||
imageEntity = await this.imageRepo.save(imageEntity, {
|
||||
reload: true,
|
||||
});
|
||||
|
||||
if (imageEntity.delete_key === null) delete imageEntity.delete_key;
|
||||
return imageEntity;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
|
||||
return imageEntity;
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
|
@ -62,6 +64,7 @@ export class ImageDBService {
|
|||
const [found, amount] = await this.imageRepo.findAndCount({
|
||||
skip: count * page,
|
||||
take: count,
|
||||
order: { created: 'DESC' },
|
||||
where: {
|
||||
user_id: userid,
|
||||
},
|
||||
|
@ -80,6 +83,39 @@ 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,
|
||||
options: Partial<Pick<EImageBackend, 'file_name' | 'expires_at'>>,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
try {
|
||||
const found = await this.imageRepo.findOne({
|
||||
where: { id, user_id: userid },
|
||||
});
|
||||
|
||||
if (!found) return Fail(FT.NotFound, 'Image not found');
|
||||
|
||||
if (options.file_name !== undefined) found.file_name = options.file_name;
|
||||
|
||||
if (options.expires_at !== undefined)
|
||||
found.expires_at = options.expires_at;
|
||||
|
||||
await this.imageRepo.save(found);
|
||||
|
||||
return found;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(
|
||||
ids: string[],
|
||||
userid: string | undefined,
|
||||
|
@ -97,18 +133,10 @@ export class ImageDBService {
|
|||
|
||||
const available_ids = deletable_images.map((i) => i.id);
|
||||
|
||||
if (available_ids.length === 0) return Fail(FT.NotFound, 'Images not found');
|
||||
if (available_ids.length === 0)
|
||||
return Fail(FT.NotFound, 'Images not found');
|
||||
|
||||
await Promise.all([
|
||||
this.imageDerivativeRepo.delete({
|
||||
image_id: In(available_ids),
|
||||
}),
|
||||
this.imageFileRepo.delete({
|
||||
image_id: In(available_ids),
|
||||
}),
|
||||
|
||||
this.imageRepo.delete({ id: In(available_ids) }),
|
||||
]);
|
||||
await this.imageRepo.delete({ id: In(available_ids) });
|
||||
|
||||
return deletable_images;
|
||||
} catch (e) {
|
||||
|
@ -116,17 +144,49 @@ export class ImageDBService {
|
|||
}
|
||||
}
|
||||
|
||||
public async deleteWithKey(
|
||||
id: string,
|
||||
key: string,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
try {
|
||||
const found = await this.imageRepo.findOne({
|
||||
where: { id, delete_key: key },
|
||||
});
|
||||
|
||||
if (!found) return Fail(FT.NotFound, 'Image not found');
|
||||
|
||||
await this.imageRepo.delete({ id: found.id });
|
||||
|
||||
return found;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAll(IAmSure: boolean): AsyncFailable<true> {
|
||||
if (!IAmSure)
|
||||
return Fail(FT.SysValidation, 'You must confirm that you want to delete all images');
|
||||
return Fail(
|
||||
FT.SysValidation,
|
||||
'You must confirm that you want to delete all images',
|
||||
);
|
||||
|
||||
try {
|
||||
await this.imageDerivativeRepo.delete({});
|
||||
await this.imageFileRepo.delete({});
|
||||
await this.imageRepo.delete({});
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async cleanupExpired(): AsyncFailable<number> {
|
||||
try {
|
||||
const res = await this.imageRepo.delete({
|
||||
expires_at: LessThan(new Date()),
|
||||
});
|
||||
|
||||
return res.affected ?? 0;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
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/failable';
|
||||
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 +62,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 +168,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;
|
||||
|
|
|
@ -2,29 +2,29 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import {
|
||||
DecodedPref,
|
||||
PrefValueType,
|
||||
PrefValueTypeStrings
|
||||
PrefValueTypeStrings,
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
Failable,
|
||||
FT,
|
||||
HasFailed
|
||||
} from 'picsur-shared/dist/types';
|
||||
HasFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
|
||||
type Enum = Record<string, string>;
|
||||
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;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PreferenceCommonService {
|
||||
private readonly logger = new Logger('PreferenceCommonService');
|
||||
private readonly logger = new Logger(PreferenceCommonService.name);
|
||||
|
||||
// Preferences values are only validated upon encoding, not decoding
|
||||
// The preference keys are always validated
|
||||
|
@ -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,12 +1,12 @@
|
|||
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 { SysPreferenceService } from './sys-preference-db.service';
|
||||
import { UsrPreferenceService } from './usr-preference-db.service';
|
||||
import { SysPreferenceDbService } from './sys-preference-db.service';
|
||||
import { UsrPreferenceDbService } from './usr-preference-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -14,11 +14,11 @@ import { UsrPreferenceService } from './usr-preference-db.service';
|
|||
EarlyConfigModule,
|
||||
],
|
||||
providers: [
|
||||
SysPreferenceService,
|
||||
UsrPreferenceService,
|
||||
SysPreferenceDbService,
|
||||
UsrPreferenceDbService,
|
||||
PreferenceDefaultsService,
|
||||
PreferenceCommonService,
|
||||
],
|
||||
exports: [SysPreferenceService, UsrPreferenceService],
|
||||
exports: [SysPreferenceDbService, UsrPreferenceDbService],
|
||||
})
|
||||
export class PreferenceModule {}
|
||||
export class PreferenceDbModule {}
|
||||
|
|
|
@ -11,19 +11,21 @@ import { EarlyJwtConfigService } from '../../config/early/early-jwt.config.servi
|
|||
|
||||
@Injectable()
|
||||
export class PreferenceDefaultsService {
|
||||
private readonly logger = new Logger('PreferenceDefaultsService');
|
||||
private readonly logger = new Logger(PreferenceDefaultsService.name);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,26 +3,32 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import {
|
||||
DecodedSysPref,
|
||||
PrefValueType,
|
||||
PrefValueTypeStrings
|
||||
PrefValueTypeStrings,
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
SysPreference,
|
||||
SysPreferenceList,
|
||||
SysPreferenceValidators,
|
||||
SysPreferenceValueTypes,
|
||||
} from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
ESysPreferenceBackend,
|
||||
ESysPreferenceSchema
|
||||
} from '../../database/entities/sys-preference.entity';
|
||||
import {
|
||||
SysPreferenceList,
|
||||
SysPreferenceValueTypes
|
||||
} from '../../models/constants/syspreferences.const';
|
||||
import { MutexFallBack } from '../../models/util/mutex-fallback';
|
||||
ESysPreferenceSchema,
|
||||
} 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';
|
||||
|
||||
@Injectable()
|
||||
export class SysPreferenceService {
|
||||
private readonly logger = new Logger('SysPreferenceService');
|
||||
export class SysPreferenceDbService {
|
||||
private readonly logger = new Logger(SysPreferenceDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ESysPreferenceBackend)
|
||||
|
@ -36,7 +42,7 @@ export class SysPreferenceService {
|
|||
value: PrefValueType,
|
||||
): AsyncFailable<DecodedSysPref> {
|
||||
// Validate
|
||||
let sysPreference = await this.encodeSysPref(key, value);
|
||||
const sysPreference = await this.encodeSysPref(key, value);
|
||||
if (HasFailed(sysPreference)) return sysPreference;
|
||||
|
||||
// Set
|
||||
|
@ -59,7 +65,7 @@ export class SysPreferenceService {
|
|||
|
||||
public async getPreference(key: string): AsyncFailable<DecodedSysPref> {
|
||||
// Validate
|
||||
let validatedKey = this.prefCommon.validatePrefKey(key, SysPreference);
|
||||
const validatedKey = this.prefCommon.validatePrefKey(key, SysPreference);
|
||||
if (HasFailed(validatedKey)) return validatedKey;
|
||||
|
||||
// See the comment in 'mutex-fallback.ts' for why we are using a mutex here
|
||||
|
@ -85,7 +91,7 @@ export class SysPreferenceService {
|
|||
|
||||
// Return
|
||||
return this.prefCommon.DecodePref(
|
||||
result.data,
|
||||
result.data as any,
|
||||
SysPreference,
|
||||
SysPreferenceValueTypes,
|
||||
);
|
||||
|
@ -111,7 +117,7 @@ export class SysPreferenceService {
|
|||
key: string,
|
||||
type: PrefValueTypeStrings,
|
||||
): AsyncFailable<PrefValueType> {
|
||||
let pref = await this.getPreference(key);
|
||||
const pref = await this.getPreference(key);
|
||||
if (HasFailed(pref)) return pref;
|
||||
if (pref.type !== type)
|
||||
return Fail(FT.UsrValidation, 'Invalid preference type');
|
||||
|
@ -121,7 +127,7 @@ export class SysPreferenceService {
|
|||
|
||||
public async getAllPreferences(): AsyncFailable<DecodedSysPref[]> {
|
||||
// TODO: We are fetching each value invidually, we should fetch all at once
|
||||
let internalSysPrefs = await Promise.all(
|
||||
const internalSysPrefs = await Promise.all(
|
||||
SysPreferenceList.map((key) => this.getPreference(key)),
|
||||
);
|
||||
if (internalSysPrefs.some((pref) => HasFailed(pref))) {
|
||||
|
@ -136,7 +142,7 @@ export class SysPreferenceService {
|
|||
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,7 +157,13 @@ export class SysPreferenceService {
|
|||
);
|
||||
if (HasFailed(validated)) return validated;
|
||||
|
||||
let verifySysPreference = new ESysPreferenceBackend();
|
||||
const valueValidated =
|
||||
SysPreferenceValidators[key as SysPreference].safeParse(value);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
const verifySysPreference = new ESysPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
verifySysPreference.value = validated.value;
|
||||
|
||||
|
|
|
@ -3,26 +3,32 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import {
|
||||
DecodedUsrPref,
|
||||
PrefValueType,
|
||||
PrefValueTypeStrings
|
||||
PrefValueTypeStrings,
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
UsrPreference,
|
||||
UsrPreferenceList,
|
||||
UsrPreferenceValidators,
|
||||
UsrPreferenceValueTypes,
|
||||
} from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
EUsrPreferenceBackend,
|
||||
EUsrPreferenceSchema
|
||||
} from '../../database/entities/usr-preference.entity';
|
||||
import {
|
||||
UsrPreferenceList,
|
||||
UsrPreferenceValueTypes
|
||||
} from '../../models/constants/usrpreferences.const';
|
||||
import { MutexFallBack } from '../../models/util/mutex-fallback';
|
||||
EUsrPreferenceSchema,
|
||||
} 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';
|
||||
|
||||
@Injectable()
|
||||
export class UsrPreferenceService {
|
||||
private readonly logger = new Logger('UsrPreferenceService');
|
||||
export class UsrPreferenceDbService {
|
||||
private readonly logger = new Logger(UsrPreferenceDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EUsrPreferenceBackend)
|
||||
|
@ -37,7 +43,7 @@ export class UsrPreferenceService {
|
|||
value: PrefValueType,
|
||||
): AsyncFailable<DecodedUsrPref> {
|
||||
// Validate
|
||||
let usrPreference = await this.encodeUsrPref(userid, key, value);
|
||||
const usrPreference = await this.encodeUsrPref(userid, key, value);
|
||||
if (HasFailed(usrPreference)) return usrPreference;
|
||||
|
||||
// Set
|
||||
|
@ -65,7 +71,7 @@ export class UsrPreferenceService {
|
|||
key: string,
|
||||
): AsyncFailable<DecodedUsrPref> {
|
||||
// Validate
|
||||
let validatedKey = this.prefCommon.validatePrefKey(key, UsrPreference);
|
||||
const validatedKey = this.prefCommon.validatePrefKey(key, UsrPreference);
|
||||
if (HasFailed(validatedKey)) return validatedKey;
|
||||
|
||||
// See the comment in 'mutex-fallback.ts' for why we are using a mutex here
|
||||
|
@ -91,7 +97,7 @@ export class UsrPreferenceService {
|
|||
|
||||
// Return
|
||||
const unpacked = this.prefCommon.DecodePref(
|
||||
result.data,
|
||||
result.data as any,
|
||||
UsrPreference,
|
||||
UsrPreferenceValueTypes,
|
||||
);
|
||||
|
@ -144,7 +150,7 @@ export class UsrPreferenceService {
|
|||
key: string,
|
||||
type: PrefValueTypeStrings,
|
||||
): AsyncFailable<PrefValueType> {
|
||||
let pref = await this.getPreference(userid, key);
|
||||
const pref = await this.getPreference(userid, key);
|
||||
if (HasFailed(pref)) return pref;
|
||||
if (pref.type !== type)
|
||||
return Fail(FT.UsrValidation, 'Invalid preference type');
|
||||
|
@ -156,7 +162,7 @@ export class UsrPreferenceService {
|
|||
userid: string,
|
||||
): AsyncFailable<DecodedUsrPref[]> {
|
||||
// TODO: We are fetching each value invidually, we should fetch all at once
|
||||
let internalSysPrefs = await Promise.all(
|
||||
const internalSysPrefs = await Promise.all(
|
||||
UsrPreferenceList.map((key) => this.getPreference(userid, key)),
|
||||
);
|
||||
if (internalSysPrefs.some((pref) => HasFailed(pref))) {
|
||||
|
@ -175,7 +181,7 @@ export class UsrPreferenceService {
|
|||
return this.setPreference(
|
||||
userid,
|
||||
key,
|
||||
this.defaultsService.usrDefaults[key](),
|
||||
this.defaultsService.getUsrDefault(key),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -192,7 +198,13 @@ export class UsrPreferenceService {
|
|||
);
|
||||
if (HasFailed(validated)) return validated;
|
||||
|
||||
let verifySysPreference = new EUsrPreferenceBackend();
|
||||
const valueValidated =
|
||||
UsrPreferenceValidators[key as UsrPreference].safeParse(value);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
const verifySysPreference = new EUsrPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
verifySysPreference.value = validated.value;
|
||||
verifySysPreference.user_id = userid;
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
||||
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,
|
||||
SystemRolesList
|
||||
SystemRolesList,
|
||||
} from '../../models/constants/roles.const';
|
||||
import { RolesService } from './role-db.service';
|
||||
import { RoleDbService } from './role-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [EarlyConfigModule, TypeOrmModule.forFeature([ERoleBackend])],
|
||||
providers: [RolesService],
|
||||
exports: [RolesService],
|
||||
providers: [RoleDbService],
|
||||
exports: [RoleDbService],
|
||||
})
|
||||
export class RolesModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('RolesModule');
|
||||
export class RoleDbModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(RoleDbModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly rolesService: RolesService,
|
||||
private readonly rolesService: RoleDbService,
|
||||
private readonly hostConfig: HostConfigService,
|
||||
) {}
|
||||
|
||||
|
@ -28,7 +28,7 @@ export class RolesModule implements OnModuleInit {
|
|||
// Nuking roles in dev environment makes testing easier
|
||||
// This ensures that the roles are always started with their default permissions
|
||||
if (!this.hostConfig.isProduction()) {
|
||||
await this.nukeRoles();
|
||||
//await this.nukeRoles();
|
||||
}
|
||||
|
||||
await this.ensureSystemRolesExist();
|
||||
|
@ -48,7 +48,10 @@ export class RolesModule implements OnModuleInit {
|
|||
this.logger.verbose(`Ensuring system role "${systemRole}" exists`);
|
||||
|
||||
const exists = await this.rolesService.exists(systemRole);
|
||||
if (exists) continue;
|
||||
if (exists) {
|
||||
this.logger.verbose(`System role "${systemRole}" already exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newRole = await this.rolesService.create(
|
||||
systemRole,
|
||||
|
|
|
@ -6,26 +6,25 @@ import {
|
|||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
HasSuccess
|
||||
} from 'picsur-shared/dist/types';
|
||||
HasSuccess,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
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,
|
||||
UndeletableRolesList
|
||||
UndeletableRolesList,
|
||||
} from '../../models/constants/roles.const';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService {
|
||||
private readonly logger = new Logger('UsersService');
|
||||
export class RoleDbService {
|
||||
private readonly logger = new Logger(RoleDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ERoleBackend)
|
||||
private readonly rolesRepository: Repository<ERoleBackend>,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
name: string,
|
||||
|
@ -34,7 +33,7 @@ export class RolesService {
|
|||
if (await this.exists(name))
|
||||
return Fail(FT.Conflict, 'Role already exists');
|
||||
|
||||
let role = new ERoleBackend();
|
||||
const role = new ERoleBackend();
|
||||
role.name = name;
|
||||
role.permissions = permissions;
|
||||
|
||||
|
@ -106,7 +105,7 @@ export class RolesService {
|
|||
role: string | ERoleBackend,
|
||||
permissions: Permissions,
|
||||
// Extra bypass for internal use
|
||||
allowImmutable: boolean = false,
|
||||
allowImmutable = false,
|
||||
): AsyncFailable<ERoleBackend> {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
@ -141,6 +140,7 @@ export class RolesService {
|
|||
try {
|
||||
const found = await this.rolesRepository.find({
|
||||
where: { name: In(names) },
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
|
||||
if (!found) return Fail(FT.NotFound, 'No roles found');
|
||||
|
@ -152,7 +152,9 @@ export class RolesService {
|
|||
|
||||
public async findAll(): AsyncFailable<ERoleBackend[]> {
|
||||
try {
|
||||
const found = await this.rolesRepository.find();
|
||||
const found = await this.rolesRepository.find({
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
if (!found) return Fail(FT.NotFound, 'No roles found');
|
||||
return found;
|
||||
} catch (e) {
|
||||
|
@ -164,7 +166,7 @@ export class RolesService {
|
|||
return HasSuccess(await this.findOne(name));
|
||||
}
|
||||
|
||||
public async nukeSystemRoles(IAmSure: boolean = false): AsyncFailable<true> {
|
||||
public async nukeSystemRoles(IAmSure = false): AsyncFailable<true> {
|
||||
if (!IAmSure)
|
||||
return Fail(
|
||||
FT.SysValidation,
|
||||
|
|
|
@ -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/failable';
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,29 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
||||
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 { PreferenceModule } from '../preference-db/preference-db.module';
|
||||
import { RolesModule } from '../role-db/role-db.module';
|
||||
import { UsersService } from './user-db.service';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
EarlyConfigModule,
|
||||
RolesModule,
|
||||
PreferenceModule,
|
||||
RoleDbModule,
|
||||
PreferenceDbModule,
|
||||
TypeOrmModule.forFeature([EUserBackend]),
|
||||
],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
providers: [UserDbService],
|
||||
exports: [UserDbService],
|
||||
})
|
||||
export class UsersModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('UsersModule');
|
||||
export class UserDbModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(UserDbModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly usersService: UserDbService,
|
||||
private readonly authConfigService: AuthConfigService,
|
||||
) {}
|
||||
|
||||
|
|
|
@ -7,35 +7,35 @@ import {
|
|||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
HasSuccess
|
||||
} from 'picsur-shared/dist/types';
|
||||
HasSuccess,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
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,
|
||||
SoulBoundRolesList
|
||||
SoulBoundRolesList,
|
||||
} from '../../models/constants/roles.const';
|
||||
import {
|
||||
ImmutableUsersList,
|
||||
LockedLoginUsersList,
|
||||
UndeletableUsersList
|
||||
UndeletableUsersList,
|
||||
} from '../../models/constants/special-users.const';
|
||||
import { GetCols } from '../../models/util/collection';
|
||||
import { SysPreferenceService } from '../preference-db/sys-preference-db.service';
|
||||
import { RolesService } from '../role-db/role-db.service';
|
||||
import { GetCols } from '../../util/collection';
|
||||
import { SysPreferenceDbService } from '../preference-db/sys-preference-db.service';
|
||||
import { RoleDbService } from '../role-db/role-db.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger('UsersService');
|
||||
export class UserDbService {
|
||||
private readonly logger = new Logger(UserDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EUserBackend)
|
||||
private readonly usersRepository: Repository<EUserBackend>,
|
||||
private readonly rolesService: RolesService,
|
||||
private readonly prefService: SysPreferenceService,
|
||||
private readonly rolesService: RoleDbService,
|
||||
private readonly prefService: SysPreferenceDbService,
|
||||
) {}
|
||||
|
||||
// Creation and deletion
|
||||
|
@ -53,7 +53,7 @@ export class UsersService {
|
|||
const strength = await this.getBCryptStrength();
|
||||
const hashedPassword = await bcrypt.hash(password, strength);
|
||||
|
||||
let user = new EUserBackend();
|
||||
const user = new EUserBackend();
|
||||
user.username = username;
|
||||
user.hashed_password = hashedPassword;
|
||||
if (byPassRoleCheck) {
|
||||
|
@ -73,15 +73,15 @@ export class UsersService {
|
|||
}
|
||||
|
||||
public async delete(uuid: string): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.findOne(uuid);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
const userToDelete = await this.findOne(uuid);
|
||||
if (HasFailed(userToDelete)) return userToDelete;
|
||||
|
||||
if (UndeletableUsersList.includes(userToModify.username)) {
|
||||
if (UndeletableUsersList.includes(userToDelete.username)) {
|
||||
return Fail(FT.Permission, 'Cannot delete system user');
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.usersRepository.remove(userToModify);
|
||||
return await this.usersRepository.remove(userToDelete);
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
|
@ -166,26 +166,49 @@ export class UsersService {
|
|||
password: string,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const user = await this.findByUsername(username, true);
|
||||
if (HasFailed(user)) return user;
|
||||
if (HasFailed(user)) {
|
||||
if (user.getType() === FT.NotFound)
|
||||
return Fail(
|
||||
FT.Authentication,
|
||||
'Wrong username or password',
|
||||
user.getDebugMessage(),
|
||||
);
|
||||
else return user;
|
||||
}
|
||||
|
||||
if (LockedLoginUsersList.includes(user.username)) {
|
||||
// Error should be kept in backend
|
||||
return Fail(FT.Authentication, 'Wrong username');
|
||||
return Fail(FT.Authentication, 'Wrong username or password');
|
||||
}
|
||||
|
||||
if (!(await bcrypt.compare(password, user.hashed_password ?? '')))
|
||||
return Fail(FT.Authentication, 'Wrong password');
|
||||
return Fail(FT.Authentication, 'Wrong username or password');
|
||||
|
||||
return await this.findOne(user.id ?? '');
|
||||
}
|
||||
|
||||
// Listing
|
||||
|
||||
public async checkUsername(username: string): AsyncFailable<{
|
||||
available: boolean;
|
||||
}> {
|
||||
try {
|
||||
const found = await this.usersRepository.findOne({
|
||||
where: { username },
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
return { available: !found };
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async findByUsername(
|
||||
username: string,
|
||||
// Also fetch fields that aren't normally sent to the client
|
||||
// (e.g. hashed password)
|
||||
getPrivate: boolean = false,
|
||||
getPrivate = false,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
try {
|
||||
const found = await this.usersRepository.findOne({
|
||||
|
@ -224,6 +247,7 @@ export class UsersService {
|
|||
const [users, amount] = await this.usersRepository.findAndCount({
|
||||
take: count,
|
||||
skip: count * page,
|
||||
order: { username: 'ASC' },
|
||||
});
|
||||
|
||||
if (users === undefined) return Fail(FT.NotFound, 'Users not found');
|
||||
|
@ -239,6 +263,14 @@ export class UsersService {
|
|||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ export class AuthConfigService {
|
|||
public getDefaultAdminPassword(): string {
|
||||
return ParseString(
|
||||
this.configService.get(`${EnvPrefix}ADMIN_PASSWORD`),
|
||||
'admin',
|
||||
'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 {}
|
||||
|
|
|
@ -3,20 +3,28 @@ import { ConfigService } from '@nestjs/config';
|
|||
import {
|
||||
ParseBool,
|
||||
ParseInt,
|
||||
ParseString
|
||||
ParseString,
|
||||
} from 'picsur-shared/dist/util/parse-simple';
|
||||
import { EnvPrefix } from '../config.static';
|
||||
|
||||
@Injectable()
|
||||
export class HostConfigService {
|
||||
private readonly logger = new Logger('HostConfigService');
|
||||
private readonly logger = new Logger(HostConfigService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.logger.log('Production: ' + this.isProduction());
|
||||
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 {
|
||||
|
@ -42,6 +50,14 @@ export class HostConfigService {
|
|||
return ParseBool(this.configService.get(`${EnvPrefix}PRODUCTION`), false);
|
||||
}
|
||||
|
||||
public isVerbose() {
|
||||
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');
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { EnvPrefix } from '../config.static';
|
|||
|
||||
@Injectable()
|
||||
export class MultipartConfigService {
|
||||
private readonly logger = new Logger('MultipartConfigService');
|
||||
private readonly logger = new Logger(MultipartConfigService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.logger.log('Max file size: ' + this.getMaxFileSize());
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
20
backend/src/config/early/redis.config.service.ts
Normal file
20
backend/src/config/early/redis.config.service.ts
Normal file
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ServeStaticModuleOptions,
|
||||
ServeStaticModuleOptionsFactory
|
||||
ServeStaticModuleOptionsFactory,
|
||||
} from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import { ParseString } from 'picsur-shared/dist/util/parse-simple';
|
||||
|
@ -12,7 +12,7 @@ import { EnvPrefix, PackageRoot } from '../config.static';
|
|||
export class ServeStaticConfigService
|
||||
implements ServeStaticModuleOptionsFactory
|
||||
{
|
||||
private readonly logger = new Logger('ServeStaticConfigService');
|
||||
private readonly logger = new Logger(ServeStaticConfigService.name);
|
||||
|
||||
private defaultLocation = join(PackageRoot, '../frontend/dist');
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmOptionsFactory } from '@nestjs/typeorm';
|
||||
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
|
||||
import { ParseInt, ParseString } from 'picsur-shared/dist/util/parse-simple';
|
||||
import { EntityList } from '../../database/entities';
|
||||
import { MigrationList } from '../../database/migrations';
|
||||
import { EntityList } from '../../database/entities/index';
|
||||
import { MigrationList } from '../../database/migrations/index';
|
||||
import { DefaultName, EnvPrefix } from '../config.static';
|
||||
import { HostConfigService } from './host.config.service';
|
||||
|
||||
@Injectable()
|
||||
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
|
||||
private readonly logger = new Logger('TypeOrmConfigService');
|
||||
private readonly logger = new Logger(TypeOrmConfigService.name);
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
|
@ -31,43 +31,42 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory {
|
|||
this.configService.get(`${EnvPrefix}DB_HOST`),
|
||||
'localhost',
|
||||
),
|
||||
port: ParseInt(
|
||||
this.configService.get<number>(`${EnvPrefix}DB_PORT`),
|
||||
5432,
|
||||
),
|
||||
port: ParseInt(this.configService.get(`${EnvPrefix}DB_PORT`), 5432),
|
||||
username: ParseString(
|
||||
this.configService.get<string>(`${EnvPrefix}DB_USERNAME`),
|
||||
this.configService.get(`${EnvPrefix}DB_USERNAME`),
|
||||
DefaultName,
|
||||
),
|
||||
password: ParseString(
|
||||
this.configService.get<string>(`${EnvPrefix}DB_PASSWORD`),
|
||||
this.configService.get(`${EnvPrefix}DB_PASSWORD`),
|
||||
DefaultName,
|
||||
),
|
||||
database: ParseString(
|
||||
this.configService.get<string>(`${EnvPrefix}DB_DATABASE`),
|
||||
this.configService.get(`${EnvPrefix}DB_DATABASE`),
|
||||
DefaultName,
|
||||
),
|
||||
};
|
||||
return varOptions;
|
||||
}
|
||||
|
||||
public createTypeOrmOptions(connectionName?: string) {
|
||||
public createTypeOrmOptions() {
|
||||
const varOptions = this.getTypeOrmServerOptions();
|
||||
return {
|
||||
type: 'postgres' as 'postgres',
|
||||
synchronize: false,
|
||||
type: 'postgres' as const,
|
||||
synchronize: !this.hostService.isProduction(),
|
||||
|
||||
migrationsRun: true,
|
||||
|
||||
entities: EntityList,
|
||||
migrations: MigrationList,
|
||||
|
||||
useUTC: true,
|
||||
|
||||
cli: {
|
||||
migrationsDir: 'src/database/migrations',
|
||||
entitiesDir: 'src/database/entities',
|
||||
},
|
||||
|
||||
...varOptions,
|
||||
};
|
||||
} as TypeOrmModuleOptions;
|
||||
}
|
||||
}
|
||||
|
|
25
backend/src/config/late/info.config.service.ts
Normal file
25
backend/src/config/late/info.config.service.ts
Normal file
|
@ -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/failable';
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import { FactoryProvider, Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtModuleOptions, JwtOptionsFactory } from '@nestjs/jwt';
|
||||
import ms from 'ms';
|
||||
import { ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { ThrowIfFailed } from 'picsur-shared/dist/types/failable';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtConfigService implements JwtOptionsFactory {
|
||||
private readonly logger = new Logger('JwtConfigService');
|
||||
private readonly logger = new Logger(JwtConfigService.name);
|
||||
|
||||
constructor(private readonly prefService: SysPreferenceService) {
|
||||
constructor(private readonly prefService: SysPreferenceDbService) {
|
||||
this.printDebug().catch(this.logger.error);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { PreferenceModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { PreferenceDbModule } from '../../collections/preference-db/preference-db.module';
|
||||
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,16 +13,21 @@ import { JwtConfigService } from './jwt.config.service';
|
|||
// Otherwise we will create a circular depedency
|
||||
|
||||
@Module({
|
||||
imports: [PreferenceModule, 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');
|
||||
private readonly logger = new Logger(LateConfigModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly envJwtConfigService: EarlyJwtConfigService,
|
||||
private readonly prefService: SysPreferenceService,
|
||||
private readonly prefService: SysPreferenceDbService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
|
|
58
backend/src/config/late/usage.config.service.ts
Normal file
58
backend/src/config/late/usage.config.service.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
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/failable';
|
||||
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;
|
||||
}
|
||||
}
|
54
backend/src/database/entities/apikey.entity.ts
Normal file
54
backend/src/database/entities/apikey.entity.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
import { EUserBackend } from './users/user.entity';
|
||||
|
||||
const OverriddenEApiKeySchema = EApiKeySchema.omit({ user: true }).merge(
|
||||
z.object({
|
||||
user: z.string().or(z.object({})),
|
||||
}),
|
||||
);
|
||||
type OverriddenEApiKey = z.infer<typeof OverriddenEApiKeySchema>;
|
||||
|
||||
@Entity()
|
||||
export class EApiKeyBackend<
|
||||
T extends string | EUserBackend = string | EUserBackend,
|
||||
> implements OverriddenEApiKey
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
key: string;
|
||||
|
||||
@ManyToOne(() => EUserBackend, (user) => user.apikeys, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
user: T;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: 'timestamptz',
|
||||
nullable: false,
|
||||
})
|
||||
created: Date;
|
||||
|
||||
@Column({
|
||||
type: 'timestamptz',
|
||||
nullable: true,
|
||||
})
|
||||
last_used: Date;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@Unique(['image_id', 'key'])
|
||||
export class EImageDerivativeBackend {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
private _id?: string;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
image_id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
key: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
filetype: string;
|
||||
|
||||
@Column({ name: 'last_read', nullable: false })
|
||||
last_read: Date;
|
||||
|
||||
// Binary data
|
||||
@Column({ type: 'bytea', nullable: false })
|
||||
data: Buffer;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class EImageBackend implements EImage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
user_id: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
created: Date;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { EImageBackend } from './image.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['image_id', 'key'])
|
||||
export class EImageDerivativeBackend {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
private _id?: string;
|
||||
|
||||
// We do a little trickery
|
||||
@Index()
|
||||
@ManyToOne(() => EImageBackend, (image) => image.derivatives, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
private _image?: any;
|
||||
|
||||
@Column({
|
||||
name: 'image_id',
|
||||
})
|
||||
image_id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
key: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
filetype: string;
|
||||
|
||||
@Column({
|
||||
type: 'timestamptz',
|
||||
name: 'last_read',
|
||||
nullable: false,
|
||||
})
|
||||
last_read: Date;
|
||||
|
||||
// Binary data
|
||||
@Column({ type: 'bytea', nullable: false })
|
||||
data: Buffer;
|
||||
}
|
|
@ -1,5 +1,14 @@
|
|||
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { EImageBackend } from './image.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['image_id', 'variant'])
|
||||
|
@ -7,8 +16,18 @@ export class EImageFileBackend {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
private _id?: string;
|
||||
|
||||
// We do a little trickery
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
@ManyToOne(() => EImageBackend, (image) => image.files, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
private _image?: any;
|
||||
|
||||
@Column({
|
||||
name: 'image_id',
|
||||
})
|
||||
image_id: string;
|
||||
|
||||
@Index()
|
46
backend/src/database/entities/images/image.entity.ts
Normal file
46
backend/src/database/entities/images/image.entity.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { EImageDerivativeBackend } from './image-derivative.entity';
|
||||
import { EImageFileBackend } from './image-file.entity';
|
||||
|
||||
@Entity()
|
||||
export class EImageBackend implements EImage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'uuid',
|
||||
})
|
||||
user_id: string;
|
||||
|
||||
@Column({
|
||||
type: 'timestamptz',
|
||||
nullable: false,
|
||||
})
|
||||
created: Date;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 'image',
|
||||
})
|
||||
file_name: string;
|
||||
|
||||
@Column({
|
||||
type: 'timestamptz',
|
||||
nullable: true,
|
||||
})
|
||||
expires_at: Date | null;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
select: false,
|
||||
})
|
||||
delete_key?: string;
|
||||
|
||||
@OneToMany(() => EImageDerivativeBackend, (derivative) => derivative.image_id)
|
||||
derivatives: EImageDerivativeBackend[];
|
||||
|
||||
@OneToMany(() => EImageFileBackend, (file) => file.image_id)
|
||||
files: EImageFileBackend[];
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
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 { EApiKeyBackend } from './apikey.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,
|
||||
|
@ -14,4 +16,6 @@ export const EntityList = [
|
|||
ERoleBackend,
|
||||
ESysPreferenceBackend,
|
||||
EUsrPreferenceBackend,
|
||||
EApiKeyBackend,
|
||||
ESystemStateBackend,
|
||||
];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IsEntityID } from 'picsur-shared/dist/validators/entity-id.validator';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import z from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const ESysPreferenceSchema = z.object({
|
||||
id: IsEntityID().optional(),
|
14
backend/src/database/entities/system/system-state.entity.ts
Normal file
14
backend/src/database/entities/system/system-state.entity.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -1,6 +1,15 @@
|
|||
import { IsEntityID } from 'picsur-shared/dist/validators/entity-id.validator';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import z from 'zod';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import * as z from 'zod';
|
||||
import { EUserBackend } from '../users/user.entity';
|
||||
|
||||
export const EUsrPreferenceSchema = z.object({
|
||||
id: IsEntityID().optional(),
|
||||
|
@ -23,7 +32,17 @@ export class EUsrPreferenceBackend implements EUsrPreference {
|
|||
@Column({ nullable: false })
|
||||
value: string;
|
||||
|
||||
// We do a little trickery
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
@ManyToOne(() => EUserBackend, (user) => user.preferences, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
private _user?: any;
|
||||
|
||||
@Column({
|
||||
name: 'user_id',
|
||||
})
|
||||
user_id: string;
|
||||
}
|
|
@ -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 {
|
|
@ -1,6 +1,14 @@
|
|||
import { EUserSchema } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
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(
|
||||
|
@ -24,4 +32,11 @@ export class EUserBackend implements OverriddenEUser {
|
|||
|
||||
@Column({ nullable: false, select: false })
|
||||
hashed_password?: string;
|
||||
|
||||
// This will never be populated, it is only here to auto delete apikeys when a user is deleted
|
||||
@OneToMany(() => EApiKeyBackend, (apikey) => apikey.user)
|
||||
apikeys?: EApiKeyBackend[];
|
||||
|
||||
@OneToMany(() => EUsrPreferenceBackend, (pref) => pref.user_id)
|
||||
preferences?: EUsrPreferenceBackend[];
|
||||
}
|
|
@ -1,44 +1,93 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V030A1661692206479 implements MigrationInterface {
|
||||
name = 'V030A1661692206479'
|
||||
name = 'V030A1661692206479';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "e_image_derivative_backend" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "image_id" character varying NOT NULL, "key" character varying NOT NULL, "filetype" character varying NOT NULL, "last_read" TIMESTAMP NOT NULL, "data" bytea NOT NULL, CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75" UNIQUE ("image_id", "key"), CONSTRAINT "PK_ff1ecff935b8d7bdcea89087810" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_37055605f39b3f8847232d604f" ON "e_image_derivative_backend" ("image_id") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7dc534a666f442383341896062" ON "e_image_derivative_backend" ("key") `);
|
||||
await queryRunner.query(`CREATE TABLE "e_image_file_backend" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "image_id" character varying NOT NULL, "variant" character varying NOT NULL, "filetype" character varying NOT NULL, "data" bytea NOT NULL, CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a" UNIQUE ("image_id", "variant"), CONSTRAINT "PK_95953be58a506e5de46feec6186" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8055f37d3b9f52f421b94ee84d" ON "e_image_file_backend" ("image_id") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d0500b00b0b4109b623f897c2d" ON "e_image_file_backend" ("variant") `);
|
||||
await queryRunner.query(`CREATE TABLE "e_image_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "created" TIMESTAMP NOT NULL, CONSTRAINT "PK_5f7993001a7c82564ec5300540d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "e_role_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "permissions" text array NOT NULL, CONSTRAINT "UQ_cbedb9f42a98a82d91422e7fedf" UNIQUE ("name"), CONSTRAINT "PK_af7ba6a46bf69a7b10c425f0367" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_cbedb9f42a98a82d91422e7fed" ON "e_role_backend" ("name") `);
|
||||
await queryRunner.query(`CREATE TABLE "e_sys_preference_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_b04e47c4814fb6e315c5879fa75" UNIQUE ("key"), CONSTRAINT "PK_b79f051e19b46e74cf255e9ba3b" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b04e47c4814fb6e315c5879fa7" ON "e_sys_preference_backend" ("key") `);
|
||||
await queryRunner.query(`CREATE TABLE "e_user_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" character varying NOT NULL, "roles" text array NOT NULL, "hashed_password" character varying NOT NULL, CONSTRAINT "UQ_ae538430fd08b28f4ab297eff09" UNIQUE ("username"), CONSTRAINT "PK_0b9d256d52e55a48d32e8b64d96" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ae538430fd08b28f4ab297eff0" ON "e_user_backend" ("username") `);
|
||||
await queryRunner.query(`CREATE TABLE "e_usr_preference_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "value" character varying NOT NULL, "user_id" character varying NOT NULL, CONSTRAINT "UQ_576678406a479d569123a33e132" UNIQUE ("key", "user_id"), CONSTRAINT "PK_8f8251016cd9283e7eb04c5498b" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_673fe530e2484ff7e31ac81099" ON "e_usr_preference_backend" ("key") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f1a427e855045fa793c275861a" ON "e_usr_preference_backend" ("user_id") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f1a427e855045fa793c275861a"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_673fe530e2484ff7e31ac81099"`);
|
||||
await queryRunner.query(`DROP TABLE "e_usr_preference_backend"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ae538430fd08b28f4ab297eff0"`);
|
||||
await queryRunner.query(`DROP TABLE "e_user_backend"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b04e47c4814fb6e315c5879fa7"`);
|
||||
await queryRunner.query(`DROP TABLE "e_sys_preference_backend"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_cbedb9f42a98a82d91422e7fed"`);
|
||||
await queryRunner.query(`DROP TABLE "e_role_backend"`);
|
||||
await queryRunner.query(`DROP TABLE "e_image_backend"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d0500b00b0b4109b623f897c2d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8055f37d3b9f52f421b94ee84d"`);
|
||||
await queryRunner.query(`DROP TABLE "e_image_file_backend"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7dc534a666f442383341896062"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_37055605f39b3f8847232d604f"`);
|
||||
await queryRunner.query(`DROP TABLE "e_image_derivative_backend"`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_image_derivative_backend" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "image_id" character varying NOT NULL, "key" character varying NOT NULL, "filetype" character varying NOT NULL, "last_read" TIMESTAMP NOT NULL, "data" bytea NOT NULL, CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75" UNIQUE ("image_id", "key"), CONSTRAINT "PK_ff1ecff935b8d7bdcea89087810" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_37055605f39b3f8847232d604f" ON "e_image_derivative_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7dc534a666f442383341896062" ON "e_image_derivative_backend" ("key") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_image_file_backend" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "image_id" character varying NOT NULL, "variant" character varying NOT NULL, "filetype" character varying NOT NULL, "data" bytea NOT NULL, CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a" UNIQUE ("image_id", "variant"), CONSTRAINT "PK_95953be58a506e5de46feec6186" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_8055f37d3b9f52f421b94ee84d" ON "e_image_file_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d0500b00b0b4109b623f897c2d" ON "e_image_file_backend" ("variant") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_image_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "created" TIMESTAMP NOT NULL, CONSTRAINT "PK_5f7993001a7c82564ec5300540d" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_role_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "permissions" text array NOT NULL, CONSTRAINT "UQ_cbedb9f42a98a82d91422e7fedf" UNIQUE ("name"), CONSTRAINT "PK_af7ba6a46bf69a7b10c425f0367" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_cbedb9f42a98a82d91422e7fed" ON "e_role_backend" ("name") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_sys_preference_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_b04e47c4814fb6e315c5879fa75" UNIQUE ("key"), CONSTRAINT "PK_b79f051e19b46e74cf255e9ba3b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b04e47c4814fb6e315c5879fa7" ON "e_sys_preference_backend" ("key") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_user_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" character varying NOT NULL, "roles" text array NOT NULL, "hashed_password" character varying NOT NULL, CONSTRAINT "UQ_ae538430fd08b28f4ab297eff09" UNIQUE ("username"), CONSTRAINT "PK_0b9d256d52e55a48d32e8b64d96" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ae538430fd08b28f4ab297eff0" ON "e_user_backend" ("username") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_usr_preference_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "value" character varying NOT NULL, "user_id" character varying NOT NULL, CONSTRAINT "UQ_576678406a479d569123a33e132" UNIQUE ("key", "user_id"), CONSTRAINT "PK_8f8251016cd9283e7eb04c5498b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_673fe530e2484ff7e31ac81099" ON "e_usr_preference_backend" ("key") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f1a427e855045fa793c275861a" ON "e_usr_preference_backend" ("user_id") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f1a427e855045fa793c275861a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_673fe530e2484ff7e31ac81099"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_usr_preference_backend"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ae538430fd08b28f4ab297eff0"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_user_backend"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b04e47c4814fb6e315c5879fa7"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_sys_preference_backend"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_cbedb9f42a98a82d91422e7fed"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_role_backend"`);
|
||||
await queryRunner.query(`DROP TABLE "e_image_backend"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_d0500b00b0b4109b623f897c2d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_8055f37d3b9f52f421b94ee84d"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_image_file_backend"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7dc534a666f442383341896062"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_37055605f39b3f8847232d604f"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_image_derivative_backend"`);
|
||||
}
|
||||
}
|
||||
|
|
17
backend/src/database/migrations/1662029904716-V_0_3_2_a.ts
Normal file
17
backend/src/database/migrations/1662029904716-V_0_3_2_a.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V032A1662029904716 implements MigrationInterface {
|
||||
name = 'V032A1662029904716';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ADD "file_name" character varying NOT NULL DEFAULT 'image'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" DROP COLUMN "file_name"`,
|
||||
);
|
||||
}
|
||||
}
|
33
backend/src/database/migrations/1662314197741-V_0_4_0_a.ts
Normal file
33
backend/src/database/migrations/1662314197741-V_0_4_0_a.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V040A1662314197741 implements MigrationInterface {
|
||||
name = 'V040A1662314197741';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "e_api_key_backend" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying NOT NULL, "name" character varying NOT NULL, "created" TIMESTAMP NOT NULL, "last_used" TIMESTAMP, "userId" uuid NOT NULL, CONSTRAINT "UQ_a244964afdff398bab8a45017c8" UNIQUE ("key"), CONSTRAINT "PK_e31f7dfe2db917a6ed1024f4e8b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a244964afdff398bab8a45017c" ON "e_api_key_backend" ("key") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ADD "delete_key" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" ADD CONSTRAINT "FK_3a32374df29b25152a84f0d1025" FOREIGN KEY ("userId") REFERENCES "e_user_backend"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" DROP CONSTRAINT "FK_3a32374df29b25152a84f0d1025"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" DROP COLUMN "delete_key"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_a244964afdff398bab8a45017c"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "e_api_key_backend"`);
|
||||
}
|
||||
}
|
92
backend/src/database/migrations/1662485374471-V_0_4_0_b.ts
Normal file
92
backend/src/database/migrations/1662485374471-V_0_4_0_b.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V040B1662485374471 implements MigrationInterface {
|
||||
name = 'V040B1662485374471';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ADD "expires_at" TIMESTAMP`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" DROP CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_8055f37d3b9f52f421b94ee84d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ALTER COLUMN "image_id" SET DATA TYPE UUID USING image_id::uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" DROP CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_37055605f39b3f8847232d604f"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ALTER COLUMN "image_id" SET DATA TYPE UUID USING image_id::uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_8055f37d3b9f52f421b94ee84d" ON "e_image_file_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_37055605f39b3f8847232d604f" ON "e_image_derivative_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ADD CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a" UNIQUE ("image_id", "variant")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ADD CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75" UNIQUE ("image_id", "key")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ADD CONSTRAINT "FK_8055f37d3b9f52f421b94ee84db" FOREIGN KEY ("image_id") REFERENCES "e_image_backend"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ADD CONSTRAINT "FK_37055605f39b3f8847232d604f8" FOREIGN KEY ("image_id") REFERENCES "e_image_backend"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" DROP CONSTRAINT "FK_37055605f39b3f8847232d604f8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" DROP CONSTRAINT "FK_8055f37d3b9f52f421b94ee84db"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" DROP CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" DROP CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_37055605f39b3f8847232d604f"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_8055f37d3b9f52f421b94ee84d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ALTER COLUMN "image_id" SET DATA TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_37055605f39b3f8847232d604f" ON "e_image_derivative_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ADD CONSTRAINT "UQ_fa03f5333afd74c5cc5ff780d75" UNIQUE ("image_id", "key")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" DROP COLUMN "image_id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ALTER COLUMN "image_id" SET DATA TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_8055f37d3b9f52f421b94ee84d" ON "e_image_file_backend" ("image_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_file_backend" ADD CONSTRAINT "UQ_872384f20feaf7bfd27e28b8d4a" UNIQUE ("image_id", "variant")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" DROP COLUMN "expires_at"`,
|
||||
);
|
||||
}
|
||||
}
|
53
backend/src/database/migrations/1662535484200-V_0_4_0_c.ts
Normal file
53
backend/src/database/migrations/1662535484200-V_0_4_0_c.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V040C1662535484200 implements MigrationInterface {
|
||||
name = 'V040C1662535484200';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" DROP CONSTRAINT "UQ_576678406a479d569123a33e132"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f1a427e855045fa793c275861a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" ALTER COLUMN "user_id" SET DATA TYPE UUID USING user_id::uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "user_id" SET DATA TYPE UUID USING user_id::uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f1a427e855045fa793c275861a" ON "e_usr_preference_backend" ("user_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" ADD CONSTRAINT "UQ_576678406a479d569123a33e132" UNIQUE ("key", "user_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" ADD CONSTRAINT "FK_f1a427e855045fa793c275861a7" FOREIGN KEY ("user_id") REFERENCES "e_user_backend"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" DROP CONSTRAINT "FK_f1a427e855045fa793c275861a7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" DROP CONSTRAINT "UQ_576678406a479d569123a33e132"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f1a427e855045fa793c275861a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "user_id" SET DATA TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" ALTER COLUMN "user_id" SET DATA TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f1a427e855045fa793c275861a" ON "e_usr_preference_backend" ("user_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_usr_preference_backend" ADD CONSTRAINT "UQ_576678406a479d569123a33e132" UNIQUE ("key", "user_id")`,
|
||||
);
|
||||
}
|
||||
}
|
41
backend/src/database/migrations/1662728275448-V_0_4_0_d.ts
Normal file
41
backend/src/database/migrations/1662728275448-V_0_4_0_d.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V040D1662728275448 implements MigrationInterface {
|
||||
name = 'V040D1662728275448';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "created" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" ALTER COLUMN "last_used" SET DATA TYPE TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "created" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "expires_at" SET DATA TYPE TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ALTER COLUMN "last_read" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "last_read" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_derivative_backend" ALTER COLUMN "last_read" SET DATA TYPE TIMESTAMP, ALTER COLUMN "last_read" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "expires_at" SET DATA TYPE TIMESTAMP`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_image_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP, ALTER COLUMN "created" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" ALTER COLUMN "last_used" SET DATA TYPE TIMESTAMP`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "e_api_key_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP, ALTER COLUMN "created" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
21
backend/src/database/migrations/1672154027079-V_0_5_0_a.ts
Normal file
21
backend/src/database/migrations/1672154027079-V_0_5_0_a.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
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"`);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
import { MigrationInterface } from 'typeorm';
|
||||
import { V030A1661692206479 } from './1661692206479-V_0_3_0_a';
|
||||
import { V032A1662029904716 } from './1662029904716-V_0_3_2_a';
|
||||
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';
|
||||
import { Newable } from 'picsur-shared/dist/types/newable.js';
|
||||
|
||||
export const MigrationList: Function[] = [V030A1661692206479];
|
||||
export const MigrationList: Newable<MigrationInterface>[] = [
|
||||
V030A1661692206479,
|
||||
V032A1662029904716,
|
||||
V040A1662314197741,
|
||||
V040B1662485374471,
|
||||
V040C1662535484200,
|
||||
V040D1662728275448,
|
||||
V050A1672154027079,
|
||||
];
|
||||
|
|
|
@ -1,24 +1,12 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import {
|
||||
FastifyAdapter, NestFastifyApplication
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { DataSource, InstanceChecker } from 'typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { TypeOrmConfigService } from './config/early/type-orm.config.service';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
|
||||
// TODO, upgrade to a version beyond typeorm 3.8, cause 3.8 is bugged
|
||||
// So here we monkeypatch 3.7
|
||||
function patchAsyncDataSourceSetup() {
|
||||
const oldIsDataSource = InstanceChecker.isDataSource;
|
||||
InstanceChecker.isDataSource = function (obj: unknown): obj is DataSource {
|
||||
if (obj instanceof Promise) {
|
||||
return true;
|
||||
}
|
||||
return oldIsDataSource(obj);
|
||||
};
|
||||
}
|
||||
patchAsyncDataSourceSetup();
|
||||
|
||||
async function createDataSource() {
|
||||
// Create nest app
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
|
@ -29,7 +17,7 @@ async function createDataSource() {
|
|||
const configFactory = app.get(TypeOrmConfigService);
|
||||
const config = await configFactory.createTypeOrmOptions();
|
||||
|
||||
return new DataSource(config);
|
||||
return new DataSource(config as any);
|
||||
}
|
||||
|
||||
export default createDataSource().catch(console.error);
|
||||
export default createDataSource();
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
|
||||
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||
import { Ext2FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FT, Fail, HasFailed } from 'picsur-shared/dist/types/failable';
|
||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { ImageFullId } from '../../models/constants/image-full-id.const';
|
||||
|
||||
@Injectable()
|
||||
export class ImageFullIdPipe implements PipeTransform<string, ImageFullId> {
|
||||
transform(value: string, metadata: ArgumentMetadata): ImageFullId {
|
||||
transform(value: string): ImageFullId {
|
||||
const split = value.split('.');
|
||||
if (split.length === 2) {
|
||||
const [id, ext] = split;
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import {
|
||||
ArgumentMetadata, Injectable,
|
||||
PipeTransform
|
||||
} from '@nestjs/common';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||
import { FT, Fail } from 'picsur-shared/dist/types/failable';
|
||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
|
||||
@Injectable()
|
||||
export class ImageIdPipe implements PipeTransform<string, string> {
|
||||
transform(value: string, metadata: ArgumentMetadata): string {
|
||||
transform(value: string): string {
|
||||
if (UUIDRegex.test(value)) return value;
|
||||
throw Fail(FT.UsrValidation, 'Invalid image id');
|
||||
}
|
||||
|
|
|
@ -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,80 +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');
|
||||
|
||||
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;
|
||||
} 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,27 +1,22 @@
|
|||
import { Multipart } from '@fastify/multipart';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope
|
||||
} from '@nestjs/common';
|
||||
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';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import { MultipartConfigService } from '../../config/early/multipart.config.service';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class PostFilePipe implements PipeTransform {
|
||||
private readonly logger = new Logger('PostFilePipe');
|
||||
private readonly logger = new Logger(PostFilePipe.name);
|
||||
|
||||
constructor(
|
||||
private readonly multipartConfigService: MultipartConfigService,
|
||||
) {}
|
||||
|
||||
async transform({ req }: { req: FastifyRequest }) {
|
||||
if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
async transform({ request }: { 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,
|
||||
|
@ -35,7 +30,9 @@ export class PostFilePipe implements PipeTransform {
|
|||
) as any;
|
||||
|
||||
// Remove non-file fields
|
||||
const files = allFields.filter((entry) => entry.file !== undefined);
|
||||
const files: MultipartFile[] = allFields.filter(
|
||||
(entry) => (entry as any).file !== undefined,
|
||||
) as MultipartFile[];
|
||||
|
||||
if (files.length !== 1) throw Fail(FT.UsrValidation, 'Invalid file');
|
||||
|
||||
|
|
28
backend/src/decorators/multipart/postfiles.pipe.ts
Normal file
28
backend/src/decorators/multipart/postfiles.pipe.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { MultipartFile } from '@fastify/multipart';
|
||||
import { Injectable, Logger, PipeTransform, Scope } from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { FT, Fail } from 'picsur-shared/dist/types/failable';
|
||||
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({ request, data }: { data: any; request: FastifyRequest }) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@ import {
|
|||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
SetMetadata,
|
||||
UseGuards
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import { CombineFCDecorators } from 'picsur-shared/dist/util/decorator';
|
||||
import { LocalAuthGuard } from '../managers/auth/guards/local-auth.guard';
|
||||
import { Permission, Permissions } from '../models/constants/permissions.const';
|
||||
import AuthFasityRequest from '../models/interfaces/authrequest.dto';
|
||||
import AuthFastifyRequest from '../models/interfaces/authrequest.dto';
|
||||
|
||||
export const RequiredPermissions = (...permissions: Permissions) => {
|
||||
return SetMetadata('permissions', permissions);
|
||||
|
@ -26,10 +26,14 @@ export const UseLocalAuth = (...permissions: Permissions) =>
|
|||
|
||||
export const HasPermission = createParamDecorator(
|
||||
(data: Permission, ctx: ExecutionContext) => {
|
||||
const req: AuthFasityRequest = ctx.switchToHttp().getRequest();
|
||||
const req: AuthFastifyRequest = ctx.switchToHttp().getRequest();
|
||||
const permissions = req.userPermissions;
|
||||
if (!permissions) {
|
||||
throw Fail(FT.Internal, undefined, 'Permissions are missing from request');
|
||||
throw Fail(
|
||||
FT.Internal,
|
||||
undefined,
|
||||
'Permissions are missing from request',
|
||||
);
|
||||
}
|
||||
|
||||
return permissions.includes(data);
|
||||
|
@ -38,10 +42,14 @@ export const HasPermission = createParamDecorator(
|
|||
|
||||
export const GetPermissions = createParamDecorator(
|
||||
(data: Permission, ctx: ExecutionContext) => {
|
||||
const req: AuthFasityRequest = ctx.switchToHttp().getRequest();
|
||||
const req: AuthFastifyRequest = ctx.switchToHttp().getRequest();
|
||||
const permissions = req.userPermissions;
|
||||
if (!permissions) {
|
||||
throw Fail(FT.Internal, undefined, 'Permissions are missing from request');
|
||||
throw Fail(
|
||||
FT.Internal,
|
||||
undefined,
|
||||
'Permissions are missing from request',
|
||||
);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import AuthFasityRequest from '../models/interfaces/authrequest.dto';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import AuthFastifyRequest from '../models/interfaces/authrequest.dto';
|
||||
|
||||
export const ReqUser = createParamDecorator(
|
||||
(input: any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest() as AuthFasityRequest;
|
||||
const request = ctx.switchToHttp().getRequest() as AuthFastifyRequest;
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
|
||||
export const ReqUserID = createParamDecorator(
|
||||
(input: any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest() as AuthFasityRequest;
|
||||
const request = ctx.switchToHttp().getRequest() as AuthFastifyRequest;
|
||||
const id = request.user.id;
|
||||
if (!id) throw Fail(FT.Internal, undefined, 'User ID is not set');
|
||||
return id;
|
||||
|
|
|
@ -6,13 +6,17 @@ import { Newable } from 'picsur-shared/dist/types/newable';
|
|||
type ReturnsMethodDecorator<ReturnType> = <
|
||||
T extends (...args: any) => ReturnType | Promise<ReturnType>,
|
||||
>(
|
||||
target: Object,
|
||||
target: object,
|
||||
propertyKey: string | symbol,
|
||||
descriptor: TypedPropertyDescriptor<T>,
|
||||
) => TypedPropertyDescriptor<T> | void;
|
||||
|
||||
export function Returns<N extends Object>(
|
||||
export function Returns<N extends object>(
|
||||
newable: Newable<N>,
|
||||
): ReturnsMethodDecorator<N> {
|
||||
return SetMetadata('returns', newable);
|
||||
}
|
||||
|
||||
export function ReturnsAnything(): ReturnsMethodDecorator<any> {
|
||||
return SetMetadata('noreturns', true);
|
||||
}
|
||||
|
|
28
backend/src/layers/PicsurLayers.module.ts
Normal file
28
backend/src/layers/PicsurLayers.module.ts
Normal file
|
@ -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 {}
|
|
@ -1,11 +1,20 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
MethodNotAllowedException,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api/api.dto';
|
||||
import {
|
||||
Fail,
|
||||
Failure,
|
||||
FT,
|
||||
IsFailure
|
||||
IsFailure,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
|
||||
// This will catch any exception that is made in any request
|
||||
|
@ -24,32 +33,13 @@ export class MainExceptionFilter implements ExceptionFilter {
|
|||
const traceString = `(${request.ip} -> ${request.method} ${request.url})`;
|
||||
|
||||
if (!IsFailure(exception)) {
|
||||
MainExceptionFilter.logger.error(
|
||||
traceString + ' Unkown exception: ' + exception,
|
||||
);
|
||||
exception = Fail(FT.Internal, 'Unknown exception', exception);
|
||||
exception = this.transformKnownExceptions(exception);
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -59,10 +49,26 @@ export class MainExceptionFilter implements ExceptionFilter {
|
|||
|
||||
data: {
|
||||
type,
|
||||
message,
|
||||
message: exception.getReason(),
|
||||
},
|
||||
};
|
||||
|
||||
response.status(status).send(toSend);
|
||||
}
|
||||
|
||||
private transformKnownExceptions(exception: any): Failure {
|
||||
if (exception instanceof UnauthorizedException) {
|
||||
return Fail(FT.Permission, exception);
|
||||
} else if (exception instanceof ForbiddenException) {
|
||||
return Fail(FT.Permission, exception);
|
||||
} else if (exception instanceof NotFoundException) {
|
||||
return Fail(FT.RouteNotFound, exception);
|
||||
} else if (exception instanceof MethodNotAllowedException) {
|
||||
return Fail(FT.RouteNotFound, exception);
|
||||
} else if (exception instanceof Error) {
|
||||
return Fail(FT.Internal, exception);
|
||||
} else {
|
||||
return Fail(FT.Unknown, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import {
|
|||
Injectable,
|
||||
Logger,
|
||||
NestInterceptor,
|
||||
Optional
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { ApiAnySuccessResponse } from 'picsur-shared/dist/dto/api/api.dto';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
|
@ -20,7 +20,7 @@ export interface ZodValidationInterceptorOptions {
|
|||
}
|
||||
|
||||
@Injectable()
|
||||
export class SuccessInterceptor<T> implements NestInterceptor {
|
||||
export class SuccessInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger();
|
||||
|
||||
// TODO: make work
|
||||
|
@ -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(),
|
||||
|
@ -63,7 +82,7 @@ export class SuccessInterceptor<T> implements NestInterceptor {
|
|||
);
|
||||
}
|
||||
|
||||
let schema = schemaStatic.zodSchema;
|
||||
const schema = schemaStatic.zodSchema;
|
||||
|
||||
const parseResult = schema.safeParse(data);
|
||||
if (!parseResult.success) {
|
||||
|
@ -86,7 +105,7 @@ export class SuccessInterceptor<T> implements NestInterceptor {
|
|||
const response = context.switchToHttp().getResponse<FastifyReply>();
|
||||
|
||||
const newResponse: ApiAnySuccessResponse = {
|
||||
success: true as true, // really typescript
|
||||
success: true as const, // really typescript
|
||||
statusCode: response.statusCode,
|
||||
timestamp: new Date().toISOString(),
|
||||
timeMs: Math.round(response.getResponseTime()),
|
||||
|
|
10
backend/src/layers/throttler/PicsurThrottler.guard.ts
Normal file
10
backend/src/layers/throttler/PicsurThrottler.guard.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { FT, Fail } from 'picsur-shared/dist/types/failable';
|
||||
|
||||
@Injectable()
|
||||
export class PicsurThrottlerGuard extends ThrottlerGuard {
|
||||
protected override throwThrottlingException(): void {
|
||||
throw Fail(FT.RateLimit);
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@ import {
|
|||
ArgumentMetadata,
|
||||
Injectable,
|
||||
Optional,
|
||||
PipeTransform
|
||||
PipeTransform,
|
||||
} from '@nestjs/common';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
||||
|
||||
export interface ZodValidationPipeOptions {
|
||||
|
@ -30,17 +30,13 @@ export class ZodValidationPipe implements PipeTransform {
|
|||
public transform(value: unknown, metadata: ArgumentMetadata): unknown {
|
||||
if (!this.validateCustom && metadata.type === 'custom') return value;
|
||||
|
||||
let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
|
||||
const zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
|
||||
|
||||
if (zodSchema) {
|
||||
const parseResult = zodSchema.safeParse(value);
|
||||
|
||||
if (!parseResult.success) {
|
||||
throw Fail(
|
||||
FT.UsrValidation,
|
||||
'Invalid data',
|
||||
parseResult.error
|
||||
);
|
||||
throw Fail(FT.UsrValidation, 'Invalid data', parseResult.error);
|
||||
}
|
||||
|
||||
return parseResult.data;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { HostConfigService } from '../config/early/host.config.service';
|
|||
export class PicsurLoggerService extends ConsoleLogger {
|
||||
constructor(hostService: HostConfigService) {
|
||||
super();
|
||||
if (hostService.isProduction()) {
|
||||
if (hostService.isProduction() && !hostService.isVerbose()) {
|
||||
super.setLogLevels(['error', 'warn', 'log']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,57 @@
|
|||
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
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { AppModule } from './app.module';
|
||||
import { UsersService } 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';
|
||||
import { HelmetOptions } from './security';
|
||||
|
||||
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>(
|
||||
AppModule,
|
||||
fastifyAdapter,
|
||||
{
|
||||
bufferLogs: true,
|
||||
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(UsersService)),
|
||||
);
|
||||
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);
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { PreferenceModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { UsersModule } from '../../collections/user-db/user-db.module';
|
||||
import { ApiKeyDbModule } from '../../collections/apikey-db/apikey-db.module';
|
||||
import { PreferenceDbModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { UserDbModule } from '../../collections/user-db/user-db.module';
|
||||
import {
|
||||
JwtConfigService,
|
||||
JwtSecretProvider,
|
||||
} from '../../config/late/jwt.config.service';
|
||||
import { LateConfigModule } from '../../config/late/late-config.module';
|
||||
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({
|
||||
imports: [
|
||||
UsersModule,
|
||||
UserDbModule,
|
||||
PassportModule,
|
||||
PreferenceModule,
|
||||
PreferenceDbModule,
|
||||
ApiKeyDbModule,
|
||||
LateConfigModule,
|
||||
JwtModule.registerAsync({
|
||||
useExisting: JwtConfigService,
|
||||
|
@ -27,12 +32,15 @@ import { GuestService } from './guest.service';
|
|||
],
|
||||
providers: [
|
||||
AuthManagerService,
|
||||
GuestService,
|
||||
JwtSecretProvider,
|
||||
LocalAuthStrategy,
|
||||
JwtStrategy,
|
||||
GuestStrategy,
|
||||
JwtSecretProvider,
|
||||
GuestService,
|
||||
ApiKeyStrategy,
|
||||
LocalAuthGuard,
|
||||
MainAuthGuard,
|
||||
],
|
||||
exports: [UsersModule, AuthManagerService],
|
||||
exports: [UserDbModule, AuthManagerService, LocalAuthGuard, MainAuthGuard],
|
||||
})
|
||||
export class AuthManagerModule {}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
|
||||
import { JwtData, JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types/failable';
|
||||
|
||||
@Injectable()
|
||||
export class AuthManagerService {
|
||||
private readonly logger = new Logger('AuthService');
|
||||
private readonly logger = new Logger(AuthManagerService.name);
|
||||
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
async createToken(user: EUser): AsyncFailable<string> {
|
||||
const jwtData = {
|
||||
user,
|
||||
const jwtData: JwtData = {
|
||||
uid: user.id,
|
||||
};
|
||||
|
||||
// Validate to be sure, this makes client experience better
|
||||
|
|
62
backend/src/managers/auth/guards/apikey.strategy.ts
Normal file
62
backend/src/managers/auth/guards/apikey.strategy.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
|
||||
import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
||||
import { IsApiKey } from 'picsur-shared/dist/validators/api-key.validator';
|
||||
import { ApiKeyDbService } from '../../../collections/apikey-db/apikey-db.service';
|
||||
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyStrategy extends PassportStrategy(
|
||||
HeaderAPIKeyStrategy,
|
||||
'apikey',
|
||||
) {
|
||||
private readonly logger = new Logger(ApiKeyStrategy.name);
|
||||
|
||||
constructor(private readonly apikeyDB: ApiKeyDbService) {
|
||||
super(
|
||||
{
|
||||
header: 'Authorization',
|
||||
prefix: 'Api-Key ',
|
||||
},
|
||||
false,
|
||||
(
|
||||
apikey: string,
|
||||
verified: (err: Error | null, user?: object, info?: object) => void,
|
||||
) => {
|
||||
this.validate(apikey)
|
||||
.then((user) => {
|
||||
verified(null, user === false ? undefined : user);
|
||||
})
|
||||
.catch((err) => {
|
||||
verified(err, undefined);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async validate(apikey: string): Promise<EUser | false> {
|
||||
const apiValidation = await IsApiKey().safeParseAsync(apikey);
|
||||
if (!apiValidation.success) {
|
||||
this.logger.warn('Invalid apikey format: ' + apikey);
|
||||
return false;
|
||||
}
|
||||
|
||||
const apikeyResult = await this.apikeyDB.resolve(apikey);
|
||||
if (HasFailed(apikeyResult)) {
|
||||
this.logger.warn('Invalid apikey: ' + apikey);
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = EUserBackend2EUser(apikeyResult.user);
|
||||
|
||||
const userValidation = await EUserSchema.safeParseAsync(user);
|
||||
if (!userValidation.success) {
|
||||
this.logger.error('Invalid user: ' + JSON.stringify(user));
|
||||
return false;
|
||||
}
|
||||
|
||||
return userValidation.data;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-strategy';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
|
||||
import { GuestService } from '../guest.service';
|
||||
import { ReqType } from './reqtype';
|
||||
|
||||
class GuestPassportStrategy extends Strategy {
|
||||
// Will be overridden by the nest implementation
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async validate(req: ReqType): Promise<any> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override async authenticate(req: ReqType, options?: any) {
|
||||
override async authenticate(req: ReqType) {
|
||||
const user = await this.validate(req);
|
||||
this.success(user);
|
||||
}
|
||||
|
@ -26,7 +29,7 @@ export class GuestStrategy extends PassportStrategy(
|
|||
}
|
||||
|
||||
// Return the guest user created by the guestservice
|
||||
override async validate(payload: any) {
|
||||
return await this.guestService.getGuestUser();
|
||||
override async validate(): Promise<EUser> {
|
||||
return EUserBackend2EUser(await this.guestService.getGuestUser());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,18 @@ import { PassportStrategy } from '@nestjs/passport';
|
|||
import { ExtractJwt, Strategy as JwtPassportStrategy } from 'passport-jwt';
|
||||
import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { ThrowIfFailed } from 'picsur-shared/dist/types/failable';
|
||||
import { UserDbService } from '../../../collections/user-db/user-db.service';
|
||||
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(JwtPassportStrategy, 'jwt') {
|
||||
private readonly logger = new Logger('JwtStrategy');
|
||||
private readonly logger = new Logger(JwtStrategy.name);
|
||||
|
||||
constructor(@Inject('JWT_SECRET') jwtSecret: string) {
|
||||
constructor(
|
||||
@Inject('JWT_SECRET') jwtSecret: string,
|
||||
private readonly usersService: UserDbService,
|
||||
) {
|
||||
// This will validate the jwt token itself
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
|
@ -24,7 +30,11 @@ export class JwtStrategy extends PassportStrategy(JwtPassportStrategy, 'jwt') {
|
|||
return false;
|
||||
}
|
||||
|
||||
const backendUser = ThrowIfFailed(
|
||||
await this.usersService.findOne(result.data.uid),
|
||||
);
|
||||
|
||||
// And return the user
|
||||
return result.data.user;
|
||||
return EUserBackend2EUser(backendUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,28 @@ import { Injectable } from '@nestjs/common';
|
|||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../../collections/user-db/user-db.service';
|
||||
import {
|
||||
AsyncFailable,
|
||||
ThrowIfFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { UserDbService } from '../../../collections/user-db/user-db.service';
|
||||
import { EUserBackend2EUser } from '../../../models/transformers/user.transformer';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
constructor(private readonly usersService: UserDbService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, password: string): AsyncFailable<EUser> {
|
||||
const start = Date.now();
|
||||
// All this does is call the usersservice authenticate for authentication
|
||||
const user = await this.usersService.authenticate(username, password);
|
||||
if (HasFailed(user)) throw user;
|
||||
|
||||
return EUserBackend2EUser(user);
|
||||
// Wait atleast 500ms
|
||||
const wait = 450 - (Date.now() - start);
|
||||
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
||||
|
||||
return EUserBackend2EUser(ThrowIfFailed(user));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,16 @@ import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
|
|||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { Fail, Failable, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../../collections/user-db/user-db.service';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
Failable,
|
||||
FT,
|
||||
HasFailed,
|
||||
ThrowIfFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { UserDbService } from '../../../collections/user-db/user-db.service';
|
||||
import { Permissions } from '../../../models/constants/permissions.const';
|
||||
import { isPermissionsArray } from '../../../models/validators/permissions.validator';
|
||||
|
||||
|
@ -12,12 +20,12 @@ import { isPermissionsArray } from '../../../models/validators/permissions.valid
|
|||
// This way a user will get his own account when logged in, but received guest permissions when not
|
||||
|
||||
@Injectable()
|
||||
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
private readonly logger = new Logger('MainAuthGuard');
|
||||
export class MainAuthGuard extends AuthGuard(['apikey', 'jwt', 'guest']) {
|
||||
private readonly logger = new Logger(MainAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly usersService: UserDbService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
@ -33,9 +41,10 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||
);
|
||||
}
|
||||
|
||||
const user = await this.validateUser(
|
||||
context.switchToHttp().getRequest().user,
|
||||
);
|
||||
const unsafeUser: EUser = context.switchToHttp().getRequest().user;
|
||||
|
||||
const user = ThrowIfFailed(await this.validateUser(unsafeUser));
|
||||
|
||||
if (!user.id) {
|
||||
throw Fail(
|
||||
FT.Internal,
|
||||
|
@ -57,14 +66,11 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||
// These are the permissions the user has
|
||||
const userPermissions = await this.usersService.getPermissions(user.id);
|
||||
if (HasFailed(userPermissions)) {
|
||||
throw Fail(
|
||||
FT.Internal,
|
||||
undefined,
|
||||
'Fetching user permissions failed: ' + userPermissions.getReason(),
|
||||
);
|
||||
throw userPermissions;
|
||||
}
|
||||
|
||||
context.switchToHttp().getRequest().userPermissions = userPermissions;
|
||||
context.switchToHttp().getRequest().user = user;
|
||||
|
||||
if (permissions.every((permission) => userPermissions.includes(permission)))
|
||||
return true;
|
||||
|
@ -75,16 +81,23 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||
const handlerName = context.getHandler().name;
|
||||
// Fall back to class permissions if none on function
|
||||
// But function has higher priority than class
|
||||
const permissions =
|
||||
this.reflector.get<Permissions>('permissions', context.getHandler()) ??
|
||||
const permissionsHandler: Permissions | undefined =
|
||||
this.reflector.get<Permissions>('permissions', context.getHandler());
|
||||
const permissionsClass: Permissions | undefined =
|
||||
this.reflector.get<Permissions>('permissions', context.getClass());
|
||||
|
||||
if (permissions === undefined)
|
||||
if (permissionsHandler === undefined && permissionsClass === undefined) {
|
||||
return Fail(
|
||||
FT.Internal,
|
||||
undefined,
|
||||
`${handlerName} does not have any permissions defined, denying access`,
|
||||
);
|
||||
}
|
||||
|
||||
const permissions = makeUnique([
|
||||
...(permissionsHandler ?? []),
|
||||
...(permissionsClass ?? []),
|
||||
]);
|
||||
|
||||
if (!isPermissionsArray(permissions))
|
||||
return Fail(
|
||||
|
@ -96,10 +109,10 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
|||
return permissions;
|
||||
}
|
||||
|
||||
private async validateUser(user: EUser): Promise<EUser> {
|
||||
private async validateUser(user: EUser): AsyncFailable<EUser> {
|
||||
const result = EUserSchema.safeParse(user);
|
||||
if (!result.success) {
|
||||
throw Fail(
|
||||
return Fail(
|
||||
FT.Internal,
|
||||
undefined,
|
||||
`Invalid user object, where it should always be valid: ${result.error}`,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../collections/user-db/user-db.service';
|
||||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
||||
import { UserDbService } from '../../collections/user-db/user-db.service';
|
||||
import { EUserBackend } from '../../database/entities/users/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GuestService {
|
||||
private fallBackUser: EUserBackend;
|
||||
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
constructor(private readonly usersService: UserDbService) {
|
||||
this.fallBackUser = new EUserBackend();
|
||||
this.fallBackUser.username = 'guest';
|
||||
this.fallBackUser.roles = ['guest'];
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
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 { RolesModule } from '../../collections/role-db/role-db.module';
|
||||
import { RoleDbModule } from '../../collections/role-db/role-db.module';
|
||||
import { EarlyConfigModule } from '../../config/early/early-config.module';
|
||||
import { HostConfigService } from '../../config/early/host.config.service';
|
||||
import { DemoManagerService } from './demo.service';
|
||||
|
||||
@Module({
|
||||
imports: [ImageDBModule, EarlyConfigModule, RolesModule],
|
||||
imports: [ImageDBModule, EarlyConfigModule, RoleDbModule],
|
||||
providers: [DemoManagerService],
|
||||
})
|
||||
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger('DemoManagerModule');
|
||||
private interval: NodeJS.Timeout;
|
||||
export class DemoManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(DemoManagerModule.name);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { RolesService } from '../../collections/role-db/role-db.service';
|
||||
import { RoleDbService } from '../../collections/role-db/role-db.service';
|
||||
import { Permission } from '../../models/constants/permissions.const';
|
||||
|
||||
@Injectable()
|
||||
export class DemoManagerService {
|
||||
private readonly logger = new Logger('DemoManagerService');
|
||||
private readonly logger = new Logger(DemoManagerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly rolesService: RolesService,
|
||||
private readonly rolesService: RoleDbService,
|
||||
) {}
|
||||
|
||||
public async setupRoles() {
|
||||
|
|
|
@ -3,18 +3,23 @@ import ms from 'ms';
|
|||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import {
|
||||
FileType,
|
||||
SupportedFileTypeCategory
|
||||
SupportedFileTypeCategory,
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { SharpOptions } from 'sharp';
|
||||
import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { SharpWrapper } from '../../workers/sharp.wrapper';
|
||||
import { ImageResult } from './imageresult';
|
||||
|
||||
@Injectable()
|
||||
export class ImageConverterService {
|
||||
constructor(private readonly sysPref: SysPreferenceService) {}
|
||||
constructor(private readonly sysPref: SysPreferenceDbService) {}
|
||||
|
||||
public async convert(
|
||||
image: Buffer,
|
||||
|
@ -57,7 +62,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 = {
|
||||
|
@ -78,13 +84,16 @@ export class ImageConverterService {
|
|||
height: options.height,
|
||||
fit: 'fill',
|
||||
kernel: 'cubic',
|
||||
withoutEnlargement: options.shrinkonly,
|
||||
});
|
||||
} else {
|
||||
sharpWrapper.operation('resize', {
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
fit: 'contain',
|
||||
fit: 'inside',
|
||||
kernel: 'cubic',
|
||||
|
||||
withoutEnlargement: options.shrinkonly,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -118,16 +127,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
78
backend/src/managers/image/image-manager.module.ts
Normal file
78
backend/src/managers/image/image-manager.module.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
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/failable';
|
||||
import { ImageDBModule } from '../../collections/image-db/image-db.module';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { PreferenceDbModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageProcessorService } from './image-processor.service';
|
||||
import { ImageManagerService } from './image.service';
|
||||
|
||||
@Module({
|
||||
imports: [ImageDBModule, PreferenceDbModule],
|
||||
providers: [
|
||||
ImageManagerService,
|
||||
ImageProcessorService,
|
||||
ImageConverterService,
|
||||
],
|
||||
exports: [ImageManagerService, ImageConverterService],
|
||||
})
|
||||
export class ImageManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(ImageManagerModule.name);
|
||||
|
||||
constructor(
|
||||
private readonly prefManager: SysPreferenceDbService,
|
||||
private readonly imageFileDB: ImageFileDBService,
|
||||
private readonly imageDB: ImageDBService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.imageManagerCron();
|
||||
}
|
||||
|
||||
@Interval(1000 * 60)
|
||||
private async imageManagerCron() {
|
||||
await this.cleanupDerivatives();
|
||||
await this.cleanupExpired();
|
||||
}
|
||||
|
||||
private async cleanupDerivatives() {
|
||||
const remove_derivatives_after = await this.prefManager.getStringPreference(
|
||||
SysPreference.RemoveDerivativesAfter,
|
||||
);
|
||||
if (HasFailed(remove_derivatives_after)) {
|
||||
this.logger.warn('Failed to get remove_derivatives_after preference');
|
||||
return;
|
||||
}
|
||||
|
||||
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)) {
|
||||
result.print(this.logger);
|
||||
}
|
||||
|
||||
if (result > 0) this.logger.log(`Cleaned up ${result} derivatives`);
|
||||
}
|
||||
|
||||
private async cleanupExpired() {
|
||||
const cleanedUp = await this.imageDB.cleanupExpired();
|
||||
|
||||
if (HasFailed(cleanedUp)) {
|
||||
cleanedUp.print(this.logger);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanedUp > 0)
|
||||
this.logger.log(`Cleaned up ${cleanedUp} expired images`);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue