mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 07:27:47 +01:00
some cleanup, split backend and frontend
This commit is contained in:
@@ -11,7 +11,7 @@ jobs:
|
||||
MYSQL_PASSWORD: photos
|
||||
MYSQL_HOST: "localhost"
|
||||
|
||||
working_directory: ~/photos
|
||||
working_directory: ~/photos/backend
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
docker:
|
||||
- image: cimg/node:14.20
|
||||
|
||||
working_directory: ~/photos
|
||||
working_directory: ~/photos/frontend
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
@@ -50,16 +50,16 @@ jobs:
|
||||
|
||||
- run:
|
||||
name: install frontend deps
|
||||
command: cd frontend && npm i
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- frontend/node_modules
|
||||
- node_modules
|
||||
key: frontend-dependencies-{{ checksum "frontend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: test frontend
|
||||
command: cd frontend && npm test
|
||||
command: npm test
|
||||
|
||||
- store_test_results:
|
||||
path: ~/photos/frontend/frontend-reports/frontend-report.xml
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
docker:
|
||||
- image: cimg/node:14.20
|
||||
|
||||
working_directory: ~/photos
|
||||
working_directory: ~/photos/frontend
|
||||
|
||||
steps:
|
||||
- checkout:
|
||||
@@ -79,16 +79,16 @@ jobs:
|
||||
|
||||
- run:
|
||||
name: install frontend deps
|
||||
command: cd frontend && npm i
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- frontend/node_modules
|
||||
- node_modules
|
||||
key: frontend-dependencies-{{ checksum "frontend/package.json" }}
|
||||
|
||||
- run:
|
||||
name: build frontend
|
||||
command: cd frontend && npm run build
|
||||
command: npm run build
|
||||
build:
|
||||
machine:
|
||||
image: ubuntu-2004:current
|
||||
|
||||
@@ -4,3 +4,7 @@ npm-debug.log
|
||||
frontend/node_modules
|
||||
frontend/npm-debug.log
|
||||
frontend/.parcel-cache
|
||||
|
||||
backend/node_modules
|
||||
backend/npm-debug.log
|
||||
backend/.parcel-cache
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
node_modules/
|
||||
build/
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -3,7 +3,7 @@
|
||||
"typeorm"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
".",
|
||||
"./backend",
|
||||
"./frontend"
|
||||
],
|
||||
"search.exclude": {
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -4,8 +4,8 @@ FROM node:16-bullseye as frontbuild
|
||||
WORKDIR /usr/src/app/frontend
|
||||
COPY ./frontend/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY ./frontend .
|
||||
COPY ./src/shared ../src/shared
|
||||
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")'
|
||||
@@ -13,15 +13,17 @@ RUN bash -O extglob -c 'rm -rfv !("frontend")'
|
||||
FROM node:16-alpine as backexceptwithoutfrontend
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
COPY ./backend/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY ./ ./
|
||||
RUN rm -rfv frontend
|
||||
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
|
||||
@@ -45,6 +47,6 @@ ENV DATA_DIR=data\
|
||||
|
||||
#EXPOSE 8080
|
||||
|
||||
RUN ["chmod", "+x", "dockerentry.sh"]
|
||||
RUN ["chmod", "+x", "./dockerentry.sh"]
|
||||
|
||||
CMD [ "./dockerentry.sh" ]
|
||||
|
||||
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
|
||||
12695
backend/package-lock.json
generated
Normal file
12695
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
86
backend/package.json
Executable file
86
backend/package.json
Executable 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ app.use(
|
||||
if (config.https) {
|
||||
app.use(sslify({ resolver: xForwardedProtoResolver }));
|
||||
}
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
secret: config.jwtSecret,
|
||||
@@ -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 { IPhotoReqJSON, IPhotoJSON } 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 })
|
||||
@@ -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 { IUserJSON, IUserAuthJSON } 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 {
|
||||
@@ -1,8 +1,16 @@
|
||||
import * as Router from "@koa/router";
|
||||
import { IPhotoReqJSON, Photo } from "~entity/Photo";
|
||||
import { User } from "~entity/User";
|
||||
import { IAPIResponse, IPhotosListPagination } from "~/shared/types";
|
||||
import * as fs from "fs/promises";
|
||||
import { Photo } from "~entity/Photo";
|
||||
import {
|
||||
IPhotoReqJSON,
|
||||
IPhotosNewRespBody,
|
||||
IPhotosNewPostBody,
|
||||
IPhotoByIDDeleteRespBody,
|
||||
IPhotosUploadRespBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosByIDGetRespBody,
|
||||
IPhotosDeleteRespBody,
|
||||
IPhotosDeleteBody,
|
||||
IAPIResponse, IPhotosListPagination } from "~/shared/types";
|
||||
import send = require("koa-send");
|
||||
import { getHash, getSize } from "~util";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -12,12 +20,6 @@ import { In } from "typeorm";
|
||||
|
||||
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) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -67,7 +69,6 @@ photosRouter.post("/photos/new", async (ctx) => {
|
||||
} as IPhotosNewRespBody;
|
||||
});
|
||||
|
||||
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.post("/photos/upload/:id", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
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) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -217,7 +217,6 @@ photosRouter.get("/photos/list", async (ctx) => {
|
||||
} as IPhotosListRespBody;
|
||||
});
|
||||
|
||||
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>;
|
||||
photosRouter.get("/photos/byID/:id", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -259,7 +258,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
||||
}
|
||||
|
||||
try {
|
||||
jwt.verify(token, config.jwtSecret) as IPhotoReqJSON;
|
||||
jwt.verify(token, config.jwtSecret);
|
||||
} catch (e) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
@@ -353,7 +352,6 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
|
||||
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID;
|
||||
});
|
||||
|
||||
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>;
|
||||
photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -385,11 +383,6 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
} as IPhotoByIDDeleteRespBody;
|
||||
});
|
||||
|
||||
export interface IPhotosDeleteBody {
|
||||
photos: IPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
|
||||
photosRouter.post("/photos/delete", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -1,11 +1,19 @@
|
||||
import * as Router from "@koa/router";
|
||||
import { getConfigValue, ConfigKey } from "~entity/Config";
|
||||
import { IUserAuthJSON, IUserJWT, User } from "~entity/User";
|
||||
import { IAPIResponse } from "~/shared/types";
|
||||
import { User } from "~entity/User";
|
||||
import {
|
||||
IUserJWT,
|
||||
IUserGetRespBody,
|
||||
IUserEditRespBody,
|
||||
IUserSignupBody,
|
||||
IUserSignupRespBody,
|
||||
IUserLoginRespBody,
|
||||
IUserEditBody,
|
||||
IUserLoginBody,
|
||||
} from "~/shared/types";
|
||||
|
||||
export const userRouter = new Router();
|
||||
|
||||
export type IUserGetRespBody = IAPIResponse<IUserAuthJSON>;
|
||||
userRouter.get("/users/user", async (ctx) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -23,11 +31,6 @@ userRouter.get("/users/user", async (ctx) => {
|
||||
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) => {
|
||||
const request = ctx.request;
|
||||
|
||||
@@ -50,12 +53,6 @@ userRouter.post("/users/login", async (ctx) => {
|
||||
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) => {
|
||||
const request = ctx.request;
|
||||
|
||||
@@ -97,10 +94,6 @@ userRouter.post("/users/signup", async (ctx) => {
|
||||
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) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -1,8 +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) {
|
||||
1
backend/src/shared
Symbolic link
1
backend/src/shared
Symbolic link
@@ -0,0 +1 @@
|
||||
../../shared
|
||||
@@ -3,12 +3,12 @@ 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 {
|
||||
import { Photo } from "~entity/Photo";
|
||||
import { IPhotoReqJSON ,
|
||||
IPhotosDeleteBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewPostBody,
|
||||
} from "~routes/photos";
|
||||
} from "~shared/types";
|
||||
import * as fs from "fs/promises";
|
||||
import { constants as fsConstants } from "fs";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
prepareMetadata,
|
||||
seedDB,
|
||||
} from "./util";
|
||||
import { sleep } from "deasync";
|
||||
import { config } from "~config";
|
||||
|
||||
const callback = app.callback();
|
||||
@@ -121,7 +120,7 @@ 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(
|
||||
dogSmallThumbSize,
|
||||
@@ -160,7 +159,7 @@ 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(
|
||||
dogSmallThumbSize,
|
||||
@@ -174,7 +173,7 @@ describe("photos", function () {
|
||||
})
|
||||
.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(
|
||||
dogSmallThumbSize2,
|
||||
|
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,7 +3,7 @@ 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,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IUserLoginRespBody,
|
||||
IUserSignupBody,
|
||||
IUserSignupRespBody,
|
||||
} from "~routes/users";
|
||||
} from "~shared/types";
|
||||
|
||||
import { allowSignups, ISeed, seedDB } from "./util";
|
||||
|
||||
@@ -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
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
|
||||
|
||||
|
||||
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",
|
||||
|
||||
68
frontend/package-lock.json
generated
68
frontend/package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"eslint-plugin-react": "^7.33.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"flush-promises": "^1.0.2",
|
||||
"io-ts": "^2.2.20",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-junit": "^14.0.1",
|
||||
@@ -60,7 +61,8 @@
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"@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": {
|
||||
@@ -6353,6 +6355,12 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -6937,6 +6945,14 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||
@@ -8063,6 +8079,12 @@
|
||||
"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": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
||||
@@ -9031,6 +9053,23 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -15762,6 +15801,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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -16150,6 +16195,12 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
||||
@@ -17613,6 +17670,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"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",
|
||||
@@ -28,6 +30,7 @@
|
||||
"eslint-plugin-react": "^7.33.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"flush-promises": "^1.0.2",
|
||||
"io-ts": "^2.2.20",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-junit": "^14.0.1",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Position, Toaster } from "@blueprintjs/core";
|
||||
import { isNumber } from "class-validator";
|
||||
import { IPhotoReqJSON } from "../../src/entity/Photo";
|
||||
import { IPhotoReqJSON } from "./shared/types";
|
||||
|
||||
export const AppToaster = Toaster.create({
|
||||
className: "recipe-toaster",
|
||||
|
||||
@@ -2,29 +2,24 @@ 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 { IUserJSON } 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";
|
||||
|
||||
@@ -95,8 +90,7 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||
transform: "translate3d(400px,0,0)",
|
||||
}}
|
||||
>
|
||||
{(_location: any) => (style: any) =>
|
||||
(
|
||||
{(_location: any) => (style: any) => (
|
||||
<animated.div
|
||||
style={style}
|
||||
className="viewComponent"
|
||||
@@ -110,10 +104,7 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||
path="/photos/:id"
|
||||
component={PhotoRoute}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
component={Overview}
|
||||
/>
|
||||
<Route path="/" component={Overview} />
|
||||
</Switch>
|
||||
</animated.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 ||
|
||||
|
||||
@@ -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 { IPhotoReqJSON } from "~/src/shared/types";
|
||||
import { PhotoCard } from "./PhotoCard";
|
||||
import {
|
||||
Alignment,
|
||||
Button,
|
||||
Classes,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
Navbar,
|
||||
@@ -25,8 +23,7 @@ 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[];
|
||||
|
||||
@@ -2,13 +2,9 @@ import "./Photo.scss";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { IPhotoReqJSON } 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 { IAppState } from "../redux/reducers";
|
||||
|
||||
@@ -8,15 +8,14 @@ import {
|
||||
Spinner,
|
||||
} from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { IPhotoReqJSON } 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;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
IUserLoginRespBody,
|
||||
IUserSignupRespBody,
|
||||
} from "../../../../../src/routes/users";
|
||||
import { IUserLoginRespBody, IUserSignupRespBody } from "~/src/shared/types";
|
||||
import { fetchJSON } from "../utils";
|
||||
|
||||
export async function login(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||
import { IPhotoReqJSON } from "~/src/shared/types";
|
||||
import {
|
||||
IPhotosByIDGetRespBody,
|
||||
IPhotosDeleteRespBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewRespBody,
|
||||
IPhotosUploadRespBody,
|
||||
} from "../../../../src/routes/photos";
|
||||
import { apiRoot } from "../../env";
|
||||
} from "~/src/shared/types";
|
||||
import { apiRoot } from "~src/env";
|
||||
import { fetchJSONAuth } from "./utils";
|
||||
|
||||
export function getPhotoImgPath(photo: IPhotoReqJSON): string {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { fetchJSONAuth } from "../utils";
|
||||
import { IUserEditRespBody, IUserGetRespBody } from "../../../../../src/routes/users";
|
||||
import { IUserEditRespBody, IUserGetRespBody } from "~/src/shared/types";
|
||||
|
||||
export async function fetchUser(): Promise<IUserGetRespBody> {
|
||||
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise<
|
||||
IUserGetRespBody
|
||||
>;
|
||||
return fetchJSONAuth(
|
||||
"/users/user",
|
||||
"GET",
|
||||
) as unknown as Promise<IUserGetRespBody>;
|
||||
}
|
||||
|
||||
export async function changeUserPassword(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiRoot } from "../../env";
|
||||
import { apiRoot } from "~src/env";
|
||||
import { IAPIResponse } from "~/src/shared/types";
|
||||
|
||||
let token: string | null;
|
||||
|
||||
@@ -14,12 +15,12 @@ export function deleteToken(): void {
|
||||
token = null;
|
||||
}
|
||||
|
||||
export async function fetchJSON(
|
||||
export async function fetchJSON<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
body?: string | Record<string, unknown> | File,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
): Promise<IAPIResponse<T>> {
|
||||
if (typeof body === "object" && !(body instanceof File)) {
|
||||
body = JSON.stringify(body);
|
||||
headers = {
|
||||
@@ -27,7 +28,7 @@ export async function fetchJSON(
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: io-ts or something like that
|
||||
if (body instanceof File) {
|
||||
const formData = new FormData();
|
||||
formData.append("photo", body);
|
||||
@@ -37,7 +38,7 @@ export async function fetchJSON(
|
||||
body: formData,
|
||||
});
|
||||
const json = (await response.json()) as Record<string, unknown>;
|
||||
return json;
|
||||
return json as unknown as IAPIResponse<T>;
|
||||
}
|
||||
|
||||
const response = await fetch(apiRoot + path, {
|
||||
@@ -46,15 +47,15 @@ export async function fetchJSON(
|
||||
headers,
|
||||
});
|
||||
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,
|
||||
method: string,
|
||||
body?: string | Record<string, unknown> | File,
|
||||
headers?: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
): Promise<IAPIResponse<T>> {
|
||||
if (token) {
|
||||
return fetchJSON(path, method, body, {
|
||||
...headers,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Action } from "redux";
|
||||
import { IUserAuthJSON } from "../../../../src/entity/User";
|
||||
import { IUserAuthJSON } from "~/src/shared/types";
|
||||
|
||||
export enum AuthTypes {
|
||||
AUTH_START = "AUTH_START",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 { IPhotoReqJSON } from "~src/shared/types";
|
||||
import {
|
||||
showPhotoCreateFailToast,
|
||||
showPhotoUploadFileFailToast,
|
||||
showPhotoUploadJSONFailToast,
|
||||
} from "../../AppToaster";
|
||||
} from "~src/AppToaster";
|
||||
|
||||
export enum PhotoTypes {
|
||||
PHOTOS_LOAD_START = "PHOTOS_LOAD",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Reducer } from "redux";
|
||||
import { IPhotoJSON, IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||
import { UserAction, UserTypes } from "../../redux/user/actions";
|
||||
import { IPhotoReqJSON } from "~/src/shared/types";
|
||||
import { UserAction, UserTypes } from "~src/redux/user/actions";
|
||||
import { PhotoAction, PhotoTypes } from "./actions";
|
||||
|
||||
export interface IPhotoState {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
cancel,
|
||||
delay,
|
||||
fork,
|
||||
put,
|
||||
race,
|
||||
takeLatest,
|
||||
@@ -18,7 +16,7 @@ import {
|
||||
fetchPhoto,
|
||||
fetchPhotosList,
|
||||
uploadPhoto,
|
||||
} from "../../redux/api/photos";
|
||||
} from "~src/redux/api/photos";
|
||||
import {
|
||||
IPhotosDeleteStartAction,
|
||||
IPhotoLoadStartAction,
|
||||
@@ -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/shared/types";
|
||||
import { IPhotosNewRespBody, IPhotosListPagination } from "~src/shared/types";
|
||||
|
||||
// Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
|
||||
function computeChecksumMd5(file: File): Promise<string> {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Action } from "redux";
|
||||
import { IUserAuthJSON, IUserJSON } from "../../../../src/entity/User";
|
||||
import { showPasswordNotSavedToast, showPasswordSavedToast } from "../../AppToaster";
|
||||
import { IUserAuthJSON } from "~src/shared/types";
|
||||
import {
|
||||
showPasswordNotSavedToast,
|
||||
showPasswordSavedToast,
|
||||
} from "~src/AppToaster";
|
||||
|
||||
export enum UserTypes {
|
||||
USER_GET = "USER_GET",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Reducer } from "react";
|
||||
import { IUserJSON } from "../../../../src/entity/User";
|
||||
import { AuthAction, AuthTypes } from "../../redux/auth/actions";
|
||||
import { IUserJSON } from "~/src/entity/User";
|
||||
import { AuthAction, AuthTypes } from "~src/redux/auth/actions";
|
||||
import { UserAction, UserTypes } from "./actions";
|
||||
|
||||
export interface IUserState {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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,
|
||||
|
||||
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"]
|
||||
}
|
||||
|
||||
12095
package-lock.json
generated
12095
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
102
package.json
Executable file → Normal file
102
package.json
Executable file → Normal file
@@ -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-multi-reporters --reporter-options configFile=mocha.json '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": "^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"
|
||||
"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.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"
|
||||
}
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
74
shared/types.ts
Normal file
74
shared/types.ts
Normal 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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user