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