mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 23:37:48 +01:00
Compare commits
31 Commits
b9149464f7
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6c738ec0 | ||
| 30039af60a | |||
| e82b753dfb | |||
| 45decc60a7 | |||
| fe58b33657 | |||
| 503d42121e | |||
| c114a72619 | |||
| 614ac4c802 | |||
| f16720f13e | |||
| faa0aa62c8 | |||
| 14210cf0cf | |||
| cc696978fb | |||
| 607b62386d | |||
|
|
dbc2c14345 | ||
| ee8a7412f4 | |||
| b6a775ce5f | |||
| 3d0dbca403 | |||
| b1a16f6fc2 | |||
| fe8f65ac10 | |||
| bb1a0a8bf9 | |||
| 320de598eb | |||
| f97e6aeb38 | |||
| d6438dc778 | |||
| 593e2181f2 | |||
| a2cdbdef7f | |||
| d081afefc6 | |||
| a2ebc390d5 | |||
| 3b97652c02 | |||
| cd9f3f9cc4 | |||
| 09844e5294 | |||
| 101e2ceed7 |
@@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
test-backend:
|
||||
docker:
|
||||
- image: cimg/node:14.20
|
||||
- image: cimg/node:16.20
|
||||
- image: cimg/mariadb:10.8
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
|
||||
@@ -14,50 +14,38 @@ jobs:
|
||||
working_directory: ~/photos
|
||||
|
||||
steps:
|
||||
# The checkout is INTO the working directory!!!
|
||||
- checkout
|
||||
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- backend-dependencies-{{ checksum "package.json" }}
|
||||
- backend-dependencies-{{ checksum "backend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: install backend deps
|
||||
command: npm i
|
||||
command: cd backend && npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: backend-dependencies-{{ checksum "package.json" }}
|
||||
- backend/node_modules
|
||||
key: backend-dependencies-{{ checksum "backend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: test backend
|
||||
command: npm test
|
||||
command: cd backend && npm test
|
||||
|
||||
- store_test_results:
|
||||
path: backend-report.xml
|
||||
path: ~/photos/backend/backend-report.xml
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
- image: cimg/node:14.20
|
||||
- image: cimg/node:16.20
|
||||
|
||||
working_directory: ~/photos
|
||||
|
||||
steps:
|
||||
# The checkout is INTO the working directory!!!
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: install backend deps
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- frontend-dependencies-{{ checksum "frontend/package.json" }}
|
||||
@@ -80,29 +68,17 @@ jobs:
|
||||
|
||||
test-frontend-build:
|
||||
docker:
|
||||
- image: cimg/node:14.20
|
||||
- image: cimg/node:16.20
|
||||
|
||||
working_directory: ~/photos/frontend
|
||||
working_directory: ~/photos
|
||||
|
||||
steps:
|
||||
# The checkout is INTO the working directory!!!
|
||||
- checkout:
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: install backend deps
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- frontend-dependencies-{{ checksum "package.json" }}
|
||||
- frontend-dependencies-{{ checksum "frontend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: install frontend deps
|
||||
@@ -111,7 +87,7 @@ jobs:
|
||||
- save_cache:
|
||||
paths:
|
||||
- frontend/node_modules
|
||||
key: frontend-dependencies-{{ checksum "package.json" }}
|
||||
key: frontend-dependencies-{{ checksum "frontend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: build frontend
|
||||
@@ -123,6 +99,7 @@ jobs:
|
||||
resource_class: large
|
||||
|
||||
steps:
|
||||
# The checkout is INTO the working directory!!!
|
||||
- checkout
|
||||
- run:
|
||||
name: log in to docker hub
|
||||
@@ -134,12 +111,26 @@ jobs:
|
||||
|
||||
- run:
|
||||
name: create docker builder
|
||||
command: docker buildx create --use
|
||||
command: docker buildx create --use --driver=docker-container
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- buildx-photos-circleci-
|
||||
|
||||
- run:
|
||||
name: build and push to docker hub
|
||||
command: docker buildx build --push --platform linux/arm64,linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH .
|
||||
#command: docker buildx build --push --platform linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH .
|
||||
command: |
|
||||
docker buildx build --progress=plain --push --platform linux/arm64,linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH \
|
||||
--cache-to=type=local,mode=max,dest=/tmp/dockercache \
|
||||
--cache-from=type=local,src=/tmp/dockercache .
|
||||
- run:
|
||||
name: prune cache
|
||||
command: docker buildx prune --keep-storage=4gb --verbose
|
||||
|
||||
- save_cache:
|
||||
key: buildx-photos-circleci-{{ checksum "/tmp/dockercache/index.json" }}
|
||||
paths:
|
||||
- /tmp/dockercache
|
||||
|
||||
# build-arm:
|
||||
# machine:
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
# Update the VARIANT arg in docker-compose.yml to pick a Node version: 10, 12, 14
|
||||
ARG VARIANT="16-buster"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
|
||||
ENV DOCKERDEV=true
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "node" user.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||
groupmod --gid $USER_GID node \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID node \
|
||||
&& chmod -R $USER_UID:$USER_GID /home/node \
|
||||
&& chmod -R $USER_UID:root /usr/local/share/nvm /usr/local/share/npm-global; \
|
||||
fi
|
||||
|
||||
RUN sudo -u node npm config set unsafe-perm=true
|
||||
ARG VARIANT=16-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -24,6 +9,5 @@ RUN sudo -u node npm config set unsafe-perm=true
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN sudo -u node npm install -g <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node modules
|
||||
# RUN su node -c "npm install -g <your-package-list-here>"
|
||||
5
.devcontainer/dbinit/fix.sql
Executable file
5
.devcontainer/dbinit/fix.sql
Executable file
@@ -0,0 +1,5 @@
|
||||
FLUSH PRIVILEGES ;
|
||||
DROP USER ''@'localhost' ;
|
||||
create database if not exists `photostestdb` ;
|
||||
grant all privileges on `photostestdb`.* to 'photostestuser'@'%' identified by 'photostestpass' ;
|
||||
FLUSH PRIVILEGES ;
|
||||
@@ -9,12 +9,18 @@
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint"],
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mikestead.dotenv",
|
||||
"esbenp.prettier-vscode",
|
||||
"foxundermoon.shell-format",
|
||||
"ms-azuretools.vscode-docker"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [1234, 3000],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "npm config set unsafe-perm=true && npm i && cd frontend && npm i",
|
||||
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"postCreateCommand": "cd /workspace; if [ ! -f ormconfig.json ]; then cp ormconfig.dockerdevexample.json ormconfig.json; fi; if [ ! -f ormconfig.test.json ]; then cp ormconfig.dockerdevexample.test.json ormconfig.test.json; fi;"
|
||||
"postCreateCommand": "cd /workspace; if [ ! -f ormconfig.json ]; then cp ormconfig.dockerdevexample.json ormconfig.json; fi; if [ ! -f ormconfig.test.json ]; then cp ormconfig.dockerdevexample.test.json ormconfig.test.json; fi; npm i && cd frontend && npm i"
|
||||
}
|
||||
|
||||
@@ -2,54 +2,33 @@ version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# [Choice] Node.js version: 14, 12, 10
|
||||
VARIANT: 16
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: 1000
|
||||
USER_GID: 1000
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
user: node
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: mariadb:latest
|
||||
image: yobasystems/alpine-mariadb:10.6
|
||||
volumes:
|
||||
- photosmariadb-data:/var/lib/mysql
|
||||
- ./dbinit:/docker-entrypoint-initdb.d:ro
|
||||
environment:
|
||||
MYSQL_DATABASE: photos
|
||||
MYSQL_USER: photos
|
||||
MYSQL_PASSWORD: photos
|
||||
MYSQL_ROOT_PASSWORD: photos
|
||||
|
||||
dbtest:
|
||||
image: mariadb:latest
|
||||
volumes:
|
||||
- photosmariadbtest-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: photos_test
|
||||
MYSQL_USER: photos
|
||||
MYSQL_PASSWORD: photos
|
||||
MYSQL_ROOT_PASSWORD: photos
|
||||
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward MongoDB locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
MYSQL_DATABASE: photosdb
|
||||
MYSQL_USER: photosuser
|
||||
MYSQL_PASSWORD: photospass
|
||||
MYSQL_ROOT_PASSWORD: photosroot
|
||||
healthcheck:
|
||||
test: "mysql $$MYSQL_DATABASE -u$$MYSQL_USER -p$$MYSQL_PASSWORD -e 'SELECT 1;'"
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
photosmariadb-data:
|
||||
photosmariadbtest-data:
|
||||
@@ -3,3 +3,8 @@ npm-debug.log
|
||||
|
||||
frontend/node_modules
|
||||
frontend/npm-debug.log
|
||||
frontend/.parcel-cache
|
||||
|
||||
backend/node_modules
|
||||
backend/npm-debug.log
|
||||
backend/.parcel-cache
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
node_modules/
|
||||
/node_modules
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
@@ -13,4 +14,4 @@ ormconfig.test.json
|
||||
data_test
|
||||
data_dev
|
||||
data
|
||||
backend-report.xml
|
||||
backend-report.xml
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -3,7 +3,7 @@
|
||||
"typeorm"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
".",
|
||||
"./backend",
|
||||
"./frontend"
|
||||
],
|
||||
"search.exclude": {
|
||||
|
||||
32
Dockerfile
32
Dockerfile
@@ -1,21 +1,29 @@
|
||||
# might fix this? https://github.com/parcel-bundler/parcel/issues/6735
|
||||
FROM node:16-bullseye
|
||||
FROM node:16-bullseye as frontbuild
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
RUN mkdir frontend
|
||||
WORKDIR frontend
|
||||
WORKDIR /usr/src/app/frontend
|
||||
COPY ./frontend/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY ./frontend/. .
|
||||
COPY ./shared ../shared
|
||||
RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")'
|
||||
WORKDIR ../
|
||||
RUN bash -O extglob -c 'rm -rfv !("frontend")'
|
||||
|
||||
COPY . .
|
||||
FROM node:16-alpine as backexceptwithoutfrontend
|
||||
|
||||
WORKDIR frontend
|
||||
RUN npm run build
|
||||
WORKDIR ../
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./backend/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY ./backend ./
|
||||
RUN rm -rfv frontend && unlink src/shared
|
||||
COPY ./shared src/shared
|
||||
|
||||
FROM backexceptwithoutfrontend
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=frontbuild /usr/src/app/frontend ./frontend
|
||||
COPY ./dockerentry.sh .
|
||||
|
||||
#ENV PORT=8080
|
||||
#ENV TYPEORM_HOST=localhost
|
||||
@@ -39,6 +47,6 @@ ENV DATA_DIR=data\
|
||||
|
||||
#EXPOSE 8080
|
||||
|
||||
RUN ["chmod", "+x", "dockerentry.sh"]
|
||||
RUN ["chmod", "+x", "./dockerentry.sh"]
|
||||
|
||||
CMD [ "./dockerentry.sh" ]
|
||||
|
||||
13
README.md
13
README.md
@@ -1,10 +1,10 @@
|
||||
# Photos
|
||||
|
||||
Something that tries to be a self-hosted alternative to Google Photos
|
||||
|
||||
<!---
|
||||
Demo: https://photos.usatiuk.com
|
||||
(no need to enter a real email, something like asdf@asdf.com is enough, data is stored on tmpfs, reset every day)
|
||||
|
||||
-->
|
||||

|
||||
|
||||
## Getting started
|
||||
@@ -13,19 +13,16 @@ Demo: https://photos.usatiuk.com
|
||||
|
||||
First, install all of the dependencies with `npm i` and `cd frontend && npm i`
|
||||
|
||||
You also need to create a ormconfig.json and ormconfig.test.json (only if you
|
||||
You also need to create a ormconfig.json and ormconfig.test.json (the latter only if you
|
||||
want to run the tests). You can use ormconfig.example.json as an example (you
|
||||
only need to change the database connection settings)
|
||||
|
||||
### Using docker
|
||||
|
||||
Open the workspace in a remote docker container using VSCode - everything should
|
||||
be set up for you. You just need to rename `ormconfig.dockerdevexample.json` and
|
||||
be set up for you. You might need to rename `ormconfig.dockerdevexample.json` and
|
||||
`ormconfig.dockerdevexample.test.json` to `ormconfig.json` and
|
||||
`ormconfig.test.json`
|
||||
|
||||
Also, you need to run database migrations with
|
||||
`npm run typeorm-dev -- migration:run`
|
||||
`ormconfig.test.json` and install dependencies too (although it should be automatic)
|
||||
|
||||
Then start with `npm run dev` and visit http://localhost:1234 (Parcel dev server
|
||||
is listening at http://localhost:1234, and koa at http://localhost:3000)
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier",
|
||||
"import"
|
||||
"import",
|
||||
"mocha"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
"plugin:import/typescript",
|
||||
"plugin:mocha/recommended"
|
||||
],
|
||||
"env": {
|
||||
"node": true
|
||||
17
backend/.gitignore
vendored
Normal file
17
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
node_modules/
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
dist/
|
||||
ormconfig.json
|
||||
ormconfig.dev.json
|
||||
ormconfig.test.json
|
||||
.env
|
||||
.directory
|
||||
.history
|
||||
data_test
|
||||
data_dev
|
||||
data
|
||||
backend-report.xml
|
||||
6
backend/mocha.json
Executable file
6
backend/mocha.json
Executable file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"reporterEnabled": "spec, mocha-junit-reporter",
|
||||
"mochaJunitReporterReporterOptions": {
|
||||
"mochaFile":"backend-report.xml"
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,18 @@
|
||||
"type": "mariadb",
|
||||
"host": "db",
|
||||
"port": 3306,
|
||||
"username": "photos",
|
||||
"password": "photos",
|
||||
"database": "photos",
|
||||
"username": "photosuser",
|
||||
"password": "photospass",
|
||||
"database": "photosdb",
|
||||
"synchronize": false,
|
||||
"logging": false,
|
||||
"entities": [
|
||||
"src/entity/**/*.ts"
|
||||
],
|
||||
"migrations": [
|
||||
"src/migration/**/*.ts"
|
||||
],
|
||||
"subscribers": [
|
||||
"src/subscriber/**/*.ts"
|
||||
],
|
||||
"entities": ["src/entity/**/*.ts"],
|
||||
"migrations": ["src/migration/**/*.ts"],
|
||||
"subscribers": ["src/subscriber/**/*.ts"],
|
||||
"cli": {
|
||||
"entitiesDir": "src/entity",
|
||||
"migrationsDir": "src/migration",
|
||||
"subscribersDir": "src/subscriber"
|
||||
},
|
||||
"charset": "utf8mb4"
|
||||
}
|
||||
}
|
||||
19
backend/ormconfig.dockerdevexample.test.json
Normal file
19
backend/ormconfig.dockerdevexample.test.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "mariadb",
|
||||
"host": "db",
|
||||
"port": 3306,
|
||||
"username": "photostestuser",
|
||||
"password": "photostestpass",
|
||||
"database": "photostestdb",
|
||||
"synchronize": true,
|
||||
"logging": false,
|
||||
"entities": ["src/entity/**/*.ts"],
|
||||
"migrations": ["src/migration/**/*.ts"],
|
||||
"subscribers": ["src/subscriber/**/*.ts"],
|
||||
"cli": {
|
||||
"entitiesDir": "src/entity",
|
||||
"migrationsDir": "src/migration",
|
||||
"subscribersDir": "src/subscriber"
|
||||
},
|
||||
"charset": "utf8mb4"
|
||||
}
|
||||
7595
backend/package-lock.json
generated
Normal file
7595
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
backend/package.json
Executable file
88
backend/package.json
Executable file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "photos",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"start": "ts-node -T -r tsconfig-paths/register src/server.ts",
|
||||
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts",
|
||||
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register --reporter mocha-multi-reporters --reporter-options configFile=mocha.json 'src/tests/**/*.ts'",
|
||||
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
|
||||
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"prettier-check": "prettier src/**/*.ts --check",
|
||||
"prettify": "prettier src/**/*.ts --write",
|
||||
"typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
|
||||
"typeorm": "cross-env NODE_ENV=production ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5",
|
||||
"@koa/router": "^12.0.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"class-validator": "^0.14.0",
|
||||
"exifreader": "^4.13.0",
|
||||
"hasha": "^5.2.2",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"koa": "^2.14.2",
|
||||
"koa-body": "^5.0.0",
|
||||
"koa-jwt": "^4.0.4",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-sslify": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mysql": "^2.18.1",
|
||||
"sharp": "^0.32.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typeorm": "^0.2.41",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/concurrently": "^6.4.0",
|
||||
"@types/deasync": "^0.1.2",
|
||||
"@types/eslint": "^8.44.1",
|
||||
"@types/eslint-config-prettier": "^6.11.0",
|
||||
"@types/eslint-plugin-mocha": "^10.4.0",
|
||||
"@types/eslint-plugin-prettier": "^3.1.0",
|
||||
"@types/hasha": "^3.0.1",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/koa": "^2.13.7",
|
||||
"@types/koa-logger": "^3.1.2",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/koa-sslify": "^4.0.3",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/koa__cors": "^4.0.0",
|
||||
"@types/koa__router": "^12.0.0",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"chai": "^4.3.7",
|
||||
"concurrently": "^8.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node-dev": "^2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint-all && npm run prettier-check"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,15 @@ import { config, EnvType } from "~config";
|
||||
import { userRouter } from "~routes/users";
|
||||
import { devRouter } from "~routes/dev";
|
||||
import { photosRouter } from "~routes/photos";
|
||||
import { TAPIErrorResponse, TUserJWT } from "~shared/types";
|
||||
|
||||
export const app = new Koa();
|
||||
export interface IAppState extends Koa.DefaultState {
|
||||
user?: TUserJWT;
|
||||
}
|
||||
|
||||
export interface IAppContext extends Koa.DefaultContext {}
|
||||
|
||||
export const app = new Koa<IAppState, IAppContext>();
|
||||
|
||||
const tmpPath = path.join(config.dataDir, "tmp");
|
||||
|
||||
@@ -35,6 +42,7 @@ app.use(
|
||||
if (config.https) {
|
||||
app.use(sslify({ resolver: xForwardedProtoResolver }));
|
||||
}
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.jwtSecret,
|
||||
@@ -93,6 +101,6 @@ app.on("error", (err, ctx) => {
|
||||
console.log(err);
|
||||
ctx.body = {
|
||||
error: err.message,
|
||||
data: false,
|
||||
};
|
||||
data: null,
|
||||
} as TAPIErrorResponse;
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export async function getConfigValue(key: ConfigKey): Promise<string> {
|
||||
}
|
||||
|
||||
try {
|
||||
const pair = await Config.findOneOrFail({ key });
|
||||
const pair = await Config.findOneOrFail({ key }, {});
|
||||
return pair.value;
|
||||
} catch (e) {
|
||||
return defaultValues[key];
|
||||
@@ -67,7 +67,7 @@ export async function setConfigValue(
|
||||
throw new Error(`${key} is not valid config key`);
|
||||
}
|
||||
|
||||
let pair = await Config.findOne({ key });
|
||||
let pair = await Config.findOne({ key }, {});
|
||||
if (!pair) {
|
||||
pair = new Config(key, val);
|
||||
} else {
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import * as mime from "mime-types";
|
||||
import { constants as fsConstants } from "fs";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { TPhotoReqJSON, TPhotoJSON } from "~/shared/types";
|
||||
|
||||
import {
|
||||
AfterRemove,
|
||||
BaseEntity,
|
||||
BeforeInsert,
|
||||
BeforeRemove,
|
||||
@@ -18,37 +17,18 @@ import {
|
||||
} from "typeorm";
|
||||
import { User } from "./User";
|
||||
import {
|
||||
isAlphanumeric,
|
||||
IsAlphanumeric,
|
||||
IsHash,
|
||||
IsIn,
|
||||
IsMimeType,
|
||||
isNumber,
|
||||
Length,
|
||||
Matches,
|
||||
validateOrReject,
|
||||
} from "class-validator";
|
||||
import { config } from "~config";
|
||||
import { fileCheck, getShotDate, resizeToJpeg } from "~util";
|
||||
|
||||
export interface IPhotoJSON {
|
||||
id: number;
|
||||
user: number;
|
||||
hash: string;
|
||||
size: string;
|
||||
format: string;
|
||||
createdAt: number;
|
||||
editedAt: number;
|
||||
shotAt: number;
|
||||
uploaded: boolean;
|
||||
}
|
||||
|
||||
export interface IPhotoReqJSON extends IPhotoJSON {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export const thumbSizes = ["512", "1024", "2048", "original"];
|
||||
export type ThumbSize = typeof thumbSizes[number];
|
||||
export type ThumbSize = (typeof thumbSizes)[number];
|
||||
|
||||
@Entity()
|
||||
@Index(["hash", "size", "user"], { unique: true })
|
||||
@@ -232,7 +212,7 @@ export class Photo extends BaseEntity {
|
||||
}
|
||||
}
|
||||
|
||||
public async toJSON(): Promise<IPhotoJSON> {
|
||||
public async toJSON(): Promise<TPhotoJSON> {
|
||||
if (!isNumber(this.user.id)) {
|
||||
throw new Error("User not loaded");
|
||||
}
|
||||
@@ -252,7 +232,7 @@ export class Photo extends BaseEntity {
|
||||
};
|
||||
}
|
||||
|
||||
public async toReqJSON(): Promise<IPhotoReqJSON> {
|
||||
public async toReqJSON(): Promise<TPhotoReqJSON> {
|
||||
const token = await this.getJWTToken();
|
||||
return { ...(await this.toJSON()), accessToken: token };
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import * as bcrypt from "bcrypt";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { TUserJSON, TUserAuthJSON } from "~/shared/types";
|
||||
|
||||
import {
|
||||
AfterInsert,
|
||||
AfterRemove,
|
||||
BaseEntity,
|
||||
BeforeInsert,
|
||||
BeforeRemove,
|
||||
@@ -17,25 +18,7 @@ import {
|
||||
} from "typeorm";
|
||||
import { config } from "../config";
|
||||
import { Photo } from "./Photo";
|
||||
import {
|
||||
IsAlphanumeric,
|
||||
IsBase32,
|
||||
IsBase64,
|
||||
IsEmail,
|
||||
IsHash,
|
||||
validateOrReject,
|
||||
} from "class-validator";
|
||||
|
||||
export type IUserJSON = Pick<User, "id" | "username" | "isAdmin">;
|
||||
|
||||
export interface IUserJWT extends IUserJSON {
|
||||
ext: number;
|
||||
iat: number;
|
||||
}
|
||||
|
||||
export interface IUserAuthJSON extends IUserJSON {
|
||||
jwt: string;
|
||||
}
|
||||
import { IsAlphanumeric, IsEmail, validateOrReject } from "class-validator";
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@@ -100,12 +83,12 @@ export class User extends BaseEntity {
|
||||
return validateOrReject(this);
|
||||
}
|
||||
|
||||
public toJSON(): IUserJSON {
|
||||
public toJSON(): TUserJSON {
|
||||
const { id, username, isAdmin } = this;
|
||||
return { id, username, isAdmin };
|
||||
}
|
||||
|
||||
public toAuthJSON(): IUserAuthJSON {
|
||||
public toAuthJSON(): TUserAuthJSON {
|
||||
const json = this.toJSON();
|
||||
return { ...json, jwt: this.toJWT() };
|
||||
}
|
||||
14
backend/src/routes/dev.ts
Normal file
14
backend/src/routes/dev.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as Router from "@koa/router";
|
||||
import { Photo } from "~entity/Photo";
|
||||
import { User } from "~entity/User";
|
||||
import { IAppContext, IAppState } from "~app";
|
||||
|
||||
export const devRouter = new Router<IAppState, IAppContext>();
|
||||
|
||||
type ContextType = Parameters<Parameters<(typeof devRouter)["post"]>["2"]>["0"];
|
||||
|
||||
devRouter.get("/dev/clean", async (ctx: ContextType) => {
|
||||
await Photo.remove(await Photo.find());
|
||||
await User.remove(await User.find());
|
||||
ctx.body = { success: true };
|
||||
});
|
||||
@@ -1,53 +1,58 @@
|
||||
import * as Router from "@koa/router";
|
||||
import { IPhotoReqJSON, Photo } from "~entity/Photo";
|
||||
import { User } from "~entity/User";
|
||||
import { IAPIResponse, IPhotosListPagination } from "~types";
|
||||
import * as fs from "fs/promises";
|
||||
import send = require("koa-send");
|
||||
import { Photo } from "~entity/Photo";
|
||||
import {
|
||||
PhotoJSON,
|
||||
PhotosDeleteBody,
|
||||
PhotosListPagination,
|
||||
PhotosNewPostBody,
|
||||
TPhotoByIDDeleteRespBody,
|
||||
TPhotoReqJSON,
|
||||
TPhotosByIDGetRespBody,
|
||||
TPhotosDeleteRespBody,
|
||||
TPhotosGetShowTokenByIDRespBody,
|
||||
TPhotosListRespBody,
|
||||
TPhotosNewRespBody,
|
||||
TPhotosUploadRespBody,
|
||||
} from "~/shared/types";
|
||||
import { getHash, getSize } from "~util";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { config } from "~config";
|
||||
import { ValidationError } from "class-validator";
|
||||
import { In } from "typeorm";
|
||||
import { IAppContext, IAppState } from "~app";
|
||||
import send = require("koa-send");
|
||||
|
||||
export const photosRouter = new Router();
|
||||
export const photosRouter = new Router<IAppState, IAppContext>();
|
||||
|
||||
export interface IPhotosNewPostBody {
|
||||
hash: string | undefined;
|
||||
size: string | undefined;
|
||||
format: string | undefined;
|
||||
}
|
||||
export type IPhotosNewRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.post("/photos/new", async (ctx) => {
|
||||
// Typescript requires explicit type annotations for CFA......
|
||||
type ContextType = Parameters<
|
||||
Parameters<(typeof photosRouter)["post"]>["2"]
|
||||
>["0"];
|
||||
|
||||
photosRouter.post("/photos/new", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
const body = ctx.request.body as IPhotosNewPostBody;
|
||||
const body = PhotosNewPostBody.parse(ctx.request.body);
|
||||
const { hash, size, format } = body;
|
||||
|
||||
if (!(hash && size && format)) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const photo = new Photo(user, hash, size, format);
|
||||
const photo = Photo.create({ user, hash, size, format });
|
||||
|
||||
try {
|
||||
await photo.save();
|
||||
} catch (e) {
|
||||
if (e.code === "ER_DUP_ENTRY") {
|
||||
const photo = await Photo.findOne({ hash, size, user });
|
||||
const photo = await Photo.findOne({ hash, size, user }, {});
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: await photo.toReqJSON(),
|
||||
} as IPhotosNewRespBody;
|
||||
} as TPhotosNewRespBody;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -55,7 +60,6 @@ photosRouter.post("/photos/new", async (ctx) => {
|
||||
(Array.isArray(e) && e.some((e) => e instanceof ValidationError))
|
||||
) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
console.log(e);
|
||||
ctx.throw(500);
|
||||
@@ -64,11 +68,10 @@ photosRouter.post("/photos/new", async (ctx) => {
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: await photo.toReqJSON(),
|
||||
} as IPhotosNewRespBody;
|
||||
} as TPhotosNewRespBody;
|
||||
});
|
||||
|
||||
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.post("/photos/upload/:id", async (ctx) => {
|
||||
photosRouter.post("/photos/upload/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -79,32 +82,28 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user });
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.request.files || Object.keys(ctx.request.files).length === 0) {
|
||||
ctx.throw(400, "No file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (photo.uploaded) {
|
||||
ctx.throw(400, "Already uploaded");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.request.files) {
|
||||
const files = ctx.request.files;
|
||||
if (Object.keys(files).length > 1) {
|
||||
ctx.throw(400, "Too many files");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = Object.values(files)[0];
|
||||
if (Array.isArray(file)) {
|
||||
throw "more than one file uploaded";
|
||||
@@ -115,7 +114,6 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
|
||||
|
||||
if (photoHash !== photo.hash || photoSize !== photo.size) {
|
||||
ctx.throw(400, "Wrong photo");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -129,54 +127,53 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: await photo.toReqJSON(),
|
||||
} as IPhotosUploadRespBody;
|
||||
} as TPhotosUploadRespBody;
|
||||
});
|
||||
|
||||
/**
|
||||
export interface IPhotosByIDPatchBody {
|
||||
}
|
||||
export type IPhotosByIDPatchRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.patch("/photos/byID/:id", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
const { id } = ctx.params as {
|
||||
id: number | undefined;
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const photo = await Photo.findOne({ id, user });
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Some actual editing
|
||||
|
||||
try {
|
||||
photo.editedAt = new Date();
|
||||
await photo.save();
|
||||
} catch (e) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: photo.toReqJSON(),
|
||||
};
|
||||
});
|
||||
*/
|
||||
export interface TPhotosByIDPatchBody {
|
||||
}
|
||||
export type TPhotosByIDPatchRespBody = IAPIResponse<TPhotoReqJSON>;
|
||||
photosRouter.patch("/photos/byID/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
return;
|
||||
}
|
||||
|
||||
export type IPhotosListRespBody = IAPIResponse<IPhotoReqJSON[]>;
|
||||
photosRouter.get("/photos/list", async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
const { id } = ctx.params as {
|
||||
id: number | undefined;
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const photo = await Photo.findOne({ id, user },{});
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Some actual editing
|
||||
|
||||
try {
|
||||
photo.editedAt = new Date();
|
||||
await photo.save();
|
||||
} catch (e) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: photo.toReqJSON(),
|
||||
};
|
||||
});
|
||||
*/
|
||||
|
||||
photosRouter.get("/photos/list", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -196,8 +193,8 @@ photosRouter.get("/photos/list", async (ctx) => {
|
||||
skip = parseInt(skip);
|
||||
}
|
||||
|
||||
if (!num || num > IPhotosListPagination) {
|
||||
num = IPhotosListPagination;
|
||||
if (!num || num > PhotosListPagination) {
|
||||
num = PhotosListPagination;
|
||||
}
|
||||
|
||||
const photos = await Photo.find({
|
||||
@@ -207,18 +204,17 @@ photosRouter.get("/photos/list", async (ctx) => {
|
||||
order: { shotAt: "DESC" },
|
||||
});
|
||||
|
||||
const photosList: IPhotoReqJSON[] = await Promise.all(
|
||||
const photosList: TPhotoReqJSON[] = await Promise.all(
|
||||
photos.map(async (photo) => await photo.toReqJSON()),
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: photosList,
|
||||
} as IPhotosListRespBody;
|
||||
} as TPhotosListRespBody;
|
||||
});
|
||||
|
||||
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.get("/photos/byID/:id", async (ctx) => {
|
||||
photosRouter.get("/photos/byID/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -229,25 +225,23 @@ photosRouter.get("/photos/byID/:id", async (ctx) => {
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user });
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: await photo.toReqJSON(),
|
||||
} as IPhotosByIDGetRespBody;
|
||||
} as TPhotosByIDGetRespBody;
|
||||
});
|
||||
|
||||
photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
||||
photosRouter.get("/photos/showByID/:id/:token", async (ctx: ContextType) => {
|
||||
const { id, token } = ctx.params as {
|
||||
id: string | undefined;
|
||||
token: string | undefined;
|
||||
@@ -255,26 +249,27 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
||||
|
||||
if (!(id && token)) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
jwt.verify(token, config.jwtSecret) as IPhotoReqJSON;
|
||||
jwt.verify(token, config.jwtSecret);
|
||||
} catch (e) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
const photoReqJSON = jwt.decode(token) as IPhotoReqJSON;
|
||||
const photoReqJSON = PhotoJSON.parse(jwt.decode(token));
|
||||
const { user } = photoReqJSON;
|
||||
|
||||
const photo = await Photo.findOne({
|
||||
id: parseInt(id),
|
||||
user: { id: user },
|
||||
});
|
||||
const photo = await Photo.findOne(
|
||||
{
|
||||
id: parseInt(id),
|
||||
user: { id: user },
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -289,7 +284,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
||||
await send(ctx, await photo.getReadyPath("original"));
|
||||
});
|
||||
|
||||
photosRouter.get("/photos/showByID/:id", async (ctx) => {
|
||||
photosRouter.get("/photos/showByID/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -300,16 +295,14 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user });
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -324,9 +317,7 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
|
||||
await send(ctx, await photo.getReadyPath("original"));
|
||||
});
|
||||
|
||||
export type IPhotoShowToken = string;
|
||||
export type IPhotosGetShowTokenByID = IAPIResponse<IPhotoShowToken>;
|
||||
photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
|
||||
photosRouter.get("/photos/getShowByIDToken/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -337,24 +328,21 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user });
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await photo.getJWTToken();
|
||||
|
||||
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID;
|
||||
ctx.body = { error: false, data: token } as TPhotosGetShowTokenByIDRespBody;
|
||||
});
|
||||
|
||||
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>;
|
||||
photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
photosRouter.delete("/photos/byID/:id", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -365,16 +353,14 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user });
|
||||
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
|
||||
|
||||
if (!photo) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
await photo.remove();
|
||||
@@ -382,27 +368,17 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: true,
|
||||
} as IPhotoByIDDeleteRespBody;
|
||||
} as TPhotoByIDDeleteRespBody;
|
||||
});
|
||||
|
||||
export interface IPhotosDeleteBody {
|
||||
photos: IPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
|
||||
photosRouter.post("/photos/delete", async (ctx) => {
|
||||
photosRouter.post("/photos/delete", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
const body = ctx.request.body as IPhotosDeleteBody;
|
||||
const body = PhotosDeleteBody.parse(ctx.request.body);
|
||||
const { photos } = body;
|
||||
|
||||
if (!photos || !Array.isArray(photos) || photos.length == 0) {
|
||||
ctx.throw(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = ctx.state;
|
||||
try {
|
||||
await Photo.delete({
|
||||
@@ -413,11 +389,11 @@ photosRouter.post("/photos/delete", async (ctx) => {
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: true,
|
||||
} as IPhotosDeleteRespBody;
|
||||
} as TPhotosDeleteRespBody;
|
||||
} catch (e) {
|
||||
ctx.body = {
|
||||
data: null,
|
||||
error: "Internal server error",
|
||||
} as IPhotosDeleteRespBody;
|
||||
} as TPhotosDeleteRespBody;
|
||||
}
|
||||
});
|
||||
121
backend/src/routes/users.ts
Normal file
121
backend/src/routes/users.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as Router from "@koa/router";
|
||||
import { ConfigKey, getConfigValue } from "~entity/Config";
|
||||
import { User } from "~entity/User";
|
||||
import {
|
||||
TUserEditRespBody,
|
||||
TUserGetRespBody,
|
||||
TUserLoginRespBody,
|
||||
TUserSignupRespBody,
|
||||
UserEditBody,
|
||||
UserLoginBody,
|
||||
UserSignupBody,
|
||||
} from "~/shared/types";
|
||||
import { IAppContext, IAppState } from "~app";
|
||||
|
||||
export const userRouter = new Router<IAppState, IAppContext>();
|
||||
|
||||
type ContextType = Parameters<
|
||||
Parameters<(typeof userRouter)["post"]>["2"]
|
||||
>["0"];
|
||||
|
||||
userRouter.get("/users/user", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
const jwt = ctx.state.user;
|
||||
const user = await User.findOne(jwt.id, {});
|
||||
|
||||
if (!user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
ctx.body = { error: false, data: user.toAuthJSON() } as TUserGetRespBody;
|
||||
});
|
||||
|
||||
userRouter.post("/users/login", async (ctx: ContextType) => {
|
||||
const request = ctx.request;
|
||||
|
||||
if (!request.body) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
const { username, password } = UserLoginBody.parse(request.body);
|
||||
|
||||
const user = await User.findOne({ username }, {});
|
||||
if (!user || !(await user.verifyPassword(password))) {
|
||||
ctx.throw(404, "User not found");
|
||||
}
|
||||
|
||||
ctx.body = { error: false, data: user.toAuthJSON() } as TUserLoginRespBody;
|
||||
});
|
||||
|
||||
userRouter.post("/users/signup", async (ctx: ContextType) => {
|
||||
const request = ctx.request;
|
||||
|
||||
if (!request.body) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
const { username, password, email } = UserSignupBody.parse(request.body);
|
||||
|
||||
const user = new User(username, email);
|
||||
const users = await User.find();
|
||||
|
||||
if (users.length === 0) {
|
||||
user.isAdmin = true;
|
||||
}
|
||||
if ((await getConfigValue(ConfigKey.signupAllowed)) !== "yes") {
|
||||
if (users.length !== 0) {
|
||||
ctx.throw(400, "Signups not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
await user.setPassword(password);
|
||||
|
||||
try {
|
||||
await user.save();
|
||||
} catch (e) {
|
||||
if (e.code === "ER_DUP_ENTRY") {
|
||||
ctx.throw(400, "User already exists");
|
||||
}
|
||||
console.log(e);
|
||||
ctx.throw(500);
|
||||
}
|
||||
|
||||
ctx.body = { error: false, data: user.toAuthJSON() } as TUserSignupRespBody;
|
||||
});
|
||||
|
||||
userRouter.post("/users/edit", async (ctx: ContextType) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
const jwt = ctx.state.user;
|
||||
const user = await User.findOne(jwt.id, {});
|
||||
const request = ctx.request;
|
||||
|
||||
if (!user) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
const { password } = UserEditBody.parse(request.body);
|
||||
|
||||
if (!password) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
await user.setPassword(password);
|
||||
|
||||
try {
|
||||
await user.save();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ctx.throw(500);
|
||||
}
|
||||
|
||||
ctx.body = { error: false, data: user.toAuthJSON() } as TUserEditRespBody;
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Connection } from "typeorm";
|
||||
import { Config, ConfigKey, setConfigValue } from "~entity/Config";
|
||||
import { app } from "./app";
|
||||
import { app } from "~app";
|
||||
import { config } from "./config";
|
||||
import { connect } from "./config/database";
|
||||
import { connect } from "~config/database";
|
||||
|
||||
async function readConfig() {
|
||||
if (process.env.SIGNUP_ALLOWED) {
|
||||
@@ -25,18 +26,29 @@ async function dumpConfig() {
|
||||
console.log(`${entry.key} = ${entry.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function migrate(connection: Connection) {
|
||||
await connection.runMigrations();
|
||||
console.log("Migrations ran");
|
||||
}
|
||||
|
||||
async function startApp() {
|
||||
app.listen(config.port);
|
||||
console.log(`Listening at ${config.port}`);
|
||||
}
|
||||
|
||||
connect()
|
||||
.then((connection) => {
|
||||
console.log(`Connected to ${connection.name}`);
|
||||
const startApp = () => {
|
||||
app.listen(config.port);
|
||||
console.log(`Listening at ${config.port}`);
|
||||
};
|
||||
|
||||
readConfig()
|
||||
migrate(connection)
|
||||
.then(() =>
|
||||
dumpConfig()
|
||||
.then(() => startApp())
|
||||
readConfig()
|
||||
.then(() =>
|
||||
dumpConfig()
|
||||
.then(() => startApp())
|
||||
.catch((e) => console.log(e)),
|
||||
)
|
||||
.catch((e) => console.log(e)),
|
||||
)
|
||||
.catch((e) => console.log(e));
|
||||
1
backend/src/shared
Symbolic link
1
backend/src/shared
Symbolic link
@@ -0,0 +1 @@
|
||||
../../shared
|
||||
@@ -3,12 +3,13 @@ import { connect } from "config/database";
|
||||
import * as request from "supertest";
|
||||
import { getConnection } from "typeorm";
|
||||
import { app } from "~app";
|
||||
import { Photo, IPhotoReqJSON } from "~entity/Photo";
|
||||
import { Photo } from "~entity/Photo";
|
||||
import {
|
||||
IPhotosDeleteBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewPostBody,
|
||||
} from "~routes/photos";
|
||||
TPhotoReqJSON,
|
||||
TPhotosDeleteBody,
|
||||
TPhotosListRespBody,
|
||||
TPhotosNewPostBody,
|
||||
} from "~shared/types";
|
||||
import * as fs from "fs/promises";
|
||||
import { constants as fsConstants } from "fs";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -30,7 +31,6 @@ import {
|
||||
prepareMetadata,
|
||||
seedDB,
|
||||
} from "./util";
|
||||
import { sleep } from "deasync";
|
||||
import { config } from "~config";
|
||||
|
||||
const callback = app.callback();
|
||||
@@ -61,7 +61,7 @@ describe("photos", function () {
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
const usedPhoto = await seed.dogPhoto.toReqJSON();
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("photos", function () {
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
})
|
||||
.expect(200);
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
});
|
||||
@@ -98,12 +98,12 @@ describe("photos", function () {
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
})
|
||||
.expect(200);
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
|
||||
await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
|
||||
const response2 = await request(callback)
|
||||
await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}`)
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
@@ -121,15 +121,15 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
const dogSmallThumbSize = (
|
||||
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
|
||||
await fs.stat(seed.dogPhoto.getThumbPath("512"))
|
||||
).size;
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogSmallThumbSize,
|
||||
);
|
||||
|
||||
await fs.unlink(await seed.dogPhoto.getReadyPath("512"));
|
||||
await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
|
||||
const response2 = await request(callback)
|
||||
await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
@@ -147,7 +147,7 @@ describe("photos", function () {
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
})
|
||||
.expect(200);
|
||||
expect(parseInt(response.header["content-length"])).to.be.lessThan(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.be.lessThan(
|
||||
dogFileSize,
|
||||
);
|
||||
});
|
||||
@@ -160,23 +160,23 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
const dogSmallThumbSize = (
|
||||
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
|
||||
await fs.stat(seed.dogPhoto.getThumbPath("512"))
|
||||
).size;
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogSmallThumbSize,
|
||||
);
|
||||
|
||||
await fs.unlink(seed.dogPhoto.getThumbPath("512"));
|
||||
const response2 = await request(callback)
|
||||
await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
})
|
||||
.expect(200);
|
||||
const dogSmallThumbSize2 = (
|
||||
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
|
||||
await fs.stat(seed.dogPhoto.getThumbPath("512"))
|
||||
).size;
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogSmallThumbSize2,
|
||||
);
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const listRespBody = listResp.body as IPhotosListRespBody;
|
||||
const listRespBody = listResp.body as TPhotosListRespBody;
|
||||
|
||||
if (listRespBody.error !== false) {
|
||||
expect(listResp.body.error).to.be.false;
|
||||
@@ -202,7 +202,7 @@ describe("photos", function () {
|
||||
const listAnyResp = await request(callback)
|
||||
.get(`/photos/showByID/${photos[0].id}/${photos[0].accessToken}`)
|
||||
.expect(200);
|
||||
expect(parseInt(listAnyResp.header["content-length"])).to.be.oneOf([
|
||||
expect(parseInt(listAnyResp.get("content-length") ?? "")).to.be.oneOf([
|
||||
dogFileSize,
|
||||
catFileSize,
|
||||
]);
|
||||
@@ -220,7 +220,7 @@ describe("photos", function () {
|
||||
const response = await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
|
||||
.expect(200);
|
||||
expect(parseInt(response.header["content-length"])).to.equal(
|
||||
expect(parseInt(response.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
|
||||
@@ -235,7 +235,7 @@ describe("photos", function () {
|
||||
const responseSS = await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}/${tokenSelfSigned}`)
|
||||
.expect(200);
|
||||
expect(parseInt(responseSS.header["content-length"])).to.equal(
|
||||
expect(parseInt(responseSS.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
});
|
||||
@@ -249,7 +249,7 @@ describe("photos", function () {
|
||||
},
|
||||
);
|
||||
|
||||
const response = await request(callback)
|
||||
await request(callback)
|
||||
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
|
||||
.expect(401);
|
||||
});
|
||||
@@ -276,17 +276,17 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(dogHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(dogHash);
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("photos", function () {
|
||||
|
||||
const dbPhotoUpl = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhotoUpl.hash).to.be.equal(dogHash);
|
||||
expect(await dbPhotoUpl.origFileExists()).to.be.equal(true);
|
||||
@@ -318,7 +318,7 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(parseInt(showResp.header["content-length"])).to.equal(
|
||||
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
});
|
||||
@@ -334,17 +334,17 @@ describe("photos", function () {
|
||||
hash: pngHash,
|
||||
size: pngSize,
|
||||
format: pngFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(pngHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(pngHash);
|
||||
|
||||
@@ -361,7 +361,7 @@ describe("photos", function () {
|
||||
|
||||
const dbPhotoUpl = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhotoUpl.hash).to.be.equal(pngHash);
|
||||
expect(dbPhotoUpl.format).to.be.equal(pngFormat);
|
||||
@@ -378,7 +378,7 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(parseInt(showResp.header["content-length"])).to.equal(
|
||||
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
|
||||
pngFileSize,
|
||||
);
|
||||
});
|
||||
@@ -394,12 +394,12 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const response2 = await request(callback)
|
||||
await request(callback)
|
||||
.post("/photos/new")
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user1.toJWT()}`,
|
||||
@@ -409,7 +409,7 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
const dbPhoto = await Photo.find({
|
||||
@@ -431,17 +431,17 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(dogHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(dogHash);
|
||||
|
||||
@@ -474,7 +474,7 @@ describe("photos", function () {
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(parseInt(showResp.header["content-length"])).to.equal(
|
||||
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
|
||||
dogFileSize,
|
||||
);
|
||||
});
|
||||
@@ -490,17 +490,17 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(dogHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(dogHash);
|
||||
|
||||
@@ -517,7 +517,7 @@ describe("photos", function () {
|
||||
|
||||
expect(await dbPhoto.origFileExists()).to.be.equal(false);
|
||||
|
||||
const showResp = await request(callback)
|
||||
await request(callback)
|
||||
.get(`/photos/showByID/${photo.id}`)
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user1.toJWT()}`,
|
||||
@@ -536,17 +536,17 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(dogHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(dogHash);
|
||||
expect(await dbPhoto.origFileExists()).to.be.equal(false);
|
||||
@@ -574,17 +574,17 @@ describe("photos", function () {
|
||||
hash: dogHash,
|
||||
size: dogSize,
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.hash).to.be.equal(dogHash);
|
||||
const dbPhoto = await Photo.findOneOrFail({
|
||||
id: photo.id,
|
||||
user: seed.user1.id as any,
|
||||
user: { id: seed.user1.id },
|
||||
});
|
||||
expect(dbPhoto.hash).to.be.equal(dogHash);
|
||||
expect(await dbPhoto.origFileExists()).to.be.equal(false);
|
||||
@@ -609,7 +609,7 @@ describe("photos", function () {
|
||||
});
|
||||
|
||||
it("should not create a photo with weird properties", async function () {
|
||||
const response = await request(callback)
|
||||
await request(callback)
|
||||
.post("/photos/new")
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user1.toJWT()}`,
|
||||
@@ -619,7 +619,7 @@ describe("photos", function () {
|
||||
hash: "../test",
|
||||
size: "33333",
|
||||
format: dogFormat,
|
||||
} as IPhotosNewPostBody)
|
||||
} as TPhotosNewPostBody)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
@@ -636,13 +636,13 @@ describe("photos", function () {
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
expect(photo.name).to.be.equal("Test1");
|
||||
|
||||
const dbPhoto = await Photo.findOne({
|
||||
id: seed.dogPhoto.id,
|
||||
user: seed.user1.id as any,
|
||||
user: {id:seed.user1.id} ,
|
||||
});
|
||||
|
||||
expect(dbPhoto.name).to.be.equal("Test1");
|
||||
@@ -663,7 +663,7 @@ describe("photos", function () {
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photos = response.body.data as IPhotoReqJSON[];
|
||||
const photos = response.body.data as TPhotoReqJSON[];
|
||||
const userPhotos = [
|
||||
await seed.dogPhoto.toReqJSON(),
|
||||
await seed.catPhoto.toReqJSON(),
|
||||
@@ -686,7 +686,7 @@ describe("photos", function () {
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const photo = response.body.data as IPhotoReqJSON;
|
||||
const photo = response.body.data as TPhotoReqJSON;
|
||||
|
||||
const usedPhoto = seed.catPhoto.toReqJSON();
|
||||
|
||||
@@ -705,7 +705,7 @@ describe("photos", function () {
|
||||
})
|
||||
.send({
|
||||
photos: [await seed.dogPhoto.toReqJSON()],
|
||||
} as IPhotosDeleteBody)
|
||||
} as TPhotosDeleteBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
@@ -737,7 +737,7 @@ describe("photos", function () {
|
||||
await seed.dogPhoto.toReqJSON(),
|
||||
await seed.catPhoto.toReqJSON(),
|
||||
],
|
||||
} as IPhotosDeleteBody)
|
||||
} as TPhotosDeleteBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
@@ -3,16 +3,16 @@ import { connect } from "config/database";
|
||||
import * as request from "supertest";
|
||||
import { getConnection } from "typeorm";
|
||||
import { app } from "~app";
|
||||
import { IUserAuthJSON, User } from "~entity/User";
|
||||
import { User } from "~entity/User";
|
||||
import {
|
||||
IUserEditBody,
|
||||
IUserEditRespBody,
|
||||
IUserGetRespBody,
|
||||
IUserLoginBody,
|
||||
IUserLoginRespBody,
|
||||
IUserSignupBody,
|
||||
IUserSignupRespBody,
|
||||
} from "~routes/users";
|
||||
TUserEditBody,
|
||||
TUserEditRespBody,
|
||||
TUserGetRespBody,
|
||||
TUserLoginBody,
|
||||
TUserLoginRespBody,
|
||||
TUserSignupBody,
|
||||
TUserSignupRespBody,
|
||||
} from "~shared/types";
|
||||
|
||||
import { allowSignups, ISeed, seedDB } from "./util";
|
||||
|
||||
@@ -43,13 +43,13 @@ describe("users", function () {
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserGetRespBody;
|
||||
const body = response.body as TUserGetRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: _, ...user } = body.data;
|
||||
|
||||
expect(user).to.deep.equal(seed.user1.toJSON());
|
||||
@@ -59,17 +59,17 @@ describe("users", function () {
|
||||
const response = await request(callback)
|
||||
.post("/users/login")
|
||||
.set({ "Content-Type": "application/json" })
|
||||
.send({ username: "User1", password: "User1" } as IUserLoginBody)
|
||||
.send({ username: "User1", password: "User1" } as TUserLoginBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserLoginRespBody;
|
||||
const body = response.body as TUserLoginRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: _, ...user } = response.body.data;
|
||||
expect(user).to.deep.equal(seed.user1.toJSON());
|
||||
});
|
||||
@@ -78,12 +78,12 @@ describe("users", function () {
|
||||
const response = await request(callback)
|
||||
.post("/users/login")
|
||||
.set({ "Content-Type": "application/json" })
|
||||
.send({ username: "User1", password: "asdf" } as IUserLoginBody)
|
||||
.send({ username: "User1", password: "asdf" } as TUserLoginBody)
|
||||
.expect(404);
|
||||
|
||||
const body = response.body as IUserLoginRespBody;
|
||||
const body = response.body as TUserLoginRespBody;
|
||||
expect(body.error).to.be.equal("User not found");
|
||||
expect(body.data).to.be.false;
|
||||
expect(body.data).to.be.null;
|
||||
});
|
||||
|
||||
it("should signup user", async function () {
|
||||
@@ -96,17 +96,17 @@ describe("users", function () {
|
||||
username: "NUser1",
|
||||
password: "NUser1",
|
||||
email: "nuser1@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserSignupRespBody;
|
||||
const body = response.body as TUserSignupRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: _, ...user } = body.data;
|
||||
const newUser = await User.findOneOrFail({ username: "NUser1" });
|
||||
expect(user).to.deep.equal(newUser.toJSON());
|
||||
@@ -120,14 +120,14 @@ describe("users", function () {
|
||||
username: "NUser1",
|
||||
password: "NUser1",
|
||||
email: "nuser1@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400);
|
||||
|
||||
const body = response.body as IUserSignupRespBody;
|
||||
const body = response.body as TUserSignupRespBody;
|
||||
|
||||
expect(body.error).to.be.equal("Signups not allowed");
|
||||
expect(body.data).to.be.false;
|
||||
expect(body.data).to.be.null;
|
||||
});
|
||||
|
||||
it("should signup first user and it should be admin, do not signup new users (by default)", async function () {
|
||||
@@ -140,17 +140,17 @@ describe("users", function () {
|
||||
username: "NUser1",
|
||||
password: "NUser1",
|
||||
email: "nuser1@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserSignupRespBody;
|
||||
const body = response.body as TUserSignupRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: _, ...user } = body.data;
|
||||
const newUser = await User.findOneOrFail({ username: "NUser1" });
|
||||
expect(user).to.deep.equal(newUser.toJSON());
|
||||
@@ -163,14 +163,14 @@ describe("users", function () {
|
||||
username: "NUser2",
|
||||
password: "NUser2",
|
||||
email: "nuser2@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400);
|
||||
|
||||
const body2 = response2.body as IUserSignupRespBody;
|
||||
const body2 = response2.body as TUserSignupRespBody;
|
||||
|
||||
expect(body2.error).to.be.equal("Signups not allowed");
|
||||
expect(body2.data).to.be.false;
|
||||
expect(body2.data).to.be.null;
|
||||
});
|
||||
|
||||
it("should signup first user and it should be admin, but not new ones", async function () {
|
||||
@@ -184,17 +184,17 @@ describe("users", function () {
|
||||
username: "NUser1",
|
||||
password: "NUser1",
|
||||
email: "nuser1@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserSignupRespBody;
|
||||
const body = response.body as TUserSignupRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: jwt1, ...user } = body.data;
|
||||
const newUser = await User.findOneOrFail({ username: "NUser1" });
|
||||
expect(user).to.deep.equal(newUser.toJSON());
|
||||
@@ -207,17 +207,17 @@ describe("users", function () {
|
||||
username: "NUser2",
|
||||
password: "NUser2",
|
||||
email: "nuser2@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body2 = response2.body as IUserSignupRespBody;
|
||||
const body2 = response2.body as TUserSignupRespBody;
|
||||
|
||||
if (body2.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: jwt2, ...user2 } = body2.data;
|
||||
const newUser2 = await User.findOneOrFail({ username: "NUser2" });
|
||||
expect(user2).to.deep.equal(newUser2.toJSON());
|
||||
@@ -234,13 +234,13 @@ describe("users", function () {
|
||||
username: "User1",
|
||||
password: "NUser1",
|
||||
email: "user1@users.com",
|
||||
} as IUserSignupBody)
|
||||
} as TUserSignupBody)
|
||||
.expect(400);
|
||||
|
||||
const body = response.body as IUserSignupRespBody;
|
||||
const body = response.body as TUserSignupRespBody;
|
||||
|
||||
expect(body.error).to.be.equal("User already exists");
|
||||
expect(body.data).to.be.false;
|
||||
expect(body.data).to.be.null;
|
||||
});
|
||||
|
||||
it("should change user's password", async function () {
|
||||
@@ -252,15 +252,14 @@ describe("users", function () {
|
||||
})
|
||||
.send({
|
||||
password: "User1NewPass",
|
||||
} as IUserEditBody)
|
||||
} as TUserEditBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as IUserEditRespBody;
|
||||
const body = response.body as TUserEditRespBody;
|
||||
|
||||
if (body.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loginResponse = await request(callback)
|
||||
@@ -269,29 +268,29 @@ describe("users", function () {
|
||||
.send({
|
||||
username: "User1",
|
||||
password: "User1NewPass",
|
||||
} as IUserLoginBody)
|
||||
} as TUserLoginBody)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200);
|
||||
|
||||
const loginBody = loginResponse.body as IUserLoginRespBody;
|
||||
const loginBody = loginResponse.body as TUserLoginRespBody;
|
||||
|
||||
if (loginBody.error !== false) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt: _, ...user } = loginBody.data;
|
||||
expect(user).to.deep.equal(seed.user1.toJSON());
|
||||
|
||||
const badLoginResponse = await request(callback)
|
||||
.post("/users/login")
|
||||
.set({ "Content-Type": "application/json" })
|
||||
.send({ username: "User1", password: "User1" } as IUserLoginBody)
|
||||
.send({ username: "User1", password: "User1" } as TUserLoginBody)
|
||||
.expect(404);
|
||||
|
||||
const badLoginBody = badLoginResponse.body as IUserLoginRespBody;
|
||||
const badLoginBody = badLoginResponse.body as TUserLoginRespBody;
|
||||
|
||||
expect(badLoginBody.error).to.be.equal("User not found");
|
||||
expect(badLoginBody.data).to.be.false;
|
||||
expect(badLoginBody.data).to.be.null;
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { User } from "entity/User";
|
||||
import { Photo } from "~entity/Photo";
|
||||
import { getHash, getSize } from "~util";
|
||||
import { Config, ConfigKey, setConfigValue } from "~entity/Config";
|
||||
import { config } from "chai";
|
||||
|
||||
export const dogPath = "./src/tests/integration/photos/dog.jpg";
|
||||
export const catPath = "./src/tests/integration/photos/cat.jpg";
|
||||
@@ -1,4 +1,3 @@
|
||||
import deasync = require("deasync");
|
||||
import { fromFile } from "hasha";
|
||||
import * as ExifReader from "exifreader";
|
||||
import * as sharp from "sharp";
|
||||
@@ -71,9 +70,4 @@ export async function fileCheck(file: string) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
export const getHashSync: (file: string) => string = deasync(getHash);
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
export const getSizeSync: (file: string) => string = deasync(getSize);
|
||||
}
|
||||
@@ -24,8 +24,5 @@
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./tests/**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"frontend"
|
||||
]
|
||||
}
|
||||
1
dockercomposeexample/.gitignore
vendored
Normal file
1
dockercomposeexample/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dbdata
|
||||
@@ -1,7 +1,8 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
photosapp:
|
||||
image: stepanusatiuk/photos:main
|
||||
# image: stepanusatiuk/photos:main
|
||||
build: ../
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
@@ -21,4 +22,3 @@ services:
|
||||
- ./dbdata:/var/lib/mysql
|
||||
env_file:
|
||||
- db.env
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
npm run typeorm -- migration:run
|
||||
|
||||
npm start
|
||||
npm start
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
|
||||
5
frontend/.prettierrc
Normal file
5
frontend/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
moduleNameMapper: {
|
||||
@@ -11,6 +8,7 @@ module.exports = {
|
||||
"react-spring/renderprops":
|
||||
"<rootDir>/node_modules/react-spring/renderprops.cjs",
|
||||
"react-spring": "<rootDir>/node_modules/react-spring/web.cjs",
|
||||
"~(.*)": "<rootDir>/$1",
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
testEnvironment: "jsdom",
|
||||
|
||||
14129
frontend/package-lock.json
generated
14129
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,64 +3,70 @@
|
||||
"scripts": {
|
||||
"start": "parcel src/index.html",
|
||||
"build": "parcel build src/index.html",
|
||||
"test": "jest",
|
||||
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"test": "jest"
|
||||
"prettier-check": "prettier src/**/*.ts src/**/*.tsx --check",
|
||||
"prettify": "prettier src/**/*.ts src/**/*.tsx --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^4.14.1",
|
||||
"@parcel/config-default": "^2.8.2",
|
||||
"@parcel/transformer-sass": "^2.8.2",
|
||||
"@parcel/transformer-typescript-tsc": "^2.8.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"@parcel/config-default": "^2.9.3",
|
||||
"@parcel/transformer-sass": "^2.9.3",
|
||||
"@parcel/transformer-typescript-tsc": "^2.9.3",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0",
|
||||
"class-validator": "^0.14.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.33.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"flush-promises": "^1.0.2",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-junit": "^14.0.1",
|
||||
"parcel": "^2.8.2",
|
||||
"parcel": "^2.9.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettier": "^2.8.2",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"process": "^0.11.10",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-spring": "^8",
|
||||
"redux": "^4.2.0",
|
||||
"redux": "^4.2.1",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.2",
|
||||
"redux-saga": "^1.2.3",
|
||||
"spark-md5": "^3.0.2",
|
||||
"ts-jest": "^29.0.4",
|
||||
"typescript": "^4.9.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/enzyme": "^3.10.12",
|
||||
"@types/eslint": "^8.4.10",
|
||||
"@types/enzyme": "^3.10.13",
|
||||
"@types/eslint": "^8.44.1",
|
||||
"@types/eslint-config-prettier": "^6.11.0",
|
||||
"@types/eslint-plugin-prettier": "^3.1.0",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/redux-devtools-extension": "^2.13.2",
|
||||
"@types/spark-md5": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function AccountComponent(props: IAccountComponentProps) {
|
||||
return (
|
||||
<Card className="AuthForm" elevation={2}>
|
||||
<form
|
||||
onSubmit={(e: React.FormEvent<any>) => {
|
||||
onSubmit={(e: React.FormEvent<never>) => {
|
||||
e.preventDefault();
|
||||
if (pass.trim()) {
|
||||
props.changePass(pass);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Position, Toaster } from "@blueprintjs/core";
|
||||
import { isNumber } from "class-validator";
|
||||
import { IPhotoReqJSON } from "../../src/entity/Photo";
|
||||
import { TPhotoReqJSON } from "./shared/types";
|
||||
|
||||
export const AppToaster = Toaster.create({
|
||||
className: "recipe-toaster",
|
||||
@@ -44,7 +43,7 @@ export function showPhotoCreateFailToast(f: File, e: string): void {
|
||||
}
|
||||
|
||||
export function showPhotoUploadJSONFailToast(
|
||||
p: IPhotoReqJSON | number,
|
||||
p: TPhotoReqJSON | number,
|
||||
e: string,
|
||||
): void {
|
||||
const photoMsg = typeof p === "number" ? p : p.hash;
|
||||
|
||||
@@ -23,7 +23,9 @@ export class AuthScreenComponent extends React.PureComponent<IAuthScreenProps> {
|
||||
}
|
||||
public render() {
|
||||
const { location } = this.props.history;
|
||||
const { from } = (this.props.location.state as any) || { from: "/" };
|
||||
const { from } = (this.props.location.state as { from: string }) || {
|
||||
from: "/",
|
||||
};
|
||||
const { loggedIn } = this.props;
|
||||
return loggedIn ? (
|
||||
<Redirect to={from} />
|
||||
@@ -46,7 +48,7 @@ export class AuthScreenComponent extends React.PureComponent<IAuthScreenProps> {
|
||||
transform: "translate3d(400px,0,0)",
|
||||
}}
|
||||
>
|
||||
{(_location: any) => (style: any) => (
|
||||
{(_location) => (style) => (
|
||||
<animated.div style={style}>
|
||||
<Switch location={_location}>
|
||||
<Route path="/login" component={Login} />
|
||||
|
||||
@@ -36,7 +36,7 @@ export class LoginComponent extends React.PureComponent<
|
||||
this.props.history.push("/signup");
|
||||
}
|
||||
|
||||
public submit(e: React.FormEvent<any>) {
|
||||
public submit<T extends React.FormEvent>(e: T) {
|
||||
e.preventDefault();
|
||||
const { username, password } = this.state;
|
||||
if (!this.props.inProgress) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class SignupComponent extends React.PureComponent<
|
||||
this.props.history.push("/login");
|
||||
}
|
||||
|
||||
public submit(e: React.FormEvent<any>) {
|
||||
public submit<T extends React.FormEvent>(e: T) {
|
||||
e.preventDefault();
|
||||
const { username, password, email } = this.state;
|
||||
if (!this.props.inProgress) {
|
||||
|
||||
@@ -2,34 +2,29 @@ import "./Home.scss";
|
||||
|
||||
import {
|
||||
Alignment,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Classes,
|
||||
IBreadcrumbProps,
|
||||
Icon,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Navbar,
|
||||
Popover,
|
||||
Spinner,
|
||||
} from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
|
||||
import { animated, config, Transition } from "react-spring/renderprops";
|
||||
import { Dispatch } from "redux";
|
||||
import { IUserJSON } from "../../../src/entity/User";
|
||||
import { TUserJSON } from "../shared/types";
|
||||
import { Account } from "../Account/Account";
|
||||
import { Overview } from "../Photos/Overview";
|
||||
import { toggleDarkMode } from "../redux/localSettings/actions";
|
||||
import { IAppState } from "../redux/reducers";
|
||||
import { logoutUser } from "../redux/user/actions";
|
||||
import { Photo } from "../Photos/Photo";
|
||||
import { PhotoRoute } from "../Photos/PhotoRoute";
|
||||
import { UploadStatus } from "./UploadStatus";
|
||||
|
||||
export interface IHomeProps extends RouteComponentProps {
|
||||
user: IUserJSON | null;
|
||||
user: TUserJSON | null;
|
||||
|
||||
darkMode: boolean;
|
||||
|
||||
@@ -95,28 +90,24 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||
transform: "translate3d(400px,0,0)",
|
||||
}}
|
||||
>
|
||||
{(_location: any) => (style: any) =>
|
||||
(
|
||||
<animated.div
|
||||
style={style}
|
||||
className="viewComponent"
|
||||
>
|
||||
<Switch location={_location}>
|
||||
<Route
|
||||
path="/account"
|
||||
component={Account}
|
||||
/>
|
||||
<Route
|
||||
path="/photos/:id"
|
||||
component={PhotoRoute}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
component={Overview}
|
||||
/>
|
||||
</Switch>
|
||||
</animated.div>
|
||||
)}
|
||||
{(_location) => (style) => (
|
||||
<animated.div
|
||||
style={style}
|
||||
className="viewComponent"
|
||||
>
|
||||
<Switch location={_location}>
|
||||
<Route
|
||||
path="/account"
|
||||
component={Account}
|
||||
/>
|
||||
<Route
|
||||
path="/photos/:id"
|
||||
component={PhotoRoute}
|
||||
/>
|
||||
<Route path="/" component={Overview} />
|
||||
</Switch>
|
||||
</animated.div>
|
||||
)}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Icon, Popover, Spinner } from "@blueprintjs/core";
|
||||
import { Button } from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { IAppState } from "../redux/reducers";
|
||||
@@ -12,9 +12,9 @@ export interface IUploadStatusComponentProps {
|
||||
uploadingQueue: number;
|
||||
}
|
||||
|
||||
export const UploadStatusComponent: React.FunctionComponent<IUploadStatusComponentProps> = (
|
||||
props,
|
||||
) => {
|
||||
export const UploadStatusComponent: React.FunctionComponent<
|
||||
IUploadStatusComponentProps
|
||||
> = (props) => {
|
||||
const { creatingNow, creatingQueue, uploadingNow, uploadingQueue } = props;
|
||||
const uploading =
|
||||
creatingNow > 0 ||
|
||||
|
||||
@@ -56,9 +56,6 @@
|
||||
object-fit: contain;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: $pt-elevation-shadow-4;
|
||||
transition: all 0.3s;
|
||||
|
||||
@@ -218,7 +215,7 @@
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: scale-down;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,18 @@ import "./Overview.scss";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { IAppState } from "../redux/reducers";
|
||||
import { IAppState } from "~/src/redux/reducers";
|
||||
import {
|
||||
photosDeleteCancel,
|
||||
photosDeleteStart,
|
||||
photosLoadStart,
|
||||
} from "../redux/photos/actions";
|
||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { LoadingStub } from "../LoadingStub";
|
||||
import { TPhotoReqJSON } from "~/src/shared/types";
|
||||
import { PhotoCard } from "./PhotoCard";
|
||||
import {
|
||||
Alignment,
|
||||
Button,
|
||||
Classes,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
Navbar,
|
||||
@@ -25,11 +23,10 @@ import {
|
||||
} from "@blueprintjs/core";
|
||||
import { UploadButton } from "./UploadButton";
|
||||
import { Photo } from "./Photo";
|
||||
import { getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { showDeletionToast } from "../AppToaster";
|
||||
import { showDeletionToast } from "~/src/AppToaster";
|
||||
|
||||
export interface IOverviewComponentProps {
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
triedLoading: boolean;
|
||||
allPhotosLoaded: boolean;
|
||||
overviewFetching: boolean;
|
||||
@@ -38,8 +35,8 @@ export interface IOverviewComponentProps {
|
||||
darkMode: boolean;
|
||||
|
||||
fetchPhotos: () => void;
|
||||
startDeletePhotos: (photos: IPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||
startDeletePhotos: (photos: TPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: TPhotoReqJSON[]) => void;
|
||||
}
|
||||
|
||||
const PhotoCardM = React.memo(PhotoCard);
|
||||
@@ -83,7 +80,7 @@ export const OverviewComponent: React.FunctionComponent<
|
||||
(
|
||||
acc: Record<
|
||||
string,
|
||||
Record<string, Record<string, IPhotoReqJSON[]>>
|
||||
Record<string, Record<string, TPhotoReqJSON[]>>
|
||||
>,
|
||||
photo,
|
||||
) => {
|
||||
@@ -111,7 +108,7 @@ export const OverviewComponent: React.FunctionComponent<
|
||||
const els = Object.keys(dates[year]).reduce(
|
||||
(accMonths: JSX.Element[], month): JSX.Element[] => {
|
||||
const photos = Object.values(dates[year][month]).reduce(
|
||||
(accDays: IPhotoReqJSON[], day) => {
|
||||
(accDays: TPhotoReqJSON[], day) => {
|
||||
return [...day, ...accDays];
|
||||
},
|
||||
[],
|
||||
@@ -267,9 +264,9 @@ function mapStateToProps(state: IAppState) {
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
fetchPhotos: () => dispatch(photosLoadStart()),
|
||||
startDeletePhotos: (photos: IPhotoReqJSON[]) =>
|
||||
startDeletePhotos: (photos: TPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteStart(photos)),
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) =>
|
||||
cancelDelete: (photos: TPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteCancel(photos)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,28 +2,31 @@ import "./Photo.scss";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { TPhotoReqJSON } from "~/src/shared/types";
|
||||
import { LoadingStub } from "../LoadingStub";
|
||||
import {
|
||||
fetchPhoto,
|
||||
getPhotoImgPath,
|
||||
getPhotoThumbPath,
|
||||
} from "../redux/api/photos";
|
||||
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { photoLoadStart } from "../redux/photos/actions";
|
||||
import { IPhotoState } from "../redux/photos/reducer";
|
||||
import { TPhotoState } from "../redux/photos/reducer";
|
||||
import { IAppState } from "../redux/reducers";
|
||||
import { LargeSize, PreviewSize } from "./helper";
|
||||
|
||||
export interface IPhotoComponentProps {
|
||||
id: number;
|
||||
photo: IPhotoReqJSON | undefined;
|
||||
photoState: IPhotoState | undefined;
|
||||
type StateProps = {
|
||||
photo: TPhotoReqJSON | undefined;
|
||||
photoState: TPhotoState | undefined;
|
||||
};
|
||||
|
||||
type DispatchProps = {
|
||||
fetchPhoto: (id: number) => void;
|
||||
close: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const PhotoComponent: React.FunctionComponent<IPhotoComponentProps> = (
|
||||
type OwnProps = {
|
||||
id: number;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
export type TPhotoComponentProps = StateProps & DispatchProps & OwnProps;
|
||||
|
||||
export const PhotoComponent: React.FunctionComponent<TPhotoComponentProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [smallPreviewFetching, setSmallPreviewFetching] =
|
||||
@@ -118,7 +121,7 @@ export const PhotoComponent: React.FunctionComponent<IPhotoComponentProps> = (
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: IAppState, props: IPhotoComponentProps) {
|
||||
function mapStateToProps(state: IAppState, props: OwnProps) {
|
||||
const { id } = props;
|
||||
return {
|
||||
photo: state.photos?.photos?.find((p) => p.id === id),
|
||||
@@ -130,8 +133,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return { fetchPhoto: (id: number) => dispatch(photoLoadStart(id)) };
|
||||
}
|
||||
|
||||
// Because https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16990
|
||||
export const Photo = connect(
|
||||
export const Photo = connect<StateProps, DispatchProps, OwnProps, IAppState>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(PhotoComponent) as any;
|
||||
)(PhotoComponent);
|
||||
|
||||
@@ -8,40 +8,39 @@ import {
|
||||
Spinner,
|
||||
} from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { TPhotoReqJSON } from "~/src/shared/types";
|
||||
import { getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { showDeletionToast } from "../AppToaster";
|
||||
import { Dispatch } from "redux";
|
||||
import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions";
|
||||
import { connect } from "react-redux";
|
||||
import { LoadingStub } from "../LoadingStub";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { LargeSize, PreviewSize } from "./helper";
|
||||
import { PreviewSize } from "./helper";
|
||||
|
||||
export interface IPhotoCardComponentProps extends RouteComponentProps {
|
||||
photo: IPhotoReqJSON;
|
||||
export interface TPhotoCardComponentProps extends RouteComponentProps {
|
||||
photo: TPhotoReqJSON;
|
||||
selected: boolean;
|
||||
id: string;
|
||||
|
||||
deletePhoto: (photos: IPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||
deletePhoto: (photos: TPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: TPhotoReqJSON[]) => void;
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
export interface IPhotoCardComponentState {
|
||||
export interface TPhotoCardComponentState {
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const defaultPhotoCardState: IPhotoCardComponentState = {
|
||||
const defaultPhotoCardState: TPhotoCardComponentState = {
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
@ContextMenuTarget
|
||||
export class PhotoCardComponent extends React.PureComponent<
|
||||
IPhotoCardComponentProps,
|
||||
IPhotoCardComponentState
|
||||
TPhotoCardComponentProps,
|
||||
TPhotoCardComponentState
|
||||
> {
|
||||
constructor(props: IPhotoCardComponentProps) {
|
||||
constructor(props: TPhotoCardComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
@@ -122,9 +121,9 @@ export class PhotoCardComponent extends React.PureComponent<
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
deletePhoto: (photos: IPhotoReqJSON[]) =>
|
||||
deletePhoto: (photos: TPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteStart(photos)),
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) =>
|
||||
cancelDelete: (photos: TPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteCancel(photos)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ function getId(props: RouteComponentProps) {
|
||||
return parseInt((props.match?.params as { id: string }).id);
|
||||
}
|
||||
|
||||
export const PhotoRouteComponent: React.FunctionComponent<RouteComponentProps> = (
|
||||
props: RouteComponentProps,
|
||||
) => {
|
||||
export const PhotoRouteComponent: React.FunctionComponent<
|
||||
RouteComponentProps
|
||||
> = (props: RouteComponentProps) => {
|
||||
const id = getId(props);
|
||||
|
||||
return <Photo id={id} />;
|
||||
return <Photo id={id} close={() => {}} />;
|
||||
};
|
||||
|
||||
export const PhotoRoute = withRouter(PhotoRouteComponent);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import {
|
||||
IUserLoginRespBody,
|
||||
IUserSignupRespBody,
|
||||
} from "../../../../../src/routes/users";
|
||||
TUserLoginRespBody,
|
||||
TUserSignupRespBody,
|
||||
UserLoginRespBody,
|
||||
UserSignupRespBody,
|
||||
} from "~/src/shared/types";
|
||||
import { fetchJSON } from "../utils";
|
||||
|
||||
export async function login(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<IUserLoginRespBody> {
|
||||
return fetchJSON("/users/login", "POST", {
|
||||
): Promise<TUserLoginRespBody> {
|
||||
return fetchJSON("/users/login", "POST", UserLoginRespBody, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
@@ -18,8 +20,8 @@ export async function signup(
|
||||
username: string,
|
||||
password: string,
|
||||
email: string,
|
||||
): Promise<IUserSignupRespBody> {
|
||||
return fetchJSON("/users/signup", "POST", {
|
||||
): Promise<TUserSignupRespBody> {
|
||||
return fetchJSON("/users/signup", "POST", UserSignupRespBody, {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||
import {
|
||||
IPhotosByIDGetRespBody,
|
||||
IPhotosDeleteRespBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewRespBody,
|
||||
IPhotosUploadRespBody,
|
||||
} from "../../../../src/routes/photos";
|
||||
import { apiRoot } from "../../env";
|
||||
PhotosByIDGetRespBody,
|
||||
PhotosDeleteRespBody,
|
||||
PhotosListRespBody,
|
||||
PhotosNewRespBody,
|
||||
PhotosUploadRespBody,
|
||||
TPhotoReqJSON,
|
||||
} from "~/src/shared/types";
|
||||
import {
|
||||
TPhotosByIDGetRespBody,
|
||||
TPhotosDeleteRespBody,
|
||||
TPhotosListRespBody,
|
||||
TPhotosNewRespBody,
|
||||
TPhotosUploadRespBody,
|
||||
} from "~/src/shared/types";
|
||||
import { apiRoot } from "~src/env";
|
||||
import { fetchJSONAuth } from "./utils";
|
||||
|
||||
export function getPhotoImgPath(photo: IPhotoReqJSON): string {
|
||||
export function getPhotoImgPath(photo: TPhotoReqJSON): string {
|
||||
return `${apiRoot}/photos/showByID/${photo.id}/${photo.accessToken}`;
|
||||
}
|
||||
|
||||
export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string {
|
||||
export function getPhotoThumbPath(photo: TPhotoReqJSON, size: number): string {
|
||||
return `${apiRoot}/photos/showByID/${photo.id}/${
|
||||
photo.accessToken
|
||||
}?size=${size.toString()}`;
|
||||
@@ -22,35 +29,50 @@ export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string {
|
||||
export async function fetchPhotosList(
|
||||
skip: number,
|
||||
num: number,
|
||||
): Promise<IPhotosListRespBody> {
|
||||
): Promise<TPhotosListRespBody> {
|
||||
const params = new URLSearchParams({
|
||||
skip: skip.toString(),
|
||||
num: num.toString(),
|
||||
});
|
||||
return fetchJSONAuth(`/photos/list?${params.toString()}`, "GET");
|
||||
return fetchJSONAuth(
|
||||
`/photos/list?${params.toString()}`,
|
||||
"GET",
|
||||
PhotosListRespBody,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchPhoto(id: number): Promise<IPhotosByIDGetRespBody> {
|
||||
return fetchJSONAuth(`/photos/byID/${id}`, "GET");
|
||||
export async function fetchPhoto(id: number): Promise<TPhotosByIDGetRespBody> {
|
||||
return fetchJSONAuth(`/photos/byID/${id}`, "GET", PhotosByIDGetRespBody);
|
||||
}
|
||||
|
||||
export async function createPhoto(
|
||||
hash: string,
|
||||
size: string,
|
||||
format: string,
|
||||
): Promise<IPhotosNewRespBody> {
|
||||
return fetchJSONAuth("/photos/new", "POST", { hash, size, format });
|
||||
): Promise<TPhotosNewRespBody> {
|
||||
return fetchJSONAuth("/photos/new", "POST", PhotosNewRespBody, {
|
||||
hash,
|
||||
size,
|
||||
format,
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadPhoto(
|
||||
file: File,
|
||||
id: number,
|
||||
): Promise<IPhotosUploadRespBody> {
|
||||
return fetchJSONAuth(`/photos/upload/${id}`, "POST", file);
|
||||
): Promise<TPhotosUploadRespBody> {
|
||||
return fetchJSONAuth(
|
||||
`/photos/upload/${id}`,
|
||||
"POST",
|
||||
PhotosUploadRespBody,
|
||||
file,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deletePhotos(
|
||||
photos: IPhotoReqJSON[],
|
||||
): Promise<IPhotosDeleteRespBody> {
|
||||
return fetchJSONAuth(`/photos/delete`, "POST", { photos });
|
||||
photos: TPhotoReqJSON[],
|
||||
): Promise<TPhotosDeleteRespBody> {
|
||||
return fetchJSONAuth(`/photos/delete`, "POST", PhotosDeleteRespBody, {
|
||||
photos,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { fetchJSONAuth } from "../utils";
|
||||
import { IUserEditRespBody, IUserGetRespBody } from "../../../../../src/routes/users";
|
||||
import {
|
||||
TUserEditRespBody,
|
||||
TUserGetRespBody,
|
||||
UserEditRespBody,
|
||||
UserGetRespBody,
|
||||
} from "~/src/shared/types";
|
||||
|
||||
export async function fetchUser(): Promise<IUserGetRespBody> {
|
||||
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise<
|
||||
IUserGetRespBody
|
||||
>;
|
||||
export async function fetchUser(): Promise<TUserGetRespBody> {
|
||||
return fetchJSONAuth("/users/user", "GET", UserGetRespBody);
|
||||
}
|
||||
|
||||
export async function changeUserPassword(
|
||||
newPassword: string,
|
||||
): Promise<IUserEditRespBody> {
|
||||
return fetchJSONAuth("/users/edit", "POST", {
|
||||
): Promise<TUserEditRespBody> {
|
||||
return fetchJSONAuth("/users/edit", "POST", UserEditRespBody, {
|
||||
password: newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiRoot } from "../../env";
|
||||
import { apiRoot } from "~src/env";
|
||||
|
||||
let token: string | null;
|
||||
|
||||
@@ -14,49 +14,44 @@ export function deleteToken(): void {
|
||||
token = null;
|
||||
}
|
||||
|
||||
export async function fetchJSON(
|
||||
export async function fetchJSON<T, P extends { parse: (string) => T }>(
|
||||
path: string,
|
||||
method: string,
|
||||
parser: P,
|
||||
body?: string | Record<string, unknown> | File,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (typeof body === "object" && !(body instanceof File)) {
|
||||
body = JSON.stringify(body);
|
||||
headers = {
|
||||
...headers,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
): Promise<T> {
|
||||
const reqBody = () =>
|
||||
body instanceof File
|
||||
? (() => {
|
||||
const fd = new FormData();
|
||||
fd.append("photo", body);
|
||||
return fd;
|
||||
})()
|
||||
: JSON.stringify(body);
|
||||
|
||||
if (body instanceof File) {
|
||||
const formData = new FormData();
|
||||
formData.append("photo", body);
|
||||
const response = await fetch(apiRoot + path, {
|
||||
method,
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
const json = (await response.json()) as Record<string, unknown>;
|
||||
return json;
|
||||
}
|
||||
const reqHeaders = () =>
|
||||
body instanceof File
|
||||
? headers
|
||||
: { ...headers, "Content-Type": "application/json" };
|
||||
|
||||
const response = await fetch(apiRoot + path, {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
headers: reqHeaders(),
|
||||
body: reqBody(),
|
||||
});
|
||||
const json = (await response.json()) as Record<string, unknown>;
|
||||
return json;
|
||||
return parser.parse(await response.json());
|
||||
}
|
||||
|
||||
export async function fetchJSONAuth(
|
||||
export async function fetchJSONAuth<T, P extends { parse: (string) => T }>(
|
||||
path: string,
|
||||
method: string,
|
||||
parser: P,
|
||||
body?: string | Record<string, unknown> | File,
|
||||
headers?: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
): Promise<T> {
|
||||
if (token) {
|
||||
return fetchJSON(path, method, body, {
|
||||
return fetchJSON(path, method, parser, body, {
|
||||
...headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Action } from "redux";
|
||||
import { IUserAuthJSON } from "../../../../src/entity/User";
|
||||
import { TUserAuthJSON } from "~/src/shared/types";
|
||||
|
||||
export enum AuthTypes {
|
||||
AUTH_START = "AUTH_START",
|
||||
@@ -28,7 +28,7 @@ export interface ISignupStartAction extends Action {
|
||||
|
||||
export interface IAuthSuccessAction extends Action {
|
||||
type: AuthTypes.AUTH_SUCCESS;
|
||||
payload: IUserAuthJSON;
|
||||
payload: TUserAuthJSON;
|
||||
}
|
||||
|
||||
export interface IAuthFailureAction extends Action {
|
||||
@@ -64,7 +64,7 @@ export function signupStart(
|
||||
};
|
||||
}
|
||||
|
||||
export function authSuccess(user: IUserAuthJSON): IAuthSuccessAction {
|
||||
export function authSuccess(user: TUserAuthJSON): IAuthSuccessAction {
|
||||
return { type: AuthTypes.AUTH_SUCCESS, payload: user };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Reducer } from "react";
|
||||
|
||||
import { setToken } from "../../redux/api/utils";
|
||||
import { UserAction, UserTypes } from "../../redux/user/actions";
|
||||
import { setToken } from "~src/redux/api/utils";
|
||||
import { UserAction, UserTypes } from "~src/redux/user/actions";
|
||||
import { AuthAction, AuthTypes } from "./actions";
|
||||
|
||||
export interface IAuthState {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
race,
|
||||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import { login, signup } from "../../redux/api/auth";
|
||||
import { login, signup } from "~src/redux/api/auth";
|
||||
|
||||
import {
|
||||
authFail,
|
||||
@@ -26,9 +26,8 @@ function* startSpinner() {
|
||||
|
||||
function* authStart(action: IAuthStartAction) {
|
||||
const { username, password } = action.payload;
|
||||
const spinner = yield fork(startSpinner);
|
||||
try {
|
||||
const spinner = yield fork(startSpinner);
|
||||
|
||||
const { response, timeout } = yield race({
|
||||
response: call(login, username, password),
|
||||
timeout: delay(10000),
|
||||
@@ -47,15 +46,15 @@ function* authStart(action: IAuthStartAction) {
|
||||
yield put(authFail(response.error));
|
||||
}
|
||||
} catch (e) {
|
||||
yield cancel(spinner);
|
||||
yield put(authFail("Internal error"));
|
||||
}
|
||||
}
|
||||
|
||||
function* signupStart(action: ISignupStartAction) {
|
||||
const { username, password, email } = action.payload;
|
||||
const spinner = yield fork(startSpinner);
|
||||
try {
|
||||
const spinner = yield fork(startSpinner);
|
||||
|
||||
const { response, timeout } = yield race({
|
||||
response: call(signup, username, password, email),
|
||||
timeout: delay(10000),
|
||||
@@ -74,6 +73,7 @@ function* signupStart(action: ISignupStartAction) {
|
||||
yield put(authFail(response.error));
|
||||
}
|
||||
} catch (e) {
|
||||
yield cancel(spinner);
|
||||
yield put(authFail(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Reducer } from "react";
|
||||
import { UserAction, UserTypes } from "../../redux/user/actions";
|
||||
import { UserAction, UserTypes } from "~src/redux/user/actions";
|
||||
|
||||
import { LocalSettingsAction, LocalSettingsTypes } from "./actions";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Action } from "redux";
|
||||
import { IPhotoReqJSON, Photo } from "../../../../src/entity/Photo";
|
||||
import { TPhotoReqJSON } from "~src/shared/types";
|
||||
import {
|
||||
showPhotoCreateFailToast,
|
||||
showPhotoUploadFileFailToast,
|
||||
showPhotoUploadJSONFailToast,
|
||||
} from "../../AppToaster";
|
||||
} from "~src/AppToaster";
|
||||
|
||||
export enum PhotoTypes {
|
||||
PHOTOS_LOAD_START = "PHOTOS_LOAD",
|
||||
@@ -29,242 +29,242 @@ export enum PhotoTypes {
|
||||
PHOTOS_DELETE_CANCEL = "PHOTOS_DELETE_CANCEL",
|
||||
}
|
||||
|
||||
export interface IPhotosLoadStartAction extends Action {
|
||||
export interface TPhotosLoadStartAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_LOAD_START;
|
||||
}
|
||||
|
||||
export interface IPhotosLoadSuccessAction extends Action {
|
||||
export interface TPhotosLoadSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_LOAD_SUCCESS;
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotosLoadFailAction extends Action {
|
||||
export interface TPhotosLoadFailAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_LOAD_FAIL;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadStartAction extends Action {
|
||||
export interface TPhotoLoadStartAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_START;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadSuccessAction extends Action {
|
||||
export interface TPhotoLoadSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_SUCCESS;
|
||||
photo: IPhotoReqJSON;
|
||||
photo: TPhotoReqJSON;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadFailAction extends Action {
|
||||
export interface TPhotoLoadFailAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_FAIL;
|
||||
id: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotosUploadStartAction extends Action {
|
||||
export interface TPhotosUploadStartAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_UPLOAD_START;
|
||||
files: FileList;
|
||||
}
|
||||
|
||||
export interface IPhotoCreateQueue extends Action {
|
||||
export interface TPhotoCreateQueue extends Action {
|
||||
type: PhotoTypes.PHOTO_CREATE_QUEUE;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface IPhotoUploadQueue extends Action {
|
||||
export interface TPhotoUploadQueue extends Action {
|
||||
type: PhotoTypes.PHOTO_UPLOAD_QUEUE;
|
||||
file: File;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IPhotoCreateStart extends Action {
|
||||
export interface TPhotoCreateStart extends Action {
|
||||
type: PhotoTypes.PHOTO_CREATE_START;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface IPhotoUploadStart extends Action {
|
||||
export interface TPhotoUploadStart extends Action {
|
||||
type: PhotoTypes.PHOTO_UPLOAD_START;
|
||||
file: File;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IPhotoUploadSuccessAction extends Action {
|
||||
export interface TPhotoUploadSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTO_UPLOAD_SUCCESS;
|
||||
photo: IPhotoReqJSON;
|
||||
photo: TPhotoReqJSON;
|
||||
}
|
||||
|
||||
export interface IPhotoUploadFailAction extends Action {
|
||||
export interface TPhotoUploadFailAction extends Action {
|
||||
type: PhotoTypes.PHOTO_UPLOAD_FAIL;
|
||||
photo: IPhotoReqJSON | number;
|
||||
photo: TPhotoReqJSON | number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotoCreateSuccessAction extends Action {
|
||||
export interface TPhotoCreateSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTO_CREATE_SUCCESS;
|
||||
photo: IPhotoReqJSON;
|
||||
photo: TPhotoReqJSON;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface IPhotoCreateFailAction extends Action {
|
||||
export interface TPhotoCreateFailAction extends Action {
|
||||
type: PhotoTypes.PHOTO_CREATE_FAIL;
|
||||
file: File;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotosDeleteStartAction extends Action {
|
||||
export interface TPhotosDeleteStartAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_START;
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotosDeleteSuccessAction extends Action {
|
||||
export interface TPhotosDeleteSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_SUCCESS;
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotosDeleteFailAction extends Action {
|
||||
export interface TPhotosDeleteFailAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_FAIL;
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IPhotosDeleteCancelAction extends Action {
|
||||
export interface TPhotosDeleteCancelAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_CANCEL;
|
||||
photos: IPhotoReqJSON[];
|
||||
photos: TPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotosStartFetchingSpinner extends Action {
|
||||
export interface TPhotosStartFetchingSpinner extends Action {
|
||||
type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER;
|
||||
}
|
||||
|
||||
export function photoCreateQueue(file: File): IPhotoCreateQueue {
|
||||
export function photoCreateQueue(file: File): TPhotoCreateQueue {
|
||||
return { type: PhotoTypes.PHOTO_CREATE_QUEUE, file };
|
||||
}
|
||||
|
||||
export function photoUploadQueue(file: File, id: number): IPhotoUploadQueue {
|
||||
export function photoUploadQueue(file: File, id: number): TPhotoUploadQueue {
|
||||
return { type: PhotoTypes.PHOTO_UPLOAD_QUEUE, file, id };
|
||||
}
|
||||
|
||||
export function photoCreateStart(file: File): IPhotoCreateStart {
|
||||
export function photoCreateStart(file: File): TPhotoCreateStart {
|
||||
return { type: PhotoTypes.PHOTO_CREATE_START, file };
|
||||
}
|
||||
|
||||
export function photoUploadStart(file: File, id: number): IPhotoUploadStart {
|
||||
export function photoUploadStart(file: File, id: number): TPhotoUploadStart {
|
||||
return { type: PhotoTypes.PHOTO_UPLOAD_START, file, id };
|
||||
}
|
||||
|
||||
export function photosLoadStart(): IPhotosLoadStartAction {
|
||||
export function photosLoadStart(): TPhotosLoadStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_LOAD_START };
|
||||
}
|
||||
|
||||
export function photoLoadStart(id: number): IPhotoLoadStartAction {
|
||||
export function photoLoadStart(id: number): TPhotoLoadStartAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_START, id };
|
||||
}
|
||||
|
||||
export function photosUploadStart(files: FileList): IPhotosUploadStartAction {
|
||||
export function photosUploadStart(files: FileList): TPhotosUploadStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_UPLOAD_START, files };
|
||||
}
|
||||
|
||||
export function photoUploadSuccess(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoUploadSuccessAction {
|
||||
photo: TPhotoReqJSON,
|
||||
): TPhotoUploadSuccessAction {
|
||||
return { type: PhotoTypes.PHOTO_UPLOAD_SUCCESS, photo };
|
||||
}
|
||||
|
||||
export function photoUploadFail(
|
||||
photo: IPhotoReqJSON | number,
|
||||
photo: TPhotoReqJSON | number,
|
||||
error: string,
|
||||
): IPhotoUploadFailAction {
|
||||
): TPhotoUploadFailAction {
|
||||
showPhotoUploadJSONFailToast(photo, error);
|
||||
return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error };
|
||||
}
|
||||
|
||||
export function photoUploadFailWithFile(
|
||||
photo: IPhotoReqJSON | number,
|
||||
photo: TPhotoReqJSON | number,
|
||||
file: File,
|
||||
error: string,
|
||||
): IPhotoUploadFailAction {
|
||||
): TPhotoUploadFailAction {
|
||||
showPhotoUploadFileFailToast(file, error);
|
||||
return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error };
|
||||
}
|
||||
|
||||
export function photoCreateSuccess(
|
||||
photo: IPhotoReqJSON,
|
||||
photo: TPhotoReqJSON,
|
||||
file: File,
|
||||
): IPhotoCreateSuccessAction {
|
||||
): TPhotoCreateSuccessAction {
|
||||
return { type: PhotoTypes.PHOTO_CREATE_SUCCESS, photo, file };
|
||||
}
|
||||
|
||||
export function photoCreateFail(
|
||||
file: File,
|
||||
error: string,
|
||||
): IPhotoCreateFailAction {
|
||||
): TPhotoCreateFailAction {
|
||||
showPhotoCreateFailToast(file, error);
|
||||
return { type: PhotoTypes.PHOTO_CREATE_FAIL, file, error };
|
||||
}
|
||||
|
||||
export function photosLoadSuccess(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosLoadSuccessAction {
|
||||
photos: TPhotoReqJSON[],
|
||||
): TPhotosLoadSuccessAction {
|
||||
return { type: PhotoTypes.PHOTOS_LOAD_SUCCESS, photos };
|
||||
}
|
||||
|
||||
export function photosLoadFail(error: string): IPhotosLoadFailAction {
|
||||
export function photosLoadFail(error: string): TPhotosLoadFailAction {
|
||||
return { type: PhotoTypes.PHOTOS_LOAD_FAIL, error };
|
||||
}
|
||||
|
||||
export function photoLoadSuccess(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoLoadSuccessAction {
|
||||
photo: TPhotoReqJSON,
|
||||
): TPhotoLoadSuccessAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_SUCCESS, photo };
|
||||
}
|
||||
|
||||
export function photoLoadFail(id: number, error: string): IPhotoLoadFailAction {
|
||||
export function photoLoadFail(id: number, error: string): TPhotoLoadFailAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_FAIL, id, error };
|
||||
}
|
||||
|
||||
export function photosDeleteStart(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteStartAction {
|
||||
photos: TPhotoReqJSON[],
|
||||
): TPhotosDeleteStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_START, photos };
|
||||
}
|
||||
export function photosDeleteSuccess(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteSuccessAction {
|
||||
photos: TPhotoReqJSON[],
|
||||
): TPhotosDeleteSuccessAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_SUCCESS, photos };
|
||||
}
|
||||
export function photosDeleteFail(
|
||||
photos: IPhotoReqJSON[],
|
||||
photos: TPhotoReqJSON[],
|
||||
error?: string,
|
||||
): IPhotosDeleteFailAction {
|
||||
): TPhotosDeleteFailAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_FAIL, photos, error };
|
||||
}
|
||||
|
||||
export function photosDeleteCancel(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteCancelAction {
|
||||
photos: TPhotoReqJSON[],
|
||||
): TPhotosDeleteCancelAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_CANCEL, photos };
|
||||
}
|
||||
|
||||
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner {
|
||||
export function photosStartFetchingSpinner(): TPhotosStartFetchingSpinner {
|
||||
return { type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER };
|
||||
}
|
||||
|
||||
export type PhotoAction =
|
||||
| IPhotosLoadStartAction
|
||||
| IPhotosLoadFailAction
|
||||
| IPhotosLoadSuccessAction
|
||||
| IPhotosStartFetchingSpinner
|
||||
| IPhotosUploadStartAction
|
||||
| IPhotoCreateFailAction
|
||||
| IPhotoCreateSuccessAction
|
||||
| IPhotoUploadFailAction
|
||||
| IPhotoUploadSuccessAction
|
||||
| IPhotosDeleteFailAction
|
||||
| IPhotosDeleteStartAction
|
||||
| IPhotosDeleteSuccessAction
|
||||
| IPhotosDeleteCancelAction
|
||||
| IPhotoLoadFailAction
|
||||
| IPhotoLoadStartAction
|
||||
| IPhotoLoadSuccessAction
|
||||
| IPhotoUploadQueue
|
||||
| IPhotoCreateQueue
|
||||
| IPhotoCreateStart
|
||||
| IPhotoUploadStart;
|
||||
| TPhotosLoadStartAction
|
||||
| TPhotosLoadFailAction
|
||||
| TPhotosLoadSuccessAction
|
||||
| TPhotosStartFetchingSpinner
|
||||
| TPhotosUploadStartAction
|
||||
| TPhotoCreateFailAction
|
||||
| TPhotoCreateSuccessAction
|
||||
| TPhotoUploadFailAction
|
||||
| TPhotoUploadSuccessAction
|
||||
| TPhotosDeleteFailAction
|
||||
| TPhotosDeleteStartAction
|
||||
| TPhotosDeleteSuccessAction
|
||||
| TPhotosDeleteCancelAction
|
||||
| TPhotoLoadFailAction
|
||||
| TPhotoLoadStartAction
|
||||
| TPhotoLoadSuccessAction
|
||||
| TPhotoUploadQueue
|
||||
| TPhotoCreateQueue
|
||||
| TPhotoCreateStart
|
||||
| TPhotoUploadStart;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Reducer } from "redux";
|
||||
import { IPhotoJSON, IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||
import { UserAction, UserTypes } from "../../redux/user/actions";
|
||||
import { TPhotoReqJSON } from "~/src/shared/types";
|
||||
import { UserAction, UserTypes } from "~src/redux/user/actions";
|
||||
import { PhotoAction, PhotoTypes } from "./actions";
|
||||
|
||||
export interface IPhotoState {
|
||||
export interface TPhotoState {
|
||||
fetching: boolean;
|
||||
fetchingError: string | null;
|
||||
}
|
||||
|
||||
export interface IPhotosState {
|
||||
photos: IPhotoReqJSON[];
|
||||
export interface TPhotosState {
|
||||
photos: TPhotoReqJSON[];
|
||||
|
||||
photoStates: Record<number, IPhotoState>;
|
||||
photoStates: Record<number, TPhotoState>;
|
||||
|
||||
overviewFetching: boolean;
|
||||
allPhotosLoaded: boolean;
|
||||
@@ -24,10 +24,10 @@ export interface IPhotosState {
|
||||
photoUploadQueue: Record<number, File>;
|
||||
photosUploading: number;
|
||||
|
||||
deleteCache: Record<number, IPhotoReqJSON>;
|
||||
deleteCache: Record<number, TPhotoReqJSON>;
|
||||
}
|
||||
|
||||
const defaultPhotosState: IPhotosState = {
|
||||
const defaultPhotosState: TPhotosState = {
|
||||
photos: [],
|
||||
allPhotosLoaded: false,
|
||||
overviewFetching: false,
|
||||
@@ -45,12 +45,12 @@ const defaultPhotosState: IPhotosState = {
|
||||
deleteCache: {},
|
||||
};
|
||||
|
||||
export function sortPhotos(photos: IPhotoReqJSON[]): IPhotoReqJSON[] {
|
||||
export function sortPhotos(photos: TPhotoReqJSON[]): TPhotoReqJSON[] {
|
||||
return [...photos].sort((a, b) => b.shotAt - a.shotAt);
|
||||
}
|
||||
|
||||
export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
state: IPhotosState = defaultPhotosState,
|
||||
export const photosReducer: Reducer<TPhotosState, PhotoAction> = (
|
||||
state: TPhotosState = defaultPhotosState,
|
||||
action: PhotoAction | UserAction,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
@@ -230,7 +230,7 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
case PhotoTypes.PHOTOS_DELETE_FAIL:
|
||||
case PhotoTypes.PHOTOS_DELETE_CANCEL: {
|
||||
const delCache = { ...state.deleteCache };
|
||||
let photos: IPhotoReqJSON[] = [...state.photos];
|
||||
let photos: TPhotoReqJSON[] = [...state.photos];
|
||||
for (const photo of action.photos) {
|
||||
if (delCache[photo.id]) {
|
||||
photos = sortPhotos([...photos, delCache[photo.id]]);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
cancel,
|
||||
delay,
|
||||
fork,
|
||||
put,
|
||||
race,
|
||||
takeLatest,
|
||||
@@ -18,11 +16,11 @@ import {
|
||||
fetchPhoto,
|
||||
fetchPhotosList,
|
||||
uploadPhoto,
|
||||
} from "../../redux/api/photos";
|
||||
} from "~src/redux/api/photos";
|
||||
import {
|
||||
IPhotosDeleteStartAction,
|
||||
IPhotoLoadStartAction,
|
||||
IPhotosUploadStartAction,
|
||||
TPhotosDeleteStartAction,
|
||||
TPhotoLoadStartAction,
|
||||
TPhotosUploadStartAction,
|
||||
photoCreateFail,
|
||||
photoCreateQueue,
|
||||
photoCreateStart,
|
||||
@@ -35,14 +33,12 @@ import {
|
||||
photosLoadSuccess,
|
||||
photosStartFetchingSpinner,
|
||||
PhotoTypes,
|
||||
photoUploadFail,
|
||||
photoUploadFailWithFile,
|
||||
photoUploadQueue,
|
||||
photoUploadStart,
|
||||
photoUploadSuccess,
|
||||
} from "./actions";
|
||||
import { IPhotosNewRespBody } from "../../../../src/routes/photos";
|
||||
import { IPhotosListPagination } from "../../../../src/types";
|
||||
import { TPhotosNewRespBody, PhotosListPagination } from "~src/shared/types";
|
||||
|
||||
// Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
|
||||
function computeChecksumMd5(file: File): Promise<string> {
|
||||
@@ -109,12 +105,6 @@ function computeSize(f: File) {
|
||||
});
|
||||
}
|
||||
|
||||
// Shouldn't be used anymore
|
||||
function* startSpinner() {
|
||||
yield delay(300);
|
||||
yield put(photosStartFetchingSpinner());
|
||||
}
|
||||
|
||||
function* photosLoad() {
|
||||
const state = yield select();
|
||||
try {
|
||||
@@ -122,7 +112,7 @@ function* photosLoad() {
|
||||
|
||||
const skip = state.photos.photos ? state.photos.photos.length : 0;
|
||||
const { response, timeout } = yield race({
|
||||
response: call(fetchPhotosList, skip, IPhotosListPagination),
|
||||
response: call(fetchPhotosList, skip, PhotosListPagination),
|
||||
timeout: delay(10000),
|
||||
});
|
||||
|
||||
@@ -143,7 +133,7 @@ function* photosLoad() {
|
||||
}
|
||||
}
|
||||
|
||||
function* photoLoad(action: IPhotoLoadStartAction) {
|
||||
function* photoLoad(action: TPhotoLoadStartAction) {
|
||||
try {
|
||||
//const spinner = yield fork(startSpinner);
|
||||
|
||||
@@ -195,7 +185,7 @@ function* photoCreate() {
|
||||
return;
|
||||
}
|
||||
if (response.data || response.error === "Photo already exists") {
|
||||
const photo = (response as IPhotosNewRespBody).data;
|
||||
const photo = (response as TPhotosNewRespBody).data;
|
||||
yield put(photoCreateSuccess(photo, f));
|
||||
yield put(photoUploadQueue(f, photo.id));
|
||||
} else {
|
||||
@@ -243,14 +233,14 @@ function* photoUpload() {
|
||||
}
|
||||
}
|
||||
|
||||
function* photosUpload(action: IPhotosUploadStartAction) {
|
||||
function* photosUpload(action: TPhotosUploadStartAction) {
|
||||
const files = Array.from(action.files);
|
||||
for (const file of files) {
|
||||
yield put(photoCreateQueue(file));
|
||||
}
|
||||
}
|
||||
|
||||
function* photosDelete(action: IPhotosDeleteStartAction) {
|
||||
function* photosDelete(action: TPhotosDeleteStartAction) {
|
||||
try {
|
||||
const { cancelled } = yield race({
|
||||
timeout: delay(3000),
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
ILocalSettingsState,
|
||||
localSettingsReducer,
|
||||
} from "./localSettings/reducer";
|
||||
import { IPhotosState, photosReducer } from "./photos/reducer";
|
||||
import { IUserState, userReducer } from "./user/reducer";
|
||||
import { TPhotosState, photosReducer } from "./photos/reducer";
|
||||
import { TUserState, userReducer } from "./user/reducer";
|
||||
|
||||
export interface IAppState {
|
||||
auth: IAuthState & PersistPartial;
|
||||
user: IUserState;
|
||||
user: TUserState;
|
||||
localSettings: ILocalSettingsState & PersistPartial;
|
||||
photos: IPhotosState;
|
||||
photos: TPhotosState;
|
||||
}
|
||||
|
||||
const authPersistConfig = {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { applyMiddleware, createStore } from "redux";
|
||||
import { composeWithDevTools } from "redux-devtools-extension";
|
||||
import { persistStore } from "redux-persist";
|
||||
import createSagaMiddlware from "redux-saga";
|
||||
import { rootReducer } from "../redux/reducers";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
import { setToken } from "./api/utils";
|
||||
import { authSaga } from "./auth/sagas";
|
||||
import { photosSaga } from "./photos/sagas";
|
||||
import { getUser } from "./user/actions";
|
||||
import { userSaga } from "./user/sagas";
|
||||
import { rootReducer } from "../redux/reducers";
|
||||
|
||||
const sagaMiddleware = createSagaMiddlware();
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
composeWithDevTools(applyMiddleware(sagaMiddleware)),
|
||||
);
|
||||
export const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: [sagaMiddleware],
|
||||
});
|
||||
|
||||
export const persistor = persistStore(store, null, () => {
|
||||
const state = store.getState();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Action } from "redux";
|
||||
import { IUserAuthJSON, IUserJSON } from "../../../../src/entity/User";
|
||||
import { showPasswordNotSavedToast, showPasswordSavedToast } from "../../AppToaster";
|
||||
import { TUserAuthJSON } from "~src/shared/types";
|
||||
import {
|
||||
showPasswordNotSavedToast,
|
||||
showPasswordSavedToast,
|
||||
} from "~src/AppToaster";
|
||||
|
||||
export enum UserTypes {
|
||||
USER_GET = "USER_GET",
|
||||
@@ -12,20 +15,20 @@ export enum UserTypes {
|
||||
USER_PASS_CHANGE_FAIL = "USER_PASS_CHANGE_FAIL",
|
||||
}
|
||||
|
||||
export interface IUserGetAction extends Action {
|
||||
export interface TUserGetAction extends Action {
|
||||
type: UserTypes.USER_GET;
|
||||
}
|
||||
|
||||
export interface IUserLogoutAction extends Action {
|
||||
export interface TUserLogoutAction extends Action {
|
||||
type: UserTypes.USER_LOGOUT;
|
||||
}
|
||||
|
||||
export interface IUserGetSuccessAction extends Action {
|
||||
export interface TUserGetSuccessAction extends Action {
|
||||
type: UserTypes.USER_GET_SUCCESS;
|
||||
payload: IUserAuthJSON;
|
||||
payload: TUserAuthJSON;
|
||||
}
|
||||
|
||||
export interface IUserGetFailAction extends Action {
|
||||
export interface TUserGetFailAction extends Action {
|
||||
type: UserTypes.USER_GET_FAIL;
|
||||
payload: {
|
||||
error: string;
|
||||
@@ -33,17 +36,17 @@ export interface IUserGetFailAction extends Action {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUserPassChangeAction extends Action {
|
||||
export interface TUserPassChangeAction extends Action {
|
||||
type: UserTypes.USER_PASS_CHANGE;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IUserPassChangeSuccessAction extends Action {
|
||||
export interface TUserPassChangeSuccessAction extends Action {
|
||||
type: UserTypes.USER_PASS_CHANGE_SUCCESS;
|
||||
payload: IUserAuthJSON;
|
||||
payload: TUserAuthJSON;
|
||||
}
|
||||
|
||||
export interface IUserPassChangeFailAction extends Action {
|
||||
export interface TUserPassChangeFailAction extends Action {
|
||||
type: UserTypes.USER_PASS_CHANGE_FAIL;
|
||||
payload: {
|
||||
error: string;
|
||||
@@ -51,32 +54,32 @@ export interface IUserPassChangeFailAction extends Action {
|
||||
};
|
||||
}
|
||||
|
||||
export function getUser(): IUserGetAction {
|
||||
export function getUser(): TUserGetAction {
|
||||
return { type: UserTypes.USER_GET };
|
||||
}
|
||||
|
||||
export function logoutUser(): IUserLogoutAction {
|
||||
export function logoutUser(): TUserLogoutAction {
|
||||
return { type: UserTypes.USER_LOGOUT };
|
||||
}
|
||||
|
||||
export function getUserSuccess(user: IUserAuthJSON): IUserGetSuccessAction {
|
||||
export function getUserSuccess(user: TUserAuthJSON): TUserGetSuccessAction {
|
||||
return { type: UserTypes.USER_GET_SUCCESS, payload: user };
|
||||
}
|
||||
|
||||
export function getUserFail(
|
||||
error: string,
|
||||
logout: boolean,
|
||||
): IUserGetFailAction {
|
||||
): TUserGetFailAction {
|
||||
return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } };
|
||||
}
|
||||
|
||||
export function userPassChange(password: string): IUserPassChangeAction {
|
||||
export function userPassChange(password: string): TUserPassChangeAction {
|
||||
return { type: UserTypes.USER_PASS_CHANGE, password };
|
||||
}
|
||||
|
||||
export function userPassChangeSuccess(
|
||||
user: IUserAuthJSON,
|
||||
): IUserPassChangeSuccessAction {
|
||||
user: TUserAuthJSON,
|
||||
): TUserPassChangeSuccessAction {
|
||||
showPasswordSavedToast();
|
||||
return { type: UserTypes.USER_PASS_CHANGE_SUCCESS, payload: user };
|
||||
}
|
||||
@@ -84,7 +87,7 @@ export function userPassChangeSuccess(
|
||||
export function userPassChangeFail(
|
||||
error: string,
|
||||
logout: boolean,
|
||||
): IUserPassChangeFailAction {
|
||||
): TUserPassChangeFailAction {
|
||||
showPasswordNotSavedToast(error);
|
||||
return {
|
||||
type: UserTypes.USER_PASS_CHANGE_FAIL,
|
||||
@@ -93,10 +96,10 @@ export function userPassChangeFail(
|
||||
}
|
||||
|
||||
export type UserAction =
|
||||
| IUserGetAction
|
||||
| IUserGetSuccessAction
|
||||
| IUserGetFailAction
|
||||
| IUserLogoutAction
|
||||
| IUserPassChangeAction
|
||||
| IUserPassChangeFailAction
|
||||
| IUserPassChangeSuccessAction;
|
||||
| TUserGetAction
|
||||
| TUserGetSuccessAction
|
||||
| TUserGetFailAction
|
||||
| TUserLogoutAction
|
||||
| TUserPassChangeAction
|
||||
| TUserPassChangeFailAction
|
||||
| TUserPassChangeSuccessAction;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Reducer } from "react";
|
||||
import { IUserJSON } from "../../../../src/entity/User";
|
||||
import { AuthAction, AuthTypes } from "../../redux/auth/actions";
|
||||
import { TUserJSON } from "~/src/shared/types";
|
||||
import { AuthAction, AuthTypes } from "~src/redux/auth/actions";
|
||||
import { UserAction, UserTypes } from "./actions";
|
||||
|
||||
export interface IUserState {
|
||||
user: IUserJSON | null;
|
||||
export interface TUserState {
|
||||
user: TUserJSON | null;
|
||||
}
|
||||
|
||||
const defaultUserState: IUserState = {
|
||||
const defaultUserState: TUserState = {
|
||||
user: null,
|
||||
};
|
||||
|
||||
export const userReducer: Reducer<IUserState, AuthAction> = (
|
||||
state: IUserState = defaultUserState,
|
||||
export const userReducer: Reducer<TUserState, AuthAction> = (
|
||||
state: TUserState = defaultUserState,
|
||||
action: AuthAction | UserAction,
|
||||
): IUserState => {
|
||||
): TUserState => {
|
||||
switch (action.type) {
|
||||
case AuthTypes.AUTH_SUCCESS:
|
||||
case UserTypes.USER_GET_SUCCESS:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { all, call, delay, put, race, takeLatest } from "redux-saga/effects";
|
||||
import { changeUserPassword, fetchUser } from "../../redux/api/user";
|
||||
import { changeUserPassword, fetchUser } from "~src/redux/api/user";
|
||||
|
||||
import {
|
||||
getUserFail,
|
||||
getUserSuccess,
|
||||
IUserPassChangeAction,
|
||||
TUserPassChangeAction,
|
||||
userPassChangeFail,
|
||||
userPassChangeSuccess,
|
||||
UserTypes,
|
||||
@@ -32,7 +32,7 @@ function* getUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function* userPassChange(action: IUserPassChangeAction) {
|
||||
function* userPassChange(action: TUserPassChangeAction) {
|
||||
try {
|
||||
const { response, timeout } = yield race({
|
||||
response: call(changeUserPassword, action.password),
|
||||
|
||||
1
frontend/src/shared
Symbolic link
1
frontend/src/shared
Symbolic link
@@ -0,0 +1 @@
|
||||
../../shared
|
||||
@@ -15,7 +15,13 @@
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"]
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"type": "mariadb",
|
||||
"host": "dbtest",
|
||||
"port": 3306,
|
||||
"username": "photos",
|
||||
"password": "photos",
|
||||
"database": "photos_test",
|
||||
"synchronize": true,
|
||||
"logging": false,
|
||||
"entities": [
|
||||
"src/entity/**/*.ts"
|
||||
],
|
||||
"migrations": [
|
||||
"src/migration/**/*.ts"
|
||||
],
|
||||
"subscribers": [
|
||||
"src/subscriber/**/*.ts"
|
||||
],
|
||||
"cli": {
|
||||
"entitiesDir": "src/entity",
|
||||
"migrationsDir": "src/migration",
|
||||
"subscribersDir": "src/subscriber"
|
||||
},
|
||||
"charset": "utf8mb4"
|
||||
}
|
||||
10936
package-lock.json
generated
10936
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
104
package.json
104
package.json
@@ -1,93 +1,27 @@
|
||||
{
|
||||
"name": "photos",
|
||||
"version": "0.0.1",
|
||||
"name": "photos-root",
|
||||
"scripts": {
|
||||
"start-frontend": "cd frontend && npm start",
|
||||
"start": "ts-node -T -r tsconfig-paths/register src/server.ts",
|
||||
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts",
|
||||
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:start-frontend -c 'blue,green'",
|
||||
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register --reporter mocha-junit-reporter --reporter-options mochaFile=backend-report.xml 'src/tests/**/*.ts' ",
|
||||
"test-frontend": "cd frontend && npm test",
|
||||
"test-all": "npm test && npm run test-frontend",
|
||||
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
|
||||
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"dev-backend": "cd backend && npm run ts-node-dev",
|
||||
"dev-frontend": "cd frontend && npm run start",
|
||||
"dev-all": "cross-env NODE_ENV=development concurrently npm:dev-backend npm:dev-frontend -c 'blue,green'",
|
||||
"test-backend": "cd backend && npm run test",
|
||||
"test-frontend": "cd frontend && npm run test",
|
||||
"test-all": "npm run test-backend && npm run test-frontend",
|
||||
"lint-backend": "cd backend && npm run lint",
|
||||
"lint-backend-fix": "cd backend && npm run lint-fix",
|
||||
"lint-frontend": "cd frontend && npm run lint",
|
||||
"lint-frontend-fix": "cd frontend && npm run lint-fix",
|
||||
"lint-all": "npm run lint && npm run lint-frontend",
|
||||
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix",
|
||||
"prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check",
|
||||
"prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --write",
|
||||
"typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
|
||||
"typeorm": "cross-env NODE_ENV=production ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@koa/cors": "^3.4.1",
|
||||
"@koa/router": "^12.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"bcrypt": "^5.1.0",
|
||||
"chai": "^4.3.7",
|
||||
"class-validator": "^0.14.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"deasync": "^0.1.28",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"exifreader": "^4.9.1",
|
||||
"hasha": "^5.2.2",
|
||||
"husky": "^8.0.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"koa": "^2.14.1",
|
||||
"koa-body": "^5.0.0",
|
||||
"koa-jwt": "^4.0.4",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-sslify": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mocha": "^10.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"prettier": "^2.8.2",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"sharp": "^0.31.3",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"typeorm": "^0.2.41",
|
||||
"typescript": "^4.9.4"
|
||||
"lint-all": "npm run lint-backend && npm run lint-frontend",
|
||||
"lint-all-fix": "npm run lint-backend-fix && npm run lint-frontend-fix",
|
||||
"prettier-check-backend": "cd backend && npm run prettier-check",
|
||||
"prettify-backend": "cd backend && npm run prettify",
|
||||
"prettier-check-frontend": "cd backend && npm run prettier-check",
|
||||
"prettify-frontend": "cd backend && npm run prettify",
|
||||
"prettier-check-all": "npm run prettier-check-backend && npm run prettier-check-frontend",
|
||||
"prettify-all": "npm run prettify-backend && npm run prettify-frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/concurrently": "^6.4.0",
|
||||
"@types/deasync": "^0.1.2",
|
||||
"@types/eslint": "^8.4.10",
|
||||
"@types/eslint-plugin-prettier": "^3.1.0",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/koa": "^2.13.5",
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^12.0.0",
|
||||
"@types/koa-logger": "^3.1.2",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/koa-sslify": "^4.0.3",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"mocha-junit-reporter": "^2.2.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint-all && npm run prettier-check"
|
||||
}
|
||||
"concurrently": "^8.2.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
15
shared/node_modules/.package-lock.json
generated
vendored
Normal file
15
shared/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "photos-shared",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
shared/node_modules/zod/LICENSE
generated
vendored
Normal file
21
shared/node_modules/zod/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Colin McDonnell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2834
shared/node_modules/zod/README.md
generated
vendored
Normal file
2834
shared/node_modules/zod/README.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
shared/node_modules/zod/index.d.ts
generated
vendored
Normal file
2
shared/node_modules/zod/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./lib";
|
||||
export as namespace Zod;
|
||||
163
shared/node_modules/zod/lib/ZodError.d.ts
generated
vendored
Normal file
163
shared/node_modules/zod/lib/ZodError.d.ts
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { TypeOf, ZodType } from ".";
|
||||
import { Primitive } from "./helpers/typeAliases";
|
||||
import { util, ZodParsedType } from "./helpers/util";
|
||||
declare type allKeys<T> = T extends any ? keyof T : never;
|
||||
export declare type inferFlattenedErrors<T extends ZodType<any, any, any>, U = string> = typeToFlattenedError<TypeOf<T>, U>;
|
||||
export declare type typeToFlattenedError<T, U = string> = {
|
||||
formErrors: U[];
|
||||
fieldErrors: {
|
||||
[P in allKeys<T>]?: U[];
|
||||
};
|
||||
};
|
||||
export declare const ZodIssueCode: {
|
||||
invalid_type: "invalid_type";
|
||||
invalid_literal: "invalid_literal";
|
||||
custom: "custom";
|
||||
invalid_union: "invalid_union";
|
||||
invalid_union_discriminator: "invalid_union_discriminator";
|
||||
invalid_enum_value: "invalid_enum_value";
|
||||
unrecognized_keys: "unrecognized_keys";
|
||||
invalid_arguments: "invalid_arguments";
|
||||
invalid_return_type: "invalid_return_type";
|
||||
invalid_date: "invalid_date";
|
||||
invalid_string: "invalid_string";
|
||||
too_small: "too_small";
|
||||
too_big: "too_big";
|
||||
invalid_intersection_types: "invalid_intersection_types";
|
||||
not_multiple_of: "not_multiple_of";
|
||||
not_finite: "not_finite";
|
||||
};
|
||||
export declare type ZodIssueCode = keyof typeof ZodIssueCode;
|
||||
export declare type ZodIssueBase = {
|
||||
path: (string | number)[];
|
||||
message?: string;
|
||||
};
|
||||
export interface ZodInvalidTypeIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_type;
|
||||
expected: ZodParsedType;
|
||||
received: ZodParsedType;
|
||||
}
|
||||
export interface ZodInvalidLiteralIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_literal;
|
||||
expected: unknown;
|
||||
received: unknown;
|
||||
}
|
||||
export interface ZodUnrecognizedKeysIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.unrecognized_keys;
|
||||
keys: string[];
|
||||
}
|
||||
export interface ZodInvalidUnionIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_union;
|
||||
unionErrors: ZodError[];
|
||||
}
|
||||
export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_union_discriminator;
|
||||
options: Primitive[];
|
||||
}
|
||||
export interface ZodInvalidEnumValueIssue extends ZodIssueBase {
|
||||
received: string | number;
|
||||
code: typeof ZodIssueCode.invalid_enum_value;
|
||||
options: (string | number)[];
|
||||
}
|
||||
export interface ZodInvalidArgumentsIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_arguments;
|
||||
argumentsError: ZodError;
|
||||
}
|
||||
export interface ZodInvalidReturnTypeIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_return_type;
|
||||
returnTypeError: ZodError;
|
||||
}
|
||||
export interface ZodInvalidDateIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_date;
|
||||
}
|
||||
export declare type StringValidation = "email" | "url" | "emoji" | "uuid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "ip" | {
|
||||
includes: string;
|
||||
position?: number;
|
||||
} | {
|
||||
startsWith: string;
|
||||
} | {
|
||||
endsWith: string;
|
||||
};
|
||||
export interface ZodInvalidStringIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_string;
|
||||
validation: StringValidation;
|
||||
}
|
||||
export interface ZodTooSmallIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.too_small;
|
||||
minimum: number | bigint;
|
||||
inclusive: boolean;
|
||||
exact?: boolean;
|
||||
type: "array" | "string" | "number" | "set" | "date" | "bigint";
|
||||
}
|
||||
export interface ZodTooBigIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.too_big;
|
||||
maximum: number | bigint;
|
||||
inclusive: boolean;
|
||||
exact?: boolean;
|
||||
type: "array" | "string" | "number" | "set" | "date" | "bigint";
|
||||
}
|
||||
export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.invalid_intersection_types;
|
||||
}
|
||||
export interface ZodNotMultipleOfIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.not_multiple_of;
|
||||
multipleOf: number | bigint;
|
||||
}
|
||||
export interface ZodNotFiniteIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.not_finite;
|
||||
}
|
||||
export interface ZodCustomIssue extends ZodIssueBase {
|
||||
code: typeof ZodIssueCode.custom;
|
||||
params?: {
|
||||
[k: string]: any;
|
||||
};
|
||||
}
|
||||
export declare type DenormalizedError = {
|
||||
[k: string]: DenormalizedError | string[];
|
||||
};
|
||||
export declare type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue;
|
||||
export declare type ZodIssue = ZodIssueOptionalMessage & {
|
||||
fatal?: boolean;
|
||||
message: string;
|
||||
};
|
||||
export declare const quotelessJson: (obj: any) => string;
|
||||
declare type recursiveZodFormattedError<T> = T extends [any, ...any[]] ? {
|
||||
[K in keyof T]?: ZodFormattedError<T[K]>;
|
||||
} : T extends any[] ? {
|
||||
[k: number]: ZodFormattedError<T[number]>;
|
||||
} : T extends object ? {
|
||||
[K in keyof T]?: ZodFormattedError<T[K]>;
|
||||
} : unknown;
|
||||
export declare type ZodFormattedError<T, U = string> = {
|
||||
_errors: U[];
|
||||
} & recursiveZodFormattedError<NonNullable<T>>;
|
||||
export declare type inferFormattedError<T extends ZodType<any, any, any>, U = string> = ZodFormattedError<TypeOf<T>, U>;
|
||||
export declare class ZodError<T = any> extends Error {
|
||||
issues: ZodIssue[];
|
||||
get errors(): ZodIssue[];
|
||||
constructor(issues: ZodIssue[]);
|
||||
format(): ZodFormattedError<T>;
|
||||
format<U>(mapper: (issue: ZodIssue) => U): ZodFormattedError<T, U>;
|
||||
static create: (issues: ZodIssue[]) => ZodError<any>;
|
||||
toString(): string;
|
||||
get message(): string;
|
||||
get isEmpty(): boolean;
|
||||
addIssue: (sub: ZodIssue) => void;
|
||||
addIssues: (subs?: ZodIssue[]) => void;
|
||||
flatten(): typeToFlattenedError<T>;
|
||||
flatten<U>(mapper?: (issue: ZodIssue) => U): typeToFlattenedError<T, U>;
|
||||
get formErrors(): typeToFlattenedError<T, string>;
|
||||
}
|
||||
declare type stripPath<T extends object> = T extends any ? util.OmitKeys<T, "path"> : never;
|
||||
export declare type IssueData = stripPath<ZodIssueOptionalMessage> & {
|
||||
path?: (string | number)[];
|
||||
fatal?: boolean;
|
||||
};
|
||||
export declare type ErrorMapCtx = {
|
||||
defaultError: string;
|
||||
data: any;
|
||||
};
|
||||
export declare type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => {
|
||||
message: string;
|
||||
};
|
||||
export {};
|
||||
132
shared/node_modules/zod/lib/ZodError.js
generated
vendored
Normal file
132
shared/node_modules/zod/lib/ZodError.js
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ZodError = exports.quotelessJson = exports.ZodIssueCode = void 0;
|
||||
const util_1 = require("./helpers/util");
|
||||
exports.ZodIssueCode = util_1.util.arrayToEnum([
|
||||
"invalid_type",
|
||||
"invalid_literal",
|
||||
"custom",
|
||||
"invalid_union",
|
||||
"invalid_union_discriminator",
|
||||
"invalid_enum_value",
|
||||
"unrecognized_keys",
|
||||
"invalid_arguments",
|
||||
"invalid_return_type",
|
||||
"invalid_date",
|
||||
"invalid_string",
|
||||
"too_small",
|
||||
"too_big",
|
||||
"invalid_intersection_types",
|
||||
"not_multiple_of",
|
||||
"not_finite",
|
||||
]);
|
||||
const quotelessJson = (obj) => {
|
||||
const json = JSON.stringify(obj, null, 2);
|
||||
return json.replace(/"([^"]+)":/g, "$1:");
|
||||
};
|
||||
exports.quotelessJson = quotelessJson;
|
||||
class ZodError extends Error {
|
||||
constructor(issues) {
|
||||
super();
|
||||
this.issues = [];
|
||||
this.addIssue = (sub) => {
|
||||
this.issues = [...this.issues, sub];
|
||||
};
|
||||
this.addIssues = (subs = []) => {
|
||||
this.issues = [...this.issues, ...subs];
|
||||
};
|
||||
const actualProto = new.target.prototype;
|
||||
if (Object.setPrototypeOf) {
|
||||
// eslint-disable-next-line ban/ban
|
||||
Object.setPrototypeOf(this, actualProto);
|
||||
}
|
||||
else {
|
||||
this.__proto__ = actualProto;
|
||||
}
|
||||
this.name = "ZodError";
|
||||
this.issues = issues;
|
||||
}
|
||||
get errors() {
|
||||
return this.issues;
|
||||
}
|
||||
format(_mapper) {
|
||||
const mapper = _mapper ||
|
||||
function (issue) {
|
||||
return issue.message;
|
||||
};
|
||||
const fieldErrors = { _errors: [] };
|
||||
const processError = (error) => {
|
||||
for (const issue of error.issues) {
|
||||
if (issue.code === "invalid_union") {
|
||||
issue.unionErrors.map(processError);
|
||||
}
|
||||
else if (issue.code === "invalid_return_type") {
|
||||
processError(issue.returnTypeError);
|
||||
}
|
||||
else if (issue.code === "invalid_arguments") {
|
||||
processError(issue.argumentsError);
|
||||
}
|
||||
else if (issue.path.length === 0) {
|
||||
fieldErrors._errors.push(mapper(issue));
|
||||
}
|
||||
else {
|
||||
let curr = fieldErrors;
|
||||
let i = 0;
|
||||
while (i < issue.path.length) {
|
||||
const el = issue.path[i];
|
||||
const terminal = i === issue.path.length - 1;
|
||||
if (!terminal) {
|
||||
curr[el] = curr[el] || { _errors: [] };
|
||||
// if (typeof el === "string") {
|
||||
// curr[el] = curr[el] || { _errors: [] };
|
||||
// } else if (typeof el === "number") {
|
||||
// const errorArray: any = [];
|
||||
// errorArray._errors = [];
|
||||
// curr[el] = curr[el] || errorArray;
|
||||
// }
|
||||
}
|
||||
else {
|
||||
curr[el] = curr[el] || { _errors: [] };
|
||||
curr[el]._errors.push(mapper(issue));
|
||||
}
|
||||
curr = curr[el];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
processError(this);
|
||||
return fieldErrors;
|
||||
}
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
get message() {
|
||||
return JSON.stringify(this.issues, util_1.util.jsonStringifyReplacer, 2);
|
||||
}
|
||||
get isEmpty() {
|
||||
return this.issues.length === 0;
|
||||
}
|
||||
flatten(mapper = (issue) => issue.message) {
|
||||
const fieldErrors = {};
|
||||
const formErrors = [];
|
||||
for (const sub of this.issues) {
|
||||
if (sub.path.length > 0) {
|
||||
fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];
|
||||
fieldErrors[sub.path[0]].push(mapper(sub));
|
||||
}
|
||||
else {
|
||||
formErrors.push(mapper(sub));
|
||||
}
|
||||
}
|
||||
return { formErrors, fieldErrors };
|
||||
}
|
||||
get formErrors() {
|
||||
return this.flatten();
|
||||
}
|
||||
}
|
||||
exports.ZodError = ZodError;
|
||||
ZodError.create = (issues) => {
|
||||
const error = new ZodError(issues);
|
||||
return error;
|
||||
};
|
||||
17
shared/node_modules/zod/lib/__tests__/Mocker.d.ts
generated
vendored
Normal file
17
shared/node_modules/zod/lib/__tests__/Mocker.d.ts
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export declare class Mocker {
|
||||
pick: (...args: any[]) => any;
|
||||
get string(): string;
|
||||
get number(): number;
|
||||
get bigint(): bigint;
|
||||
get boolean(): boolean;
|
||||
get date(): Date;
|
||||
get symbol(): symbol;
|
||||
get null(): null;
|
||||
get undefined(): undefined;
|
||||
get stringOptional(): any;
|
||||
get stringNullable(): any;
|
||||
get numberOptional(): any;
|
||||
get numberNullable(): any;
|
||||
get booleanOptional(): any;
|
||||
get booleanNullable(): any;
|
||||
}
|
||||
57
shared/node_modules/zod/lib/__tests__/Mocker.js
generated
vendored
Normal file
57
shared/node_modules/zod/lib/__tests__/Mocker.js
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Mocker = void 0;
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
const testSymbol = Symbol("test");
|
||||
class Mocker {
|
||||
constructor() {
|
||||
this.pick = (...args) => {
|
||||
return args[getRandomInt(args.length)];
|
||||
};
|
||||
}
|
||||
get string() {
|
||||
return Math.random().toString(36).substring(7);
|
||||
}
|
||||
get number() {
|
||||
return Math.random() * 100;
|
||||
}
|
||||
get bigint() {
|
||||
return BigInt(Math.floor(Math.random() * 10000));
|
||||
}
|
||||
get boolean() {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
get date() {
|
||||
return new Date(Math.floor(Date.now() * Math.random()));
|
||||
}
|
||||
get symbol() {
|
||||
return testSymbol;
|
||||
}
|
||||
get null() {
|
||||
return null;
|
||||
}
|
||||
get undefined() {
|
||||
return undefined;
|
||||
}
|
||||
get stringOptional() {
|
||||
return this.pick(this.string, this.undefined);
|
||||
}
|
||||
get stringNullable() {
|
||||
return this.pick(this.string, this.null);
|
||||
}
|
||||
get numberOptional() {
|
||||
return this.pick(this.number, this.undefined);
|
||||
}
|
||||
get numberNullable() {
|
||||
return this.pick(this.number, this.null);
|
||||
}
|
||||
get booleanOptional() {
|
||||
return this.pick(this.boolean, this.undefined);
|
||||
}
|
||||
get booleanNullable() {
|
||||
return this.pick(this.boolean, this.null);
|
||||
}
|
||||
}
|
||||
exports.Mocker = Mocker;
|
||||
5
shared/node_modules/zod/lib/benchmarks/discriminatedUnion.d.ts
generated
vendored
Normal file
5
shared/node_modules/zod/lib/benchmarks/discriminatedUnion.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import Benchmark from "benchmark";
|
||||
declare const _default: {
|
||||
suites: Benchmark.Suite[];
|
||||
};
|
||||
export default _default;
|
||||
79
shared/node_modules/zod/lib/benchmarks/discriminatedUnion.js
generated
vendored
Normal file
79
shared/node_modules/zod/lib/benchmarks/discriminatedUnion.js
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const benchmark_1 = __importDefault(require("benchmark"));
|
||||
const index_1 = require("../index");
|
||||
const doubleSuite = new benchmark_1.default.Suite("z.discriminatedUnion: double");
|
||||
const manySuite = new benchmark_1.default.Suite("z.discriminatedUnion: many");
|
||||
const aSchema = index_1.z.object({
|
||||
type: index_1.z.literal("a"),
|
||||
});
|
||||
const objA = {
|
||||
type: "a",
|
||||
};
|
||||
const bSchema = index_1.z.object({
|
||||
type: index_1.z.literal("b"),
|
||||
});
|
||||
const objB = {
|
||||
type: "b",
|
||||
};
|
||||
const cSchema = index_1.z.object({
|
||||
type: index_1.z.literal("c"),
|
||||
});
|
||||
const objC = {
|
||||
type: "c",
|
||||
};
|
||||
const dSchema = index_1.z.object({
|
||||
type: index_1.z.literal("d"),
|
||||
});
|
||||
const double = index_1.z.discriminatedUnion("type", [aSchema, bSchema]);
|
||||
const many = index_1.z.discriminatedUnion("type", [aSchema, bSchema, cSchema, dSchema]);
|
||||
doubleSuite
|
||||
.add("valid: a", () => {
|
||||
double.parse(objA);
|
||||
})
|
||||
.add("valid: b", () => {
|
||||
double.parse(objB);
|
||||
})
|
||||
.add("invalid: null", () => {
|
||||
try {
|
||||
double.parse(null);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.add("invalid: wrong shape", () => {
|
||||
try {
|
||||
double.parse(objC);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`${doubleSuite.name}: ${e.target}`);
|
||||
});
|
||||
manySuite
|
||||
.add("valid: a", () => {
|
||||
many.parse(objA);
|
||||
})
|
||||
.add("valid: c", () => {
|
||||
many.parse(objC);
|
||||
})
|
||||
.add("invalid: null", () => {
|
||||
try {
|
||||
many.parse(null);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.add("invalid: wrong shape", () => {
|
||||
try {
|
||||
many.parse({ type: "unknown" });
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`${manySuite.name}: ${e.target}`);
|
||||
});
|
||||
exports.default = {
|
||||
suites: [doubleSuite, manySuite],
|
||||
};
|
||||
1
shared/node_modules/zod/lib/benchmarks/index.d.ts
generated
vendored
Normal file
1
shared/node_modules/zod/lib/benchmarks/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
46
shared/node_modules/zod/lib/benchmarks/index.js
generated
vendored
Normal file
46
shared/node_modules/zod/lib/benchmarks/index.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const discriminatedUnion_1 = __importDefault(require("./discriminatedUnion"));
|
||||
const object_1 = __importDefault(require("./object"));
|
||||
const primitives_1 = __importDefault(require("./primitives"));
|
||||
const realworld_1 = __importDefault(require("./realworld"));
|
||||
const string_1 = __importDefault(require("./string"));
|
||||
const union_1 = __importDefault(require("./union"));
|
||||
const argv = process.argv.slice(2);
|
||||
let suites = [];
|
||||
if (!argv.length) {
|
||||
suites = [
|
||||
...realworld_1.default.suites,
|
||||
...primitives_1.default.suites,
|
||||
...string_1.default.suites,
|
||||
...object_1.default.suites,
|
||||
...union_1.default.suites,
|
||||
...discriminatedUnion_1.default.suites,
|
||||
];
|
||||
}
|
||||
else {
|
||||
if (argv.includes("--realworld")) {
|
||||
suites.push(...realworld_1.default.suites);
|
||||
}
|
||||
if (argv.includes("--primitives")) {
|
||||
suites.push(...primitives_1.default.suites);
|
||||
}
|
||||
if (argv.includes("--string")) {
|
||||
suites.push(...string_1.default.suites);
|
||||
}
|
||||
if (argv.includes("--object")) {
|
||||
suites.push(...object_1.default.suites);
|
||||
}
|
||||
if (argv.includes("--union")) {
|
||||
suites.push(...union_1.default.suites);
|
||||
}
|
||||
if (argv.includes("--discriminatedUnion")) {
|
||||
suites.push(...discriminatedUnion_1.default.suites);
|
||||
}
|
||||
}
|
||||
for (const suite of suites) {
|
||||
suite.run();
|
||||
}
|
||||
5
shared/node_modules/zod/lib/benchmarks/object.d.ts
generated
vendored
Normal file
5
shared/node_modules/zod/lib/benchmarks/object.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import Benchmark from "benchmark";
|
||||
declare const _default: {
|
||||
suites: Benchmark.Suite[];
|
||||
};
|
||||
export default _default;
|
||||
70
shared/node_modules/zod/lib/benchmarks/object.js
generated
vendored
Normal file
70
shared/node_modules/zod/lib/benchmarks/object.js
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const benchmark_1 = __importDefault(require("benchmark"));
|
||||
const index_1 = require("../index");
|
||||
const emptySuite = new benchmark_1.default.Suite("z.object: empty");
|
||||
const shortSuite = new benchmark_1.default.Suite("z.object: short");
|
||||
const longSuite = new benchmark_1.default.Suite("z.object: long");
|
||||
const empty = index_1.z.object({});
|
||||
const short = index_1.z.object({
|
||||
string: index_1.z.string(),
|
||||
});
|
||||
const long = index_1.z.object({
|
||||
string: index_1.z.string(),
|
||||
number: index_1.z.number(),
|
||||
boolean: index_1.z.boolean(),
|
||||
});
|
||||
emptySuite
|
||||
.add("valid", () => {
|
||||
empty.parse({});
|
||||
})
|
||||
.add("valid: extra keys", () => {
|
||||
empty.parse({ string: "string" });
|
||||
})
|
||||
.add("invalid: null", () => {
|
||||
try {
|
||||
empty.parse(null);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`${emptySuite.name}: ${e.target}`);
|
||||
});
|
||||
shortSuite
|
||||
.add("valid", () => {
|
||||
short.parse({ string: "string" });
|
||||
})
|
||||
.add("valid: extra keys", () => {
|
||||
short.parse({ string: "string", number: 42 });
|
||||
})
|
||||
.add("invalid: null", () => {
|
||||
try {
|
||||
short.parse(null);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`${shortSuite.name}: ${e.target}`);
|
||||
});
|
||||
longSuite
|
||||
.add("valid", () => {
|
||||
long.parse({ string: "string", number: 42, boolean: true });
|
||||
})
|
||||
.add("valid: extra keys", () => {
|
||||
long.parse({ string: "string", number: 42, boolean: true, list: [] });
|
||||
})
|
||||
.add("invalid: null", () => {
|
||||
try {
|
||||
long.parse(null);
|
||||
}
|
||||
catch (err) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`${longSuite.name}: ${e.target}`);
|
||||
});
|
||||
exports.default = {
|
||||
suites: [emptySuite, shortSuite, longSuite],
|
||||
};
|
||||
5
shared/node_modules/zod/lib/benchmarks/primitives.d.ts
generated
vendored
Normal file
5
shared/node_modules/zod/lib/benchmarks/primitives.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import Benchmark from "benchmark";
|
||||
declare const _default: {
|
||||
suites: Benchmark.Suite[];
|
||||
};
|
||||
export default _default;
|
||||
136
shared/node_modules/zod/lib/benchmarks/primitives.js
generated
vendored
Normal file
136
shared/node_modules/zod/lib/benchmarks/primitives.js
generated
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const benchmark_1 = __importDefault(require("benchmark"));
|
||||
const Mocker_1 = require("../__tests__/Mocker");
|
||||
const index_1 = require("../index");
|
||||
const val = new Mocker_1.Mocker();
|
||||
const enumSuite = new benchmark_1.default.Suite("z.enum");
|
||||
const enumSchema = index_1.z.enum(["a", "b", "c"]);
|
||||
enumSuite
|
||||
.add("valid", () => {
|
||||
enumSchema.parse("a");
|
||||
})
|
||||
.add("invalid", () => {
|
||||
try {
|
||||
enumSchema.parse("x");
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.enum: ${e.target}`);
|
||||
});
|
||||
const undefinedSuite = new benchmark_1.default.Suite("z.undefined");
|
||||
const undefinedSchema = index_1.z.undefined();
|
||||
undefinedSuite
|
||||
.add("valid", () => {
|
||||
undefinedSchema.parse(undefined);
|
||||
})
|
||||
.add("invalid", () => {
|
||||
try {
|
||||
undefinedSchema.parse(1);
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.undefined: ${e.target}`);
|
||||
});
|
||||
const literalSuite = new benchmark_1.default.Suite("z.literal");
|
||||
const short = "short";
|
||||
const bad = "bad";
|
||||
const literalSchema = index_1.z.literal("short");
|
||||
literalSuite
|
||||
.add("valid", () => {
|
||||
literalSchema.parse(short);
|
||||
})
|
||||
.add("invalid", () => {
|
||||
try {
|
||||
literalSchema.parse(bad);
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.literal: ${e.target}`);
|
||||
});
|
||||
const numberSuite = new benchmark_1.default.Suite("z.number");
|
||||
const numberSchema = index_1.z.number().int();
|
||||
numberSuite
|
||||
.add("valid", () => {
|
||||
numberSchema.parse(1);
|
||||
})
|
||||
.add("invalid type", () => {
|
||||
try {
|
||||
numberSchema.parse("bad");
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.add("invalid number", () => {
|
||||
try {
|
||||
numberSchema.parse(0.5);
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.number: ${e.target}`);
|
||||
});
|
||||
const dateSuite = new benchmark_1.default.Suite("z.date");
|
||||
const plainDate = index_1.z.date();
|
||||
const minMaxDate = index_1.z
|
||||
.date()
|
||||
.min(new Date("2021-01-01"))
|
||||
.max(new Date("2030-01-01"));
|
||||
dateSuite
|
||||
.add("valid", () => {
|
||||
plainDate.parse(new Date());
|
||||
})
|
||||
.add("invalid", () => {
|
||||
try {
|
||||
plainDate.parse(1);
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.add("valid min and max", () => {
|
||||
minMaxDate.parse(new Date("2023-01-01"));
|
||||
})
|
||||
.add("invalid min", () => {
|
||||
try {
|
||||
minMaxDate.parse(new Date("2019-01-01"));
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.add("invalid max", () => {
|
||||
try {
|
||||
minMaxDate.parse(new Date("2031-01-01"));
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.date: ${e.target}`);
|
||||
});
|
||||
const symbolSuite = new benchmark_1.default.Suite("z.symbol");
|
||||
const symbolSchema = index_1.z.symbol();
|
||||
symbolSuite
|
||||
.add("valid", () => {
|
||||
symbolSchema.parse(val.symbol);
|
||||
})
|
||||
.add("invalid", () => {
|
||||
try {
|
||||
symbolSchema.parse(1);
|
||||
}
|
||||
catch (e) { }
|
||||
})
|
||||
.on("cycle", (e) => {
|
||||
console.log(`z.symbol: ${e.target}`);
|
||||
});
|
||||
exports.default = {
|
||||
suites: [
|
||||
enumSuite,
|
||||
undefinedSuite,
|
||||
literalSuite,
|
||||
numberSuite,
|
||||
dateSuite,
|
||||
symbolSuite,
|
||||
],
|
||||
};
|
||||
5
shared/node_modules/zod/lib/benchmarks/realworld.d.ts
generated
vendored
Normal file
5
shared/node_modules/zod/lib/benchmarks/realworld.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import Benchmark from "benchmark";
|
||||
declare const _default: {
|
||||
suites: Benchmark.Suite[];
|
||||
};
|
||||
export default _default;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user