add crud api for channels
This commit is contained in:
parent
72dbb36dbb
commit
aa320e1b54
8 changed files with 189 additions and 19 deletions
|
@ -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
|
||||||
|
);
|
||||||
`));
|
`));
|
||||||
}
|
}
|
|
@ -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" }
|
||||||
};
|
};
|
||||||
|
|
128
src/routes/api/v1/channels.ts
Normal file
128
src/routes/api/v1/channels.ts
Normal 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;
|
|
@ -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;
|
||||||
|
|
|
@ -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
5
src/types/channel.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
interface Channel {
|
||||||
|
id: number,
|
||||||
|
name: number,
|
||||||
|
owner_id: number
|
||||||
|
}
|
4
src/types/express.d.ts
vendored
4
src/types/express.d.ts
vendored
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
39
test.rest
39
test.rest
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue