add crud api for channels

This commit is contained in:
hippoz 2022-04-06 18:50:36 +03:00
parent 72dbb36dbb
commit aa320e1b54
No known key found for this signature in database
GPG key ID: 7C52899193467641
8 changed files with 189 additions and 19 deletions

View file

@ -2,10 +2,16 @@ import { query } from ".";
export default async function databaseInit() { export default async function databaseInit() {
console.log(await query(` console.log(await query(`
CREATE TABLE users( CREATE TABLE IF NOT EXISTS users(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
username VARCHAR(32) UNIQUE NOT NULL, username VARCHAR(32) UNIQUE NOT NULL,
password TEXT password TEXT
); );
CREATE TABLE IF NOT EXISTS channels(
id SERIAL PRIMARY KEY,
name VARCHAR(32) UNIQUE NOT NULL,
owner_id SERIAL REFERENCES users
);
`)); `));
} }

View file

@ -2,5 +2,7 @@ export const errors = {
INVALID_DATA: { code: 6001, message: "Invalid data" }, INVALID_DATA: { code: 6001, message: "Invalid data" },
BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Bad login credentials provided" }, BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Bad login credentials provided" },
BAD_AUTH: { code: 6003, message: "Bad authentication" }, 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" } GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" }
}; };

View file

@ -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;

View file

@ -5,15 +5,11 @@ import { body, validationResult } from "express-validator";
import { compare, hash } from "bcrypt"; import { compare, hash } from "bcrypt";
import { authenticateRoute, signToken } from "../../../auth"; 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) => { router.post(
res.send({ message: "api/v1/users OK" });
});
route.post(
"/register", "/register",
body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
body("password").isLength({ min: 8, max: 1000 }), body("password").isLength({ min: 8, max: 1000 }),
@ -45,7 +41,7 @@ route.post(
} }
); );
route.post( router.post(
"/login", "/login",
body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
body("password").isLength({ min: 8, max: 1000 }), body("password").isLength({ min: 8, max: 1000 }),
@ -72,7 +68,7 @@ route.post(
} }
); );
route.get( router.get(
"/self", "/self",
authenticateRoute(), authenticateRoute(),
(req, res) => { (req, res) => {
@ -80,4 +76,4 @@ route.get(
} }
); );
export default route; export default router;

View file

@ -1,6 +1,8 @@
import { Application } from "express"; 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) { export default function(app: Application) {
app.use("/api/v1/users", route); app.use("/api/v1/users", usersRouter);
app.use("/api/v1/channels", channelsRouter);
}; };

5
src/types/channel.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
interface Channel {
id: number,
name: number,
owner_id: number
}

View file

@ -1,6 +1,6 @@
declare namespace Express { declare namespace Express {
export interface Request { export interface Request {
user: User | null, user: User,
publicUser: User | null publicUser: User
} }
} }

View file

@ -2,8 +2,8 @@ POST http://localhost:3000/api/v1/users/register HTTP/1.1
content-type: application/json content-type: application/json
{ {
"username": "testuser", "username": "another user",
"password": "123123123" "password": "234234234"
} }
### ###
@ -12,12 +12,43 @@ POST http://localhost:3000/api/v1/users/login HTTP/1.1
content-type: application/json content-type: application/json
{ {
"username": "testuser", "username": "another user",
"password": "123123123" "password": "234234234"
} }
### ###
GET http://localhost:3000/api/v1/users/self HTTP/1.1 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 content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU 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