some cleanup, split backend and frontend

This commit is contained in:
2023-07-29 13:04:26 +02:00
parent 14210cf0cf
commit faa0aa62c8
69 changed files with 13194 additions and 12319 deletions

View File

@@ -11,7 +11,7 @@ jobs:
MYSQL_PASSWORD: photos MYSQL_PASSWORD: photos
MYSQL_HOST: "localhost" MYSQL_HOST: "localhost"
working_directory: ~/photos working_directory: ~/photos/backend
steps: steps:
- checkout - checkout
@@ -40,7 +40,7 @@ jobs:
docker: docker:
- image: cimg/node:14.20 - image: cimg/node:14.20
working_directory: ~/photos working_directory: ~/photos/frontend
steps: steps:
- checkout - checkout
@@ -50,16 +50,16 @@ jobs:
- run: - run:
name: install frontend deps name: install frontend deps
command: cd frontend && npm i command: npm i
- save_cache: - save_cache:
paths: paths:
- frontend/node_modules - node_modules
key: frontend-dependencies-{{ checksum "frontend/package.json" }} key: frontend-dependencies-{{ checksum "frontend/package.json" }}
- run: - run:
name: test frontend name: test frontend
command: cd frontend && npm test command: npm test
- store_test_results: - store_test_results:
path: ~/photos/frontend/frontend-reports/frontend-report.xml path: ~/photos/frontend/frontend-reports/frontend-report.xml
@@ -68,7 +68,7 @@ jobs:
docker: docker:
- image: cimg/node:14.20 - image: cimg/node:14.20
working_directory: ~/photos working_directory: ~/photos/frontend
steps: steps:
- checkout: - checkout:
@@ -79,16 +79,16 @@ jobs:
- run: - run:
name: install frontend deps name: install frontend deps
command: cd frontend && npm i command: npm i
- save_cache: - save_cache:
paths: paths:
- frontend/node_modules - node_modules
key: frontend-dependencies-{{ checksum "frontend/package.json" }} key: frontend-dependencies-{{ checksum "frontend/package.json" }}
- run: - run:
name: build frontend name: build frontend
command: cd frontend && npm run build command: npm run build
build: build:
machine: machine:
image: ubuntu-2004:current image: ubuntu-2004:current

View File

@@ -4,3 +4,7 @@ npm-debug.log
frontend/node_modules frontend/node_modules
frontend/npm-debug.log frontend/npm-debug.log
frontend/.parcel-cache frontend/.parcel-cache
backend/node_modules
backend/npm-debug.log
backend/.parcel-cache

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
.idea/ .idea/
node_modules/ node_modules/
build/ build/
@@ -13,4 +14,4 @@ ormconfig.test.json
data_test data_test
data_dev data_dev
data data
backend-report.xml backend-report.xml

View File

@@ -3,7 +3,7 @@
"typeorm" "typeorm"
], ],
"eslint.workingDirectories": [ "eslint.workingDirectories": [
".", "./backend",
"./frontend" "./frontend"
], ],
"search.exclude": { "search.exclude": {

View File

@@ -4,8 +4,8 @@ FROM node:16-bullseye as frontbuild
WORKDIR /usr/src/app/frontend WORKDIR /usr/src/app/frontend
COPY ./frontend/package*.json ./ COPY ./frontend/package*.json ./
RUN npm ci --only=production RUN npm ci --only=production
COPY ./frontend . COPY ./frontend/. .
COPY ./src/shared ../src/shared COPY ./shared ../shared
RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")' RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")'
WORKDIR ../ WORKDIR ../
RUN bash -O extglob -c 'rm -rfv !("frontend")' RUN bash -O extglob -c 'rm -rfv !("frontend")'
@@ -13,15 +13,17 @@ RUN bash -O extglob -c 'rm -rfv !("frontend")'
FROM node:16-alpine as backexceptwithoutfrontend FROM node:16-alpine as backexceptwithoutfrontend
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json ./ COPY ./backend/package*.json ./
RUN npm ci --only=production RUN npm ci --only=production
COPY ./ ./ COPY ./backend ./
RUN rm -rfv frontend RUN rm -rfv frontend && unlink src/shared
COPY ./shared src/shared
FROM backexceptwithoutfrontend FROM backexceptwithoutfrontend
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=frontbuild /usr/src/app/frontend ./frontend COPY --from=frontbuild /usr/src/app/frontend ./frontend
COPY ./dockerentry.sh .
#ENV PORT=8080 #ENV PORT=8080
#ENV TYPEORM_HOST=localhost #ENV TYPEORM_HOST=localhost
@@ -45,6 +47,6 @@ ENV DATA_DIR=data\
#EXPOSE 8080 #EXPOSE 8080
RUN ["chmod", "+x", "dockerentry.sh"] RUN ["chmod", "+x", "./dockerentry.sh"]
CMD [ "./dockerentry.sh" ] CMD [ "./dockerentry.sh" ]

17
backend/.gitignore vendored Normal file
View 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

12695
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

86
backend/package.json Executable file
View File

@@ -0,0 +1,86 @@
{
"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": "^4.0.0",
"@koa/router": "^12.0.0",
"bcrypt": "^5.1.0",
"class-validator": "^0.14.0",
"exifreader": "^4.13.0",
"hasha": "^5.2.2",
"io-ts": "^2.2.20",
"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-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/koa": "^2.13.7",
"@types/koa__cors": "^4.0.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.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"
}
}
}

View File

@@ -35,6 +35,7 @@ app.use(
if (config.https) { if (config.https) {
app.use(sslify({ resolver: xForwardedProtoResolver })); app.use(sslify({ resolver: xForwardedProtoResolver }));
} }
app.use( app.use(
jwt({ jwt({
secret: config.jwtSecret, secret: config.jwtSecret,

View File

@@ -1,11 +1,10 @@
import * as path from "path"; import * as path from "path";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import * as mime from "mime-types"; import * as mime from "mime-types";
import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import { IPhotoReqJSON, IPhotoJSON } from "~/shared/types";
import { import {
AfterRemove,
BaseEntity, BaseEntity,
BeforeInsert, BeforeInsert,
BeforeRemove, BeforeRemove,
@@ -18,37 +17,18 @@ import {
} from "typeorm"; } from "typeorm";
import { User } from "./User"; import { User } from "./User";
import { import {
isAlphanumeric,
IsAlphanumeric, IsAlphanumeric,
IsHash, IsHash,
IsIn,
IsMimeType, IsMimeType,
isNumber, isNumber,
Length,
Matches, Matches,
validateOrReject, validateOrReject,
} from "class-validator"; } from "class-validator";
import { config } from "~config"; import { config } from "~config";
import { fileCheck, getShotDate, resizeToJpeg } from "~util"; 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 const thumbSizes = ["512", "1024", "2048", "original"];
export type ThumbSize = typeof thumbSizes[number]; export type ThumbSize = (typeof thumbSizes)[number];
@Entity() @Entity()
@Index(["hash", "size", "user"], { unique: true }) @Index(["hash", "size", "user"], { unique: true })

View File

@@ -2,9 +2,10 @@ import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as path from "path"; import * as path from "path";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { IUserJSON, IUserAuthJSON } from "~/shared/types";
import { import {
AfterInsert, AfterInsert,
AfterRemove,
BaseEntity, BaseEntity,
BeforeInsert, BeforeInsert,
BeforeRemove, BeforeRemove,
@@ -17,25 +18,7 @@ import {
} from "typeorm"; } from "typeorm";
import { config } from "../config"; import { config } from "../config";
import { Photo } from "./Photo"; import { Photo } from "./Photo";
import { import { IsAlphanumeric, IsEmail, validateOrReject } from "class-validator";
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;
}
@Entity() @Entity()
export class User extends BaseEntity { export class User extends BaseEntity {

View File

@@ -1,8 +1,16 @@
import * as Router from "@koa/router"; import * as Router from "@koa/router";
import { IPhotoReqJSON, Photo } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { User } from "~entity/User"; import {
import { IAPIResponse, IPhotosListPagination } from "~/shared/types"; IPhotoReqJSON,
import * as fs from "fs/promises"; IPhotosNewRespBody,
IPhotosNewPostBody,
IPhotoByIDDeleteRespBody,
IPhotosUploadRespBody,
IPhotosListRespBody,
IPhotosByIDGetRespBody,
IPhotosDeleteRespBody,
IPhotosDeleteBody,
IAPIResponse, IPhotosListPagination } from "~/shared/types";
import send = require("koa-send"); import send = require("koa-send");
import { getHash, getSize } from "~util"; import { getHash, getSize } from "~util";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
@@ -12,12 +20,6 @@ import { In } from "typeorm";
export const photosRouter = new Router(); export const photosRouter = new Router();
export interface IPhotosNewPostBody {
hash: string | undefined;
size: string | undefined;
format: string | undefined;
}
export type IPhotosNewRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.post("/photos/new", async (ctx) => { photosRouter.post("/photos/new", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -67,7 +69,6 @@ photosRouter.post("/photos/new", async (ctx) => {
} as IPhotosNewRespBody; } as IPhotosNewRespBody;
}); });
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.post("/photos/upload/:id", async (ctx) => { photosRouter.post("/photos/upload/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -175,7 +176,6 @@ photosRouter.patch("/photos/byID/:id", async (ctx) => {
}); });
*/ */
export type IPhotosListRespBody = IAPIResponse<IPhotoReqJSON[]>;
photosRouter.get("/photos/list", async (ctx) => { photosRouter.get("/photos/list", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -217,7 +217,6 @@ photosRouter.get("/photos/list", async (ctx) => {
} as IPhotosListRespBody; } as IPhotosListRespBody;
}); });
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.get("/photos/byID/:id", async (ctx) => { photosRouter.get("/photos/byID/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -259,7 +258,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
} }
try { try {
jwt.verify(token, config.jwtSecret) as IPhotoReqJSON; jwt.verify(token, config.jwtSecret);
} catch (e) { } catch (e) {
ctx.throw(401); ctx.throw(401);
} }
@@ -353,7 +352,6 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID; ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID;
}); });
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>;
photosRouter.delete("/photos/byID/:id", async (ctx) => { photosRouter.delete("/photos/byID/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -385,11 +383,6 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
} as IPhotoByIDDeleteRespBody; } as IPhotoByIDDeleteRespBody;
}); });
export interface IPhotosDeleteBody {
photos: IPhotoReqJSON[];
}
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
photosRouter.post("/photos/delete", async (ctx) => { photosRouter.post("/photos/delete", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);

View File

@@ -1,11 +1,19 @@
import * as Router from "@koa/router"; import * as Router from "@koa/router";
import { getConfigValue, ConfigKey } from "~entity/Config"; import { getConfigValue, ConfigKey } from "~entity/Config";
import { IUserAuthJSON, IUserJWT, User } from "~entity/User"; import { User } from "~entity/User";
import { IAPIResponse } from "~/shared/types"; import {
IUserJWT,
IUserGetRespBody,
IUserEditRespBody,
IUserSignupBody,
IUserSignupRespBody,
IUserLoginRespBody,
IUserEditBody,
IUserLoginBody,
} from "~/shared/types";
export const userRouter = new Router(); export const userRouter = new Router();
export type IUserGetRespBody = IAPIResponse<IUserAuthJSON>;
userRouter.get("/users/user", async (ctx) => { userRouter.get("/users/user", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
@@ -23,11 +31,6 @@ userRouter.get("/users/user", async (ctx) => {
ctx.body = { error: false, data: user.toAuthJSON() } as IUserGetRespBody; ctx.body = { error: false, data: user.toAuthJSON() } as IUserGetRespBody;
}); });
export interface IUserLoginBody {
username: string | undefined;
password: string | undefined;
}
export type IUserLoginRespBody = IAPIResponse<IUserAuthJSON>;
userRouter.post("/users/login", async (ctx) => { userRouter.post("/users/login", async (ctx) => {
const request = ctx.request; const request = ctx.request;
@@ -50,12 +53,6 @@ userRouter.post("/users/login", async (ctx) => {
ctx.body = { error: false, data: user.toAuthJSON() } as IUserLoginRespBody; ctx.body = { error: false, data: user.toAuthJSON() } as IUserLoginRespBody;
}); });
export interface IUserSignupBody {
username: string | undefined;
password: string | undefined;
email: string | undefined;
}
export type IUserSignupRespBody = IAPIResponse<IUserAuthJSON>;
userRouter.post("/users/signup", async (ctx) => { userRouter.post("/users/signup", async (ctx) => {
const request = ctx.request; const request = ctx.request;
@@ -97,10 +94,6 @@ userRouter.post("/users/signup", async (ctx) => {
ctx.body = { error: false, data: user.toAuthJSON() } as IUserSignupRespBody; ctx.body = { error: false, data: user.toAuthJSON() } as IUserSignupRespBody;
}); });
export interface IUserEditBody {
password: string | undefined;
}
export type IUserEditRespBody = IAPIResponse<IUserAuthJSON>;
userRouter.post("/users/edit", async (ctx) => { userRouter.post("/users/edit", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);

View File

@@ -1,8 +1,8 @@
import { Connection } from "typeorm"; import { Connection } from "typeorm";
import { Config, ConfigKey, setConfigValue } from "~entity/Config"; import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { app } from "./app"; import { app } from "~app";
import { config } from "./config"; import { config } from "./config";
import { connect } from "./config/database"; import { connect } from "~config/database";
async function readConfig() { async function readConfig() {
if (process.env.SIGNUP_ALLOWED) { if (process.env.SIGNUP_ALLOWED) {

1
backend/src/shared Symbolic link
View File

@@ -0,0 +1 @@
../../shared

View File

@@ -3,12 +3,12 @@ import { connect } from "config/database";
import * as request from "supertest"; import * as request from "supertest";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import { app } from "~app"; import { app } from "~app";
import { Photo, IPhotoReqJSON } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { import { IPhotoReqJSON ,
IPhotosDeleteBody, IPhotosDeleteBody,
IPhotosListRespBody, IPhotosListRespBody,
IPhotosNewPostBody, IPhotosNewPostBody,
} from "~routes/photos"; } from "~shared/types";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { constants as fsConstants } from "fs"; import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
@@ -30,7 +30,6 @@ import {
prepareMetadata, prepareMetadata,
seedDB, seedDB,
} from "./util"; } from "./util";
import { sleep } from "deasync";
import { config } from "~config"; import { config } from "~config";
const callback = app.callback(); const callback = app.callback();
@@ -121,7 +120,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const dogSmallThumbSize = ( const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.header["content-length"])).to.equal(
dogSmallThumbSize, dogSmallThumbSize,
@@ -160,7 +159,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const dogSmallThumbSize = ( const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.header["content-length"])).to.equal(
dogSmallThumbSize, dogSmallThumbSize,
@@ -174,7 +173,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const dogSmallThumbSize2 = ( const dogSmallThumbSize2 = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.header["content-length"])).to.equal(
dogSmallThumbSize2, dogSmallThumbSize2,

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -3,7 +3,7 @@ import { connect } from "config/database";
import * as request from "supertest"; import * as request from "supertest";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import { app } from "~app"; import { app } from "~app";
import { IUserAuthJSON, User } from "~entity/User"; import { User } from "~entity/User";
import { import {
IUserEditBody, IUserEditBody,
IUserEditRespBody, IUserEditRespBody,
@@ -12,7 +12,7 @@ import {
IUserLoginRespBody, IUserLoginRespBody,
IUserSignupBody, IUserSignupBody,
IUserSignupRespBody, IUserSignupRespBody,
} from "~routes/users"; } from "~shared/types";
import { allowSignups, ISeed, seedDB } from "./util"; import { allowSignups, ISeed, seedDB } from "./util";

View File

@@ -4,7 +4,6 @@ import { User } from "entity/User";
import { Photo } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { getHash, getSize } from "~util"; import { getHash, getSize } from "~util";
import { Config, ConfigKey, setConfigValue } from "~entity/Config"; import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { config } from "chai";
export const dogPath = "./src/tests/integration/photos/dog.jpg"; export const dogPath = "./src/tests/integration/photos/dog.jpg";
export const catPath = "./src/tests/integration/photos/cat.jpg"; export const catPath = "./src/tests/integration/photos/cat.jpg";

1
dockercomposeexample/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dbdata

View File

@@ -1,7 +1,8 @@
version: "3.8" version: "3.8"
services: services:
photosapp: photosapp:
image: stepanusatiuk/photos:main # image: stepanusatiuk/photos:main
build: ../
restart: always restart: always
ports: ports:
- "8080:8080" - "8080:8080"
@@ -21,4 +22,3 @@ services:
- ./dbdata:/var/lib/mysql - ./dbdata:/var/lib/mysql
env_file: env_file:
- db.env - db.env

5
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"trailingComma": "all",
"tabWidth": 4,
"endOfLine": "auto"
}

View File

@@ -1,7 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
module.exports = { module.exports = {
preset: "ts-jest", preset: "ts-jest",
moduleNameMapper: { moduleNameMapper: {
@@ -11,6 +8,7 @@ module.exports = {
"react-spring/renderprops": "react-spring/renderprops":
"<rootDir>/node_modules/react-spring/renderprops.cjs", "<rootDir>/node_modules/react-spring/renderprops.cjs",
"react-spring": "<rootDir>/node_modules/react-spring/web.cjs", "react-spring": "<rootDir>/node_modules/react-spring/web.cjs",
"~(.*)": "<rootDir>/$1",
}, },
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"], setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
testEnvironment: "jsdom", testEnvironment: "jsdom",

View File

@@ -26,6 +26,7 @@
"eslint-plugin-react": "^7.33.0", "eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"io-ts": "^2.2.20",
"jest": "^29.6.2", "jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2", "jest-environment-jsdom": "^29.6.2",
"jest-junit": "^14.0.1", "jest-junit": "^14.0.1",
@@ -60,7 +61,8 @@
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@types/react-router": "^5.1.20", "@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/spark-md5": "^3.0.2" "@types/spark-md5": "^3.0.2",
"parcel-resolver-ts-base-url": "^1.3.1"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@@ -6353,6 +6355,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fp-ts": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==",
"peer": true
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6937,6 +6945,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/io-ts": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz",
"integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==",
"peerDependencies": {
"fp-ts": "^2.5.0"
}
},
"node_modules/is-arguments": { "node_modules/is-arguments": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -8063,6 +8079,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
"dev": true
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@@ -9031,6 +9053,23 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/parcel-resolver-ts-base-url": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/parcel-resolver-ts-base-url/-/parcel-resolver-ts-base-url-1.3.1.tgz",
"integrity": "sha512-/GfSk1P6oW7uXtViKlqGtH406kQ17Cu+bKHVTdbuIh8t8hICsJrYRqeDDx1dJWsiMAKvtXzXT0TBWLkdusS8PQ==",
"dev": true,
"dependencies": {
"jsonc-parser": "^3.2.0"
},
"engines": {
"node": ">=12",
"parcel": "^2"
},
"peerDependencies": {
"@parcel/plugin": "^2.0.0",
"parcel": "^2.0.0"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -15762,6 +15801,12 @@
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
} }
}, },
"fp-ts": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==",
"peer": true
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -16150,6 +16195,12 @@
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
} }
}, },
"io-ts": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz",
"integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==",
"requires": {}
},
"is-arguments": { "is-arguments": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -16949,6 +17000,12 @@
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
}, },
"jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
"dev": true
},
"jsx-ast-utils": { "jsx-ast-utils": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@@ -17613,6 +17670,15 @@
"get-port": "^4.2.0" "get-port": "^4.2.0"
} }
}, },
"parcel-resolver-ts-base-url": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/parcel-resolver-ts-base-url/-/parcel-resolver-ts-base-url-1.3.1.tgz",
"integrity": "sha512-/GfSk1P6oW7uXtViKlqGtH406kQ17Cu+bKHVTdbuIh8t8hICsJrYRqeDDx1dJWsiMAKvtXzXT0TBWLkdusS8PQ==",
"dev": true,
"requires": {
"jsonc-parser": "^3.2.0"
}
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View File

@@ -3,9 +3,11 @@
"scripts": { "scripts": {
"start": "parcel src/index.html", "start": "parcel src/index.html",
"build": "parcel build src/index.html", "build": "parcel build src/index.html",
"test": "jest",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix", "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": { "dependencies": {
"@blueprintjs/core": "^4.14.1", "@blueprintjs/core": "^4.14.1",
@@ -28,6 +30,7 @@
"eslint-plugin-react": "^7.33.0", "eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"io-ts": "^2.2.20",
"jest": "^29.6.2", "jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2", "jest-environment-jsdom": "^29.6.2",
"jest-junit": "^14.0.1", "jest-junit": "^14.0.1",

View File

@@ -1,6 +1,5 @@
import { Position, Toaster } from "@blueprintjs/core"; import { Position, Toaster } from "@blueprintjs/core";
import { isNumber } from "class-validator"; import { IPhotoReqJSON } from "./shared/types";
import { IPhotoReqJSON } from "../../src/entity/Photo";
export const AppToaster = Toaster.create({ export const AppToaster = Toaster.create({
className: "recipe-toaster", className: "recipe-toaster",

View File

@@ -2,29 +2,24 @@ import "./Home.scss";
import { import {
Alignment, Alignment,
Breadcrumbs,
Button, Button,
Classes, Classes,
IBreadcrumbProps,
Icon,
Menu, Menu,
MenuItem, MenuItem,
Navbar, Navbar,
Popover, Popover,
Spinner,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, RouteComponentProps, Switch, withRouter } from "react-router"; import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
import { animated, config, Transition } from "react-spring/renderprops"; import { animated, config, Transition } from "react-spring/renderprops";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IUserJSON } from "../../../src/entity/User"; import { IUserJSON } from "../shared/types";
import { Account } from "../Account/Account"; import { Account } from "../Account/Account";
import { Overview } from "../Photos/Overview"; import { Overview } from "../Photos/Overview";
import { toggleDarkMode } from "../redux/localSettings/actions"; import { toggleDarkMode } from "../redux/localSettings/actions";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";
import { logoutUser } from "../redux/user/actions"; import { logoutUser } from "../redux/user/actions";
import { Photo } from "../Photos/Photo";
import { PhotoRoute } from "../Photos/PhotoRoute"; import { PhotoRoute } from "../Photos/PhotoRoute";
import { UploadStatus } from "./UploadStatus"; import { UploadStatus } from "./UploadStatus";
@@ -95,28 +90,24 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
transform: "translate3d(400px,0,0)", transform: "translate3d(400px,0,0)",
}} }}
> >
{(_location: any) => (style: any) => {(_location: any) => (style: any) => (
( <animated.div
<animated.div style={style}
style={style} className="viewComponent"
className="viewComponent" >
> <Switch location={_location}>
<Switch location={_location}> <Route
<Route path="/account"
path="/account" component={Account}
component={Account} />
/> <Route
<Route path="/photos/:id"
path="/photos/:id" component={PhotoRoute}
component={PhotoRoute} />
/> <Route path="/" component={Overview} />
<Route </Switch>
path="/" </animated.div>
component={Overview} )}
/>
</Switch>
</animated.div>
)}
</Transition> </Transition>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Popover, Spinner } from "@blueprintjs/core"; import { Button } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";
@@ -12,9 +12,9 @@ export interface IUploadStatusComponentProps {
uploadingQueue: number; uploadingQueue: number;
} }
export const UploadStatusComponent: React.FunctionComponent<IUploadStatusComponentProps> = ( export const UploadStatusComponent: React.FunctionComponent<
props, IUploadStatusComponentProps
) => { > = (props) => {
const { creatingNow, creatingQueue, uploadingNow, uploadingQueue } = props; const { creatingNow, creatingQueue, uploadingNow, uploadingQueue } = props;
const uploading = const uploading =
creatingNow > 0 || creatingNow > 0 ||

View File

@@ -3,20 +3,18 @@ import "./Overview.scss";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IAppState } from "../redux/reducers"; import { IAppState } from "~/src/redux/reducers";
import { import {
photosDeleteCancel, photosDeleteCancel,
photosDeleteStart, photosDeleteStart,
photosLoadStart, photosLoadStart,
} from "../redux/photos/actions"; } from "../redux/photos/actions";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { IPhotoReqJSON } from "~/src/shared/types";
import { LoadingStub } from "../LoadingStub";
import { PhotoCard } from "./PhotoCard"; import { PhotoCard } from "./PhotoCard";
import { import {
Alignment, Alignment,
Button, Button,
Classes, Classes,
H1,
H2, H2,
H3, H3,
Navbar, Navbar,
@@ -25,8 +23,7 @@ import {
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { UploadButton } from "./UploadButton"; import { UploadButton } from "./UploadButton";
import { Photo } from "./Photo"; import { Photo } from "./Photo";
import { getPhotoThumbPath } from "../redux/api/photos"; import { showDeletionToast } from "~/src/AppToaster";
import { showDeletionToast } from "../AppToaster";
export interface IOverviewComponentProps { export interface IOverviewComponentProps {
photos: IPhotoReqJSON[]; photos: IPhotoReqJSON[];

View File

@@ -2,13 +2,9 @@ import "./Photo.scss";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { IPhotoReqJSON } from "~/src/shared/types";
import { LoadingStub } from "../LoadingStub"; import { LoadingStub } from "../LoadingStub";
import { import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
fetchPhoto,
getPhotoImgPath,
getPhotoThumbPath,
} from "../redux/api/photos";
import { photoLoadStart } from "../redux/photos/actions"; import { photoLoadStart } from "../redux/photos/actions";
import { IPhotoState } from "../redux/photos/reducer"; import { IPhotoState } from "../redux/photos/reducer";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";

View File

@@ -8,15 +8,14 @@ import {
Spinner, Spinner,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { IPhotoReqJSON } from "~/src/shared/types";
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos"; import { getPhotoThumbPath } from "../redux/api/photos";
import { showDeletionToast } from "../AppToaster"; import { showDeletionToast } from "../AppToaster";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions"; import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { LoadingStub } from "../LoadingStub";
import { RouteComponentProps, withRouter } from "react-router"; import { RouteComponentProps, withRouter } from "react-router";
import { LargeSize, PreviewSize } from "./helper"; import { PreviewSize } from "./helper";
export interface IPhotoCardComponentProps extends RouteComponentProps { export interface IPhotoCardComponentProps extends RouteComponentProps {
photo: IPhotoReqJSON; photo: IPhotoReqJSON;

View File

@@ -1,7 +1,4 @@
import { import { IUserLoginRespBody, IUserSignupRespBody } from "~/src/shared/types";
IUserLoginRespBody,
IUserSignupRespBody,
} from "../../../../../src/routes/users";
import { fetchJSON } from "../utils"; import { fetchJSON } from "../utils";
export async function login( export async function login(

View File

@@ -1,12 +1,12 @@
import { IPhotoReqJSON } from "../../../../src/entity/Photo"; import { IPhotoReqJSON } from "~/src/shared/types";
import { import {
IPhotosByIDGetRespBody, IPhotosByIDGetRespBody,
IPhotosDeleteRespBody, IPhotosDeleteRespBody,
IPhotosListRespBody, IPhotosListRespBody,
IPhotosNewRespBody, IPhotosNewRespBody,
IPhotosUploadRespBody, IPhotosUploadRespBody,
} from "../../../../src/routes/photos"; } from "~/src/shared/types";
import { apiRoot } from "../../env"; import { apiRoot } from "~src/env";
import { fetchJSONAuth } from "./utils"; import { fetchJSONAuth } from "./utils";
export function getPhotoImgPath(photo: IPhotoReqJSON): string { export function getPhotoImgPath(photo: IPhotoReqJSON): string {

View File

@@ -1,10 +1,11 @@
import { fetchJSONAuth } from "../utils"; import { fetchJSONAuth } from "../utils";
import { IUserEditRespBody, IUserGetRespBody } from "../../../../../src/routes/users"; import { IUserEditRespBody, IUserGetRespBody } from "~/src/shared/types";
export async function fetchUser(): Promise<IUserGetRespBody> { export async function fetchUser(): Promise<IUserGetRespBody> {
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise< return fetchJSONAuth(
IUserGetRespBody "/users/user",
>; "GET",
) as unknown as Promise<IUserGetRespBody>;
} }
export async function changeUserPassword( export async function changeUserPassword(

View File

@@ -1,4 +1,5 @@
import { apiRoot } from "../../env"; import { apiRoot } from "~src/env";
import { IAPIResponse } from "~/src/shared/types";
let token: string | null; let token: string | null;
@@ -14,12 +15,12 @@ export function deleteToken(): void {
token = null; token = null;
} }
export async function fetchJSON( export async function fetchJSON<T>(
path: string, path: string,
method: string, method: string,
body?: string | Record<string, unknown> | File, body?: string | Record<string, unknown> | File,
headers?: Record<string, string>, headers?: Record<string, string>,
): Promise<Record<string, unknown>> { ): Promise<IAPIResponse<T>> {
if (typeof body === "object" && !(body instanceof File)) { if (typeof body === "object" && !(body instanceof File)) {
body = JSON.stringify(body); body = JSON.stringify(body);
headers = { headers = {
@@ -27,7 +28,7 @@ export async function fetchJSON(
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
} }
// TODO: io-ts or something like that
if (body instanceof File) { if (body instanceof File) {
const formData = new FormData(); const formData = new FormData();
formData.append("photo", body); formData.append("photo", body);
@@ -37,7 +38,7 @@ export async function fetchJSON(
body: formData, body: formData,
}); });
const json = (await response.json()) as Record<string, unknown>; const json = (await response.json()) as Record<string, unknown>;
return json; return json as unknown as IAPIResponse<T>;
} }
const response = await fetch(apiRoot + path, { const response = await fetch(apiRoot + path, {
@@ -46,15 +47,15 @@ export async function fetchJSON(
headers, headers,
}); });
const json = (await response.json()) as Record<string, unknown>; const json = (await response.json()) as Record<string, unknown>;
return json; return json as unknown as IAPIResponse<T>;
} }
export async function fetchJSONAuth( export async function fetchJSONAuth<T>(
path: string, path: string,
method: string, method: string,
body?: string | Record<string, unknown> | File, body?: string | Record<string, unknown> | File,
headers?: Record<string, unknown>, headers?: Record<string, unknown>,
): Promise<Record<string, unknown>> { ): Promise<IAPIResponse<T>> {
if (token) { if (token) {
return fetchJSON(path, method, body, { return fetchJSON(path, method, body, {
...headers, ...headers,

View File

@@ -1,5 +1,5 @@
import { Action } from "redux"; import { Action } from "redux";
import { IUserAuthJSON } from "../../../../src/entity/User"; import { IUserAuthJSON } from "~/src/shared/types";
export enum AuthTypes { export enum AuthTypes {
AUTH_START = "AUTH_START", AUTH_START = "AUTH_START",

View File

@@ -1,7 +1,7 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { setToken } from "../../redux/api/utils"; import { setToken } from "~src/redux/api/utils";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { AuthAction, AuthTypes } from "./actions"; import { AuthAction, AuthTypes } from "./actions";
export interface IAuthState { export interface IAuthState {

View File

@@ -8,7 +8,7 @@ import {
race, race,
takeLatest, takeLatest,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { login, signup } from "../../redux/api/auth"; import { login, signup } from "~src/redux/api/auth";
import { import {
authFail, authFail,

View File

@@ -1,5 +1,5 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { LocalSettingsAction, LocalSettingsTypes } from "./actions"; import { LocalSettingsAction, LocalSettingsTypes } from "./actions";

View File

@@ -1,10 +1,10 @@
import { Action } from "redux"; import { Action } from "redux";
import { IPhotoReqJSON, Photo } from "../../../../src/entity/Photo"; import { IPhotoReqJSON } from "~src/shared/types";
import { import {
showPhotoCreateFailToast, showPhotoCreateFailToast,
showPhotoUploadFileFailToast, showPhotoUploadFileFailToast,
showPhotoUploadJSONFailToast, showPhotoUploadJSONFailToast,
} from "../../AppToaster"; } from "~src/AppToaster";
export enum PhotoTypes { export enum PhotoTypes {
PHOTOS_LOAD_START = "PHOTOS_LOAD", PHOTOS_LOAD_START = "PHOTOS_LOAD",

View File

@@ -1,6 +1,6 @@
import { Reducer } from "redux"; import { Reducer } from "redux";
import { IPhotoJSON, IPhotoReqJSON } from "../../../../src/entity/Photo"; import { IPhotoReqJSON } from "~/src/shared/types";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { PhotoAction, PhotoTypes } from "./actions"; import { PhotoAction, PhotoTypes } from "./actions";
export interface IPhotoState { export interface IPhotoState {

View File

@@ -1,9 +1,7 @@
import { import {
all, all,
call, call,
cancel,
delay, delay,
fork,
put, put,
race, race,
takeLatest, takeLatest,
@@ -18,7 +16,7 @@ import {
fetchPhoto, fetchPhoto,
fetchPhotosList, fetchPhotosList,
uploadPhoto, uploadPhoto,
} from "../../redux/api/photos"; } from "~src/redux/api/photos";
import { import {
IPhotosDeleteStartAction, IPhotosDeleteStartAction,
IPhotoLoadStartAction, IPhotoLoadStartAction,
@@ -35,14 +33,12 @@ import {
photosLoadSuccess, photosLoadSuccess,
photosStartFetchingSpinner, photosStartFetchingSpinner,
PhotoTypes, PhotoTypes,
photoUploadFail,
photoUploadFailWithFile, photoUploadFailWithFile,
photoUploadQueue, photoUploadQueue,
photoUploadStart, photoUploadStart,
photoUploadSuccess, photoUploadSuccess,
} from "./actions"; } from "./actions";
import { IPhotosNewRespBody } from "../../../../src/routes/photos"; import { IPhotosNewRespBody, IPhotosListPagination } from "~src/shared/types";
import { IPhotosListPagination } from "../../../../src/shared/types";
// Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4 // Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
function computeChecksumMd5(file: File): Promise<string> { function computeChecksumMd5(file: File): Promise<string> {

View File

@@ -1,6 +1,9 @@
import { Action } from "redux"; import { Action } from "redux";
import { IUserAuthJSON, IUserJSON } from "../../../../src/entity/User"; import { IUserAuthJSON } from "~src/shared/types";
import { showPasswordNotSavedToast, showPasswordSavedToast } from "../../AppToaster"; import {
showPasswordNotSavedToast,
showPasswordSavedToast,
} from "~src/AppToaster";
export enum UserTypes { export enum UserTypes {
USER_GET = "USER_GET", USER_GET = "USER_GET",

View File

@@ -1,6 +1,6 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { IUserJSON } from "../../../../src/entity/User"; import { IUserJSON } from "~/src/entity/User";
import { AuthAction, AuthTypes } from "../../redux/auth/actions"; import { AuthAction, AuthTypes } from "~src/redux/auth/actions";
import { UserAction, UserTypes } from "./actions"; import { UserAction, UserTypes } from "./actions";
export interface IUserState { export interface IUserState {

View File

@@ -1,5 +1,5 @@
import { all, call, delay, put, race, takeLatest } from "redux-saga/effects"; 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 { import {
getUserFail, getUserFail,

1
frontend/src/shared Symbolic link
View File

@@ -0,0 +1 @@
../../shared

View File

@@ -15,7 +15,13 @@
"strictNullChecks": true, "strictNullChecks": true,
"skipLibCheck": true, "skipLibCheck": true,
"isolatedModules": true, "isolatedModules": true,
"downlevelIteration": true "downlevelIteration": true,
"baseUrl": "./",
"paths": {
"~*": [
"./*"
]
}
}, },
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"] "include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"]
} }

12097
package-lock.json generated

File diff suppressed because it is too large Load Diff

102
package.json Executable file → Normal file
View File

@@ -1,93 +1,27 @@
{ {
"name": "photos", "name": "photos-root",
"version": "0.0.1",
"scripts": { "scripts": {
"start-frontend": "cd frontend && npm start", "dev-backend": "cd backend && npm run ts-node-dev",
"start": "ts-node -T -r tsconfig-paths/register src/server.ts", "dev-frontend": "cd frontend && npm run start",
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts", "dev-all": "cross-env NODE_ENV=development concurrently npm:dev-backend npm:dev-frontend -c 'blue,green'",
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:start-frontend -c 'blue,green'", "test-backend": "cd backend && npm run test",
"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' ", "test-frontend": "cd frontend && npm run test",
"test-frontend": "cd frontend && npm test", "test-all": "npm run test-backend && npm run test-frontend",
"test-all": "npm test && npm run test-frontend", "lint-backend": "cd backend && npm run lint",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit", "lint-backend-fix": "cd backend && npm run lint-fix",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
"lint-frontend": "cd frontend && npm run lint", "lint-frontend": "cd frontend && npm run lint",
"lint-frontend-fix": "cd frontend && npm run lint-fix", "lint-frontend-fix": "cd frontend && npm run lint-fix",
"lint-all": "npm run lint && npm run lint-frontend", "lint-all": "npm run lint-backend && npm run lint-frontend",
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix", "lint-all-fix": "npm run lint-backend-fix && npm run lint-frontend-fix",
"prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check", "prettier-check-backend": "cd backend && npm run prettier-check",
"prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --write", "prettify-backend": "cd backend && npm run prettify",
"typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "prettier-check-frontend": "cd backend && npm run prettier-check",
"typeorm": "cross-env NODE_ENV=production ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js" "prettify-frontend": "cd backend && npm run prettify",
}, "prettier-check-all": "npm run prettier-check-backend && npm run prettier-check-frontend",
"license": "MIT", "prettify-all": "npm run prettify-backend && npm run prettify-frontend"
"dependencies": {
"@koa/cors": "^4.0.0",
"@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": { "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-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/koa": "^2.13.7",
"@types/koa__cors": "^4.0.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.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", "concurrently": "^8.2.0",
"cross-env": "^7.0.3", "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"
}
} }
} }

74
shared/types.ts Normal file
View File

@@ -0,0 +1,74 @@
interface IAPIErrorResponse {
data: null;
error: string;
}
interface IAPISuccessResponse<T> {
error: false;
data: T;
}
export type IAPIResponse<T> = IAPIErrorResponse | IAPISuccessResponse<T>;
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 interface IUserJSON {
id: number;
username: string;
isAdmin: boolean;
}
export interface IUserJWT extends IUserJSON {
ext: number;
iat: number;
}
export interface IUserAuthJSON extends IUserJSON {
jwt: string;
}
export interface IPhotosNewPostBody {
hash: string | undefined;
size: string | undefined;
format: string | undefined;
}
export type IPhotosNewRespBody = IAPIResponse<IPhotoReqJSON>;
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>;
export type IPhotosListRespBody = IAPIResponse<IPhotoReqJSON[]>;
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>;
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>;
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
export type IUserGetRespBody = IAPIResponse<IUserAuthJSON>;
export type IUserLoginRespBody = IAPIResponse<IUserAuthJSON>;
export interface IUserSignupBody {
username: string | undefined;
password: string | undefined;
email: string | undefined;
}
export type IUserSignupRespBody = IAPIResponse<IUserAuthJSON>;
export interface IUserEditBody {
password: string | undefined;
}
export type IUserEditRespBody = IAPIResponse<IUserAuthJSON>;
export interface IUserLoginBody {
username: string | undefined;
password: string | undefined;
}
export interface IPhotosDeleteBody {
photos: IPhotoReqJSON[];
}
export const IPhotosListPagination = 50;

View File

@@ -1,14 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface IAPIErrorResponse<T> {
data: null;
error: string;
}
interface IAPISuccessResponse<T> {
error: false;
data: T;
}
export type IAPIResponse<T> = IAPIErrorResponse<T> | IAPISuccessResponse<T>;
export const IPhotosListPagination = 50;