backend done
This commit is contained in:
parent
e30649ef14
commit
b82c2dc1d6
|
@ -0,0 +1,9 @@
|
||||||
|
FROM node:22-bullseye
|
||||||
|
|
||||||
|
WORKDIR /opt/backend
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm i
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENTRYPOINT ["npm", "run", "start"]
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"type": "postgres",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"username": "WelBeX",
|
||||||
|
"password": "123",
|
||||||
|
"database": "blog",
|
||||||
|
"synchronize": true,
|
||||||
|
"logging": false,
|
||||||
|
"entities": [
|
||||||
|
"src/entity/*.ts" ],
|
||||||
|
"migrations": [ "src/migration/**/*.ts"
|
||||||
|
],
|
||||||
|
"subscribers": [ "src/subscriber/**/*.ts"
|
||||||
|
],
|
||||||
|
"cli": {
|
||||||
|
"entitiesDir":"src/entity", "migrationsDir":"src/migration", "subscribersDir":"src/subscriber"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "welbex-job-interview",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "task for a job interview",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.foxarmy.org/leca/welbex-job-interview"
|
||||||
|
},
|
||||||
|
"license": "WTFPL",
|
||||||
|
"author": "leca",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "npx tsx --env-file=.env --watch src/index.ts",
|
||||||
|
"typeorm": "typeorm-ts-node-commonjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"bcrypt-ts": "^5.0.3",
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"typeorm": "0.3.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-session": "^1.18.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.8",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
|
"@types/node": "^16.18.125",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
import { AppDataSource } from "../data-source";
|
||||||
|
import { Post } from "../entity/Post";
|
||||||
|
|
||||||
|
class PostController {
|
||||||
|
async create(req: Request, res: Response): Promise<void> {
|
||||||
|
const post = res.locals.post;
|
||||||
|
AppDataSource.manager.save(post);
|
||||||
|
|
||||||
|
res.status(200).send("Ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(req: Request, res: Response): Promise<void> {
|
||||||
|
const { postId } = req.params;
|
||||||
|
|
||||||
|
const post = res.locals.post;
|
||||||
|
AppDataSource.manager.update(Post, { id: postId }, post);
|
||||||
|
|
||||||
|
res.status(200).send("Ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(req: Request, res: Response): Promise<void> {
|
||||||
|
const { postId } = req.params;
|
||||||
|
|
||||||
|
AppDataSource.manager.delete(Post, { id: postId });
|
||||||
|
|
||||||
|
res.status(200).send("Ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PostController();
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { compareSync, genSaltSync, hashSync } from "bcrypt-ts";
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
import { AppDataSource } from '../data-source';
|
||||||
|
import { User } from "../entity/User";
|
||||||
|
|
||||||
|
class UserController {
|
||||||
|
async register(req: Request, res: Response): Promise<void> {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
user.username = username;
|
||||||
|
user.password_hash = hashSync(password, genSaltSync(10));
|
||||||
|
|
||||||
|
const userId = (await AppDataSource.manager.save(user)).id;
|
||||||
|
|
||||||
|
res.cookie("jwt", jwt.sign({ username, id: userId }, process.env.JWT_SECRET));
|
||||||
|
res.status(200).redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(req: Request, res: Response): Promise<void> {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
let savedUser = (await AppDataSource.manager.findOneBy(User, { username }));
|
||||||
|
if (!compareSync(password, savedUser.password_hash)) {
|
||||||
|
res.status(401).send("Incorrect password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.cookie("jwt", jwt.sign({ username, id: savedUser.id }, process.env.JWT_SECRET));
|
||||||
|
res.status(200).redirect("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new UserController();
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'reflect-metadata'
|
||||||
|
import { DataSource } from 'typeorm'
|
||||||
|
import { User } from './entity/User'
|
||||||
|
import { Post } from './entity/Post'
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: "postgres",
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: Number.parseInt(process.env.DB_PORT),
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
synchronize: true,
|
||||||
|
logging: false,
|
||||||
|
entities: [User, Post],
|
||||||
|
migrations: [],
|
||||||
|
subscribers: [],
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm"
|
||||||
|
import { User } from "./User"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Post {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: "integer" })
|
||||||
|
authorId: number
|
||||||
|
|
||||||
|
@ManyToOne((type) => User)
|
||||||
|
@JoinColumn()
|
||||||
|
author: User
|
||||||
|
|
||||||
|
@Column("timestamp")
|
||||||
|
date: string
|
||||||
|
|
||||||
|
@Column("smallint")
|
||||||
|
type: number
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
message: string
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class User {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: "varchar",
|
||||||
|
length: 32,
|
||||||
|
unique: true
|
||||||
|
})
|
||||||
|
username: string
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
password_hash: string
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import express from "express";
|
||||||
|
import { AppDataSource } from "./data-source";
|
||||||
|
import UserRouter from "./routers/user";
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import PostRouter from "./routers/post";
|
||||||
|
import session from "express-session";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json({limit: "200mb"}));
|
||||||
|
app.use(express.urlencoded({ extended: false, limit: "200mb", parameterLimit: 100000 }));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
AppDataSource.initialize().then(() => {
|
||||||
|
app.use('/api/v1/user/', UserRouter);
|
||||||
|
app.use('/api/v1/post/', PostRouter);
|
||||||
|
|
||||||
|
const port = process.env.APP_PORT;
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Listening on port ${port}`)
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { AppDataSource } from "../data-source";
|
||||||
|
import { Post } from "../entity/Post";
|
||||||
|
import { User } from "../entity/User";
|
||||||
|
|
||||||
|
const authenticate = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const token = req.cookies.jwt;
|
||||||
|
if (!token || !jwt.verify(token, process.env.JWT_SECRET)) {
|
||||||
|
res.status(401).send("No valid JWT is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorizeForPost = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const { postId } = req.params;
|
||||||
|
const user = (jwt.decode(req.cookies.jwt) as jwt.JwtPayload);
|
||||||
|
const userId = user.id;
|
||||||
|
const postAuthorId = (await AppDataSource.manager.findOneBy(
|
||||||
|
Post,
|
||||||
|
{ id: Number.parseInt(postId) }
|
||||||
|
)).authorId
|
||||||
|
|
||||||
|
if (userId != postAuthorId) {
|
||||||
|
res.status(403).send("Not your post");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { authenticate, authorizeForPost };
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { AppDataSource } from '../data-source';
|
||||||
|
import { User } from "../entity/User";
|
||||||
|
import { Post } from "../entity/Post";
|
||||||
|
|
||||||
|
const userShouldExist = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const { username } = req.body;
|
||||||
|
|
||||||
|
if (!(await AppDataSource.manager.findOneBy(User, {
|
||||||
|
username
|
||||||
|
}))) {
|
||||||
|
res.status(404).send("User does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userShouldNotExist = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const { username } = req.body;
|
||||||
|
|
||||||
|
if (await AppDataSource.manager.findOneBy(User, {
|
||||||
|
username
|
||||||
|
})) {
|
||||||
|
res.status(409).send("Such user already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const postShouldExist = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const postId = Number.parseInt(req.params.postId);
|
||||||
|
|
||||||
|
if (!(await AppDataSource.manager.findOneBy(Post, {id: postId}))) {
|
||||||
|
res.status(404).send("Post not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { userShouldExist, userShouldNotExist, postShouldExist};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { Post } from "../entity/Post";
|
||||||
|
import path from "path";
|
||||||
|
import fs from 'fs';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import { AppDataSource } from "../data-source";
|
||||||
|
|
||||||
|
// Updates or creates a post and handles things like deleting old post's media
|
||||||
|
const handlePostData = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const token = req.cookies.jwt;
|
||||||
|
const user = (jwt.decode(token) as JwtPayload)
|
||||||
|
|
||||||
|
const post = new Post();
|
||||||
|
|
||||||
|
const { message } = req.body;
|
||||||
|
if (req.method == "PUT") {
|
||||||
|
// Delete old post data if it was media
|
||||||
|
const postToUpdate = (await AppDataSource.manager.findOneBy(Post, { id: Number.parseInt(req.params.postId) }));
|
||||||
|
|
||||||
|
if (postToUpdate.type == 1) {
|
||||||
|
const filename = postToUpdate.message;
|
||||||
|
fs.rmSync(`${process.env.UPLOAD_DESTINATION}/${filename}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (req.file) {
|
||||||
|
const extension = path.extname(req.file.originalname).toLowerCase()
|
||||||
|
if ([".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm"].indexOf(extension) < 0) {
|
||||||
|
res.status(400).send("Unknown mime type");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = fs.readFileSync(req.file.path);
|
||||||
|
const hash = crypto.createHash('md5');
|
||||||
|
hash.update(buffer);
|
||||||
|
const newFilename = `${hash.digest('hex')}${extension}`;
|
||||||
|
fs.renameSync(`./${req.file.path}`, `${process.env.UPLOAD_DESTINATION}/${newFilename}`)
|
||||||
|
post.message = newFilename
|
||||||
|
post.type = 1
|
||||||
|
} else {
|
||||||
|
post.type = 0;
|
||||||
|
post.message = message;
|
||||||
|
}
|
||||||
|
if (req.method == "POST") post.date = new Date().toISOString();
|
||||||
|
post.authorId = user.id;
|
||||||
|
res.locals.post = post;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { handlePostData };
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
import PostController from "../controllers/post";
|
||||||
|
import auth from '../middlewares/auth';
|
||||||
|
import multer from 'multer';
|
||||||
|
import existance from "../middlewares/existance";
|
||||||
|
import utils from "../middlewares/utils";
|
||||||
|
|
||||||
|
const PostRouter = Router();
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: "./temp",
|
||||||
|
limits: {
|
||||||
|
fileSize: 12 * 1024 * 1024
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
PostRouter.post('/create', auth.authenticate, upload.single("file"), utils.handlePostData, PostController.create);
|
||||||
|
PostRouter.put('/update/:postId', auth.authorizeForPost, existance.postShouldExist, upload.single("file"), utils.handlePostData, PostController.update);
|
||||||
|
PostRouter.delete('/delete/:postId', auth.authorizeForPost, existance.postShouldExist, PostController.delete);
|
||||||
|
|
||||||
|
export default PostRouter;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
import UserController from "../controllers/user";
|
||||||
|
import existance from '../middlewares/existance';
|
||||||
|
|
||||||
|
const UserRouter = Router();
|
||||||
|
|
||||||
|
UserRouter.post('/register', existance.userShouldNotExist, UserController.register);
|
||||||
|
UserRouter.post('/login', existance.userShouldExist, UserController.login);
|
||||||
|
|
||||||
|
export default UserRouter;
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"es5",
|
||||||
|
"es6"
|
||||||
|
],
|
||||||
|
"target": "es5",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./build",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: 'postgres:15'
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data/db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASS}
|
||||||
|
POSTGRES_DB: ${DB_NAME}
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U $${DB_USER} -d $${DB_NAME}
|
||||||
|
backend:
|
||||||
|
build: backend
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- 8080:${APP_PORT}
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
restart: on-failure
|
||||||
|
volumes:
|
||||||
|
- ./data/files:${PWD}/${UPLOAD_DESTINATION}
|
|
@ -0,0 +1,9 @@
|
||||||
|
APP_PORT=3000
|
||||||
|
DB_HOST=database
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=WelbeX
|
||||||
|
DB_PASS=123
|
||||||
|
DB_NAME=blog
|
||||||
|
JWT_SECRET=SUP3RS3CR37
|
||||||
|
SESSION_SECRET=SUP3RS3CR37
|
||||||
|
UPLOAD_DESTINATION=./files
|
Loading…
Reference in New Issue