From aa320e1b54a6256adace1f56db114e79f97a2f70 Mon Sep 17 00:00:00 2001 From: hippoz <10706925-hippoz@users.noreply.gitlab.com> Date: Wed, 6 Apr 2022 18:50:36 +0300 Subject: [PATCH] add crud api for channels --- src/database/init.ts | 8 ++- src/errors.ts | 2 + src/routes/api/v1/channels.ts | 128 ++++++++++++++++++++++++++++++++++ src/routes/api/v1/users.ts | 16 ++--- src/server.ts | 6 +- src/types/channel.d.ts | 5 ++ src/types/express.d.ts | 4 +- test.rest | 39 +++++++++-- 8 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 src/routes/api/v1/channels.ts create mode 100644 src/types/channel.d.ts diff --git a/src/database/init.ts b/src/database/init.ts index 6383d69..553b5a5 100644 --- a/src/database/init.ts +++ b/src/database/init.ts @@ -2,10 +2,16 @@ import { query } from "."; export default async function databaseInit() { console.log(await query(` - CREATE TABLE users( + CREATE TABLE IF NOT EXISTS users( id SERIAL PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, password TEXT ); + + CREATE TABLE IF NOT EXISTS channels( + id SERIAL PRIMARY KEY, + name VARCHAR(32) UNIQUE NOT NULL, + owner_id SERIAL REFERENCES users + ); `)); } \ No newline at end of file diff --git a/src/errors.ts b/src/errors.ts index 6519ba5..998b2bd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -2,5 +2,7 @@ export const errors = { INVALID_DATA: { code: 6001, message: "Invalid data" }, BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Bad login credentials provided" }, BAD_AUTH: { code: 6003, message: "Bad authentication" }, + NOT_FOUND: { code: 6004, message: "Not found" }, + FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" }, GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" } }; diff --git a/src/routes/api/v1/channels.ts b/src/routes/api/v1/channels.ts new file mode 100644 index 0000000..80fea6b --- /dev/null +++ b/src/routes/api/v1/channels.ts @@ -0,0 +1,128 @@ +import express from "express"; +import { body, param, validationResult } from "express-validator"; +import { authenticateRoute } from "../../../auth"; +import { query } from "../../../database"; +import { errors } from "../../../errors"; + +const router = express.Router(); + +router.use(express.json()); + +router.post( + "/", + authenticateRoute(), + body("name").isLength({ min: 1, max: 40 }).isAlphanumeric("en-US", { ignore: " _-" }), + async (req, res) => { + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) { + return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); + } + + const { name } = req.body; + const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, req.user.id]); + if (result.rowCount < 1) { + return res.status(500).json({ + ...errors.GOT_NO_DATABASE_DATA + }); + } + + res.status(201).send(result.rows[0]); + } +); + +router.put( + "/:id", + authenticateRoute(), + body("name").isLength({ min: 1, max: 40 }).isAlphanumeric("en-US", { ignore: " _-" }), + param("id").isNumeric(), + async (req, res) => { + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) { + return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); + } + + const { name } = req.body; + const { id } = req.params; + + const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); + if (permissionCheckResult.rowCount < 1) { + return res.status(404).json({ + ...errors.NOT_FOUND + }); + } + if (permissionCheckResult.rows[0].owner_id !== req.user.id) { + return res.status(403).json({ + ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS + }); + } + + const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]); + if (result.rowCount < 1) { + return res.status(500).json({ + ...errors.GOT_NO_DATABASE_DATA + }); + } + + return res.status(200).send({ + id: parseInt(id), // TODO: ?? + name, + owner_id: permissionCheckResult.rows[0].owner_id + }); + } +); + +router.delete( + "/:id", + authenticateRoute(), + param("id").isNumeric(), + async (req, res) => { + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) { + return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); + } + + const { name } = req.body; + const { id } = req.params; + + const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); + if (permissionCheckResult.rowCount < 1) { + return res.status(404).json({ + ...errors.NOT_FOUND + }); + } + if (permissionCheckResult.rows[0].owner_id !== req.user.id) { + return res.status(403).json({ + ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS + }); + } + + const result = await query("DELETE FROM channels WHERE id = $1", [id]); + if (result.rowCount < 1) { + return res.status(500).json({ + ...errors.GOT_NO_DATABASE_DATA + }); + } + + return res.status(204).send(""); + } +); + + +router.get( + "/:id", + authenticateRoute(), + param("id").isNumeric(), + async (req, res) => { + const { id } = req.params; + const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]); + if (result.rowCount < 1) { + return res.status(404).json({ + ...errors.NOT_FOUND + }); + } + + return res.status(200).send(result.rows[0]); + } +); + +export default router; \ No newline at end of file diff --git a/src/routes/api/v1/users.ts b/src/routes/api/v1/users.ts index e5462e5..7cfdc73 100644 --- a/src/routes/api/v1/users.ts +++ b/src/routes/api/v1/users.ts @@ -5,15 +5,11 @@ import { body, validationResult } from "express-validator"; import { compare, hash } from "bcrypt"; import { authenticateRoute, signToken } from "../../../auth"; -const route = express.Router(); +const router = express.Router(); -route.use(express.json()); +router.use(express.json()); -route.get("/", (req, res) => { - res.send({ message: "api/v1/users OK" }); -}); - -route.post( +router.post( "/register", body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("password").isLength({ min: 8, max: 1000 }), @@ -45,7 +41,7 @@ route.post( } ); -route.post( +router.post( "/login", body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("password").isLength({ min: 8, max: 1000 }), @@ -72,7 +68,7 @@ route.post( } ); -route.get( +router.get( "/self", authenticateRoute(), (req, res) => { @@ -80,4 +76,4 @@ route.get( } ); -export default route; +export default router; diff --git a/src/server.ts b/src/server.ts index 3741711..e55cea9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,8 @@ import { Application } from "express"; -import route from "./routes/api/v1/users"; +import usersRouter from "./routes/api/v1/users"; +import channelsRouter from "./routes/api/v1/channels"; export default function(app: Application) { - app.use("/api/v1/users", route); + app.use("/api/v1/users", usersRouter); + app.use("/api/v1/channels", channelsRouter); }; diff --git a/src/types/channel.d.ts b/src/types/channel.d.ts new file mode 100644 index 0000000..73ca86b --- /dev/null +++ b/src/types/channel.d.ts @@ -0,0 +1,5 @@ +interface Channel { + id: number, + name: number, + owner_id: number +} diff --git a/src/types/express.d.ts b/src/types/express.d.ts index bbecf00..c2c5bc6 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,6 +1,6 @@ declare namespace Express { export interface Request { - user: User | null, - publicUser: User | null + user: User, + publicUser: User } } diff --git a/test.rest b/test.rest index 6a6ed84..c10fe23 100644 --- a/test.rest +++ b/test.rest @@ -2,8 +2,8 @@ POST http://localhost:3000/api/v1/users/register HTTP/1.1 content-type: application/json { - "username": "testuser", - "password": "123123123" + "username": "another user", + "password": "234234234" } ### @@ -12,12 +12,43 @@ POST http://localhost:3000/api/v1/users/login HTTP/1.1 content-type: application/json { - "username": "testuser", - "password": "123123123" + "username": "another user", + "password": "234234234" } ### GET http://localhost:3000/api/v1/users/self HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU + +### + +POST http://localhost:3000/api/v1/channels HTTP/1.1 content-type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU + +{ + "name": "my channel" +} + +### + +PUT http://localhost:3000/api/v1/channels/2 HTTP/1.1 +content-type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU +#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY + +{ + "name": "this is my channel" +} + +### + +DELETE http://localhost:3000/api/v1/channels/1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU +#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY + +### + +GET http://localhost:3000/api/v1/channels/1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU