Initial commit

This commit is contained in:
Wroclaw 2023-05-11 06:03:22 +02:00
commit 1e63e008af
48 changed files with 12715 additions and 0 deletions

112
server/api/clients.get.ts Normal file
View file

@ -0,0 +1,112 @@
/* global defineEventHandler getQuery, createError */
import { QueryObject } from "ufo";
import { data, database } from "../utils/database";
import { isString } from "../utils/isString";
import { client } from "~/utils/types/database";
type queryType = {
type: "before" | "after" | "around",
id: string
} | {
type: null
};
function getLocationParameterType(query: QueryObject): queryType {
const before = query.before;
const after = query.after;
const around = query.around;
let setLocationParametersCount = 0;
let rvalue: queryType = { type: null };
if (isString(before)) {
setLocationParametersCount++;
rvalue = { type: "before", id: before };
}
if (isString(after)) {
setLocationParametersCount++;
rvalue = { type: "after", id: after };
}
if (isString(around)) {
setLocationParametersCount++;
rvalue = { type: "around", id: around };
}
if (setLocationParametersCount > 1) {
throw createError({
statusCode: 400,
message: "multiple location parameters not allowed",
});
}
return rvalue;
}
async function getResults(
queryType: queryType,
limit = 50,
) {
switch (queryType.type) {
case "before": {
const [data] = await database.query(
"SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` < ? ORDER BY `id` DESC LIMIT ?",
[queryType.id, limit],
) as unknown as data<client>;
return data;
}
case "after": {
const [data] = await database.query(
"SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` > ? ORDER BY `id` DESC LIMIT ?",
[queryType.id, limit],
) as unknown as data<client>;
return data;
}
case "around": {
const [data] = await database.query(
"(SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` >= ? ORDER BY `id` ASC LIMIT ?)\n" +
"UNION ALL\n" +
"(SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` < ? ORDER BY `id` DESC LIMIT ?)\n" +
"ORDER BY `id` DESC",
[queryType.id, Math.ceil(limit / 2), queryType.id, Math.floor(limit / 2)],
) as unknown as data<client>;
return data;
}
case null: {
const [data] = await database.query(
"SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` ORDER BY `id` DESC LIMIT ?",
[limit],
) as unknown as data<client>;
return data;
}
default:
throw createError("Not implemented");
}
}
export default defineEventHandler(async (e) => {
const query = getQuery(e);
let limit = 50;
if (query.limit) limit = Number(query.limit);
if (limit > 200) {
throw createError({
statusCode: 400,
message: "Cannot retrieve more than 200 records",
});
}
if (limit <= 0) {
throw createError({
statusCode: 400,
message: "Tried to retireve 0 or less records",
});
}
const queryData = getLocationParameterType(query);
const result = await getResults(queryData, limit);
return result;
});

View file

@ -0,0 +1,67 @@
/* global defineEventHandler, createError, readBody */
import { database } from "../utils/database";
import Snowflake from "../utils/snowflake";
import { client } from "~/utils/types/database";
const clientKeys = [
"name",
"address",
"phone",
"email",
];
export function checkIsClient(
value: any,
required = true,
): value is Partial<Omit<client, "id">> {
const errors = new Map<string, string>();
if (typeof value !== "object") {
throw createError({
statusCode: 400,
message: "Invalid body",
});
}
if (!(typeof value.name === "string" || value.name === null || (!required && value.name === undefined))) errors.set("name", "is not string or null");
if (!(typeof value.address === "string" || value.address === null || (!required && value.address === undefined))) errors.set("address", "is not string or null");
if (!(typeof value.phone === "string" || value.phone === null || (!required && value.phone === undefined))) errors.set("phone", "is not string or null");
if (!(typeof value.email === "string" || value.email === null || (!required && value.email === undefined))) errors.set("email", "is not string or null");
for (const i in value)
if (!clientKeys.includes(i)) errors.set(i, `excessive property`);
if (errors.size !== 0) {
let message = "Invalid Parameters: ";
for (const i in errors)
message += i + ", ";
message = message.slice(0, -2);
throw createError({
statusCode: 400,
message,
data: {
errors: Object.fromEntries(errors),
},
});
}
return true;
}
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const id = new Snowflake().toString();
if (!checkIsClient(body)) return; // checkIsClient already throws an detailed error
await database.query(
"INSERT INTO `clients` VALUES (?, ?, ?, ?, ?)",
[id, body.name, body.address, body.phone, body.email],
);
// FIXME: data may be turncated in the database
// either throw an error when data is too large or
// reply with turncated data
return { id, ...body };
});

View file

@ -0,0 +1,17 @@
/* global defineEventHandler, createError */
import { ResultSetHeader } from "mysql2";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const id = e.context.params?.id as string;
const [result] = await database.query(
"DELETE FROM `clients` WHERE `id` = ?",
[id],
) as unknown as [ResultSetHeader];
if (result.affectedRows === 0) throw createError({ statusCode: 404 });
return null;
});

View file

@ -0,0 +1,20 @@
/* global defineEventHandler, createError */
import { database, data } from "~/server/utils/database";
import { client } from "~/utils/types/database";
export default defineEventHandler(async (e) => {
const id = e.context.params?.id;
const [data] = await database.query(
"SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` = ?",
[id],
) as unknown as data<client>;
if (!data[0]) {
throw createError({
statusCode: 404,
});
}
return data[0];
});

View file

@ -0,0 +1,36 @@
/* global defineEventHandler, readBody, createError */
import { ResultSetHeader } from "mysql2";
import { checkIsClient } from "../clients.post";
import { client } from "~/utils/types/database";
import { database, data } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const id = e.context.params?.id as string;
if (!checkIsClient(body, false)) return; // checkIsClient already throws an detailed error
for (const [k, v] of Object.entries(body)) {
const [res] = await database.query(
// I believe it is safe to put key in the template
// because it is limited to 4 values here
`UPDATE \`clients\` SET \`${k}\` = ? WHERE \`id\` = ?`,
[v, id],
) as unknown as [ResultSetHeader];
if (res.affectedRows !== 1) {
throw createError({
statusCode: 404,
});
}
}
const [data] = await database.query(
"SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` = ?",
[id],
) as unknown as data<client>;
return data[0];
});

View file

@ -0,0 +1,12 @@
/* global defineEventHandler, createError */
import { database, data } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const [[data]] = await database.query(
"SELECT COUNT(*) as `count` FROM `clients`",
) as unknown as data<{count: number}>;
if (!data) throw createError("Database returned no rows");
return data;
});

9
server/api/dbtest.get.ts Normal file
View file

@ -0,0 +1,9 @@
/* global defineEventHandler */
import { database } from "../utils/database";
export default defineEventHandler(async () => {
const [owo] = await database.execute("SELECT * FROM `sch_baza_smartfony`.`lombardy`");
return owo as {id: number, nazwa: string, adres: string, kontakt: string}[];
});

26
server/api/dbtest.post.ts Normal file
View file

@ -0,0 +1,26 @@
/* global defineEventHandler, readBody */
import { RowDataPacket } from "mysql2";
import { database } from "../utils/database";
import { isString } from "../utils/isString";
export default defineEventHandler(async (e) => {
const data = await readBody(e);
const nazwa = data.nazwa;
const adres = data.adres;
const kontakt = data.kontakt;
if (!isString(nazwa)) throw new Error("nazwa is not string");
if (!isString(adres)) throw new Error("adres is not string");
if (!isString(kontakt)) throw new Error("kontakt is not string");
const [inserted] = await database.query("INSERT INTO `sch_baza_smartfony`.`lombardy` (`nazwa`, `adres`, `kontakt`) VALUES (?, ?, ?);", [nazwa, adres, kontakt]) as RowDataPacket[];
return {
id: inserted.insertId as number,
nazwa,
adres,
kontakt,
};
});

View file

@ -0,0 +1,9 @@
/* global defineEventHandler */
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
if (!e.context.params?.id) return Error("id is not provided");
const rowID = e.context.params.id;
await database.execute("DELETE FROM `sch_baza_smartfony`.`lombardy` WHERE `id` = ?", [rowID]);
});

6
server/api/echo.post.ts Normal file
View file

@ -0,0 +1,6 @@
/* global defineEventHandler */
export default defineEventHandler((event) => {
const message = event.node.req.read();
return message;
});

5
server/api/hi.ts Normal file
View file

@ -0,0 +1,5 @@
/* global defineEventHandler */
export default defineEventHandler(() => {
return "Hi mom!";
});

40
server/api/login.post.ts Normal file
View file

@ -0,0 +1,40 @@
/* global defineEventHandler, getCookie, setCookie, readBody, createError */
import crypto from "crypto";
import { database, data } from "../utils/database";
import { isString } from "../utils/isString";
import Snowflake from "../utils/snowflake";
import { cookieSettings } from "../utils/rootUtils";
export default defineEventHandler(async (e) => {
if (getCookie(e, "token"))
throw createError({ statusCode: 501, message: "Case not implemented: logging in while cookie is set" });
await new Promise(resolve => setTimeout(resolve, 420));
const data = await readBody(e);
const login = data.login;
const password = data.password;
if (!isString(login)) throw createError({ statusCode: 400, message: "Login is not string." });
if (!isString(password)) throw createError({ statusCode: 400, message: "Password is not string." });
const hashedPassword = crypto.createHmac("sha512", "42")
.update(password)
.digest("hex");
const [account] = await database.query(
"SELECT CONVERT(`id`, CHAR(32)) AS `id` from `users` WHERE `username` = ? AND LOWER(HEX(`password`)) = ? LIMIT 1",
[login, hashedPassword],
)as unknown as data<{id: string}>;
if (account.length === 0) throw createError({ statusCode: 400, message: "Invalid username or password." });
const sessionId = new Snowflake().toString();
await database.query(
"INSERT INTO `sessions` (`id`, `user`) VALUES ( ? , ? )",
[sessionId, account[0].id],
);
setCookie(e, "token", sessionId, cookieSettings);
return { message: "Login successful", token: sessionId };
});

29
server/api/logout.ts Normal file
View file

@ -0,0 +1,29 @@
/* global defineEventHandler, createError, getCookie, deleteCookie */
import { isAuthorised } from "../middleware/auth";
import { database } from "../utils/database";
import { cookieSettings } from "../utils/rootUtils";
export default defineEventHandler(async (e) => {
const token = getCookie(e, "token");
if (token === undefined) {
throw createError({
statusCode: 401,
data: "You can't log out if you're already logged out (no session cookie)",
});
}
deleteCookie(e, "token", cookieSettings);
if (!await isAuthorised(token)) {
throw createError({
statusCode: 401,
message: "You can't log out if you're already logged out (session expired or never existed)",
});
}
database.query(
"DELETE FROM `sessions` WHERE `id` = ?",
[token],
);
return { message: "Logged out" };
});

View file

@ -0,0 +1,14 @@
/* global defineEventHandler, getCookie */
import { database, data } from "~/server/utils/database";
import { user } from "~/utils/types/database";
export default defineEventHandler(async (e) => {
const token = getCookie(e, "token");
const [[userData]] = await database.query(
"SELECT CONVERT(`users`.`id`, CHAR(32)) as `id`, `users`.`username` as `username`, `users`.`email` as `email`, `users`.`display_name` as `display_name` FROM `sessions` LEFT JOIN `users` ON `sessions`.`user` = `users`.`id` WHERE `sessions`.`id` = ?",
[token],
) as unknown as data<user>;
return userData;
});

40
server/middleware/auth.ts Normal file
View file

@ -0,0 +1,40 @@
/* global defineEventHandler, createError, getCookie */
import { database, data } from "~/server/utils/database";
const endpointsWithoutAuth: string[] = [
"/dbtest",
"/echo",
"/hi",
"/login",
"/logout",
];
export default defineEventHandler(async (e) => {
const endpoint = e.path?.match(/^\/api(\/.*)/)?.[1];
// if client does not access api
if (!endpoint) return;
for (const i of endpointsWithoutAuth)
// if accessed endpoint doesn't require auth
if (endpoint.startsWith(i)) return;
const token = getCookie(e, "token");
if (!await isAuthorised(token))
throw createError({ statusCode: 401, message: "Unauthorized" });
});
/**
* Checks if the token is authorised
* @param token the token to ckeck
*/
export async function isAuthorised(token: string | undefined): Promise<boolean> {
if (!token) return false;
const [[session]] = await database.query(
"SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
[token],
) as unknown as data<{logged_in: number}>;
return session.logged_in === 1;
}

View file

@ -0,0 +1,20 @@
type queryType = {
type: "before" | "after" | "around",
id: string
} | {
type: null
};
/**
* Before, around, after pagination wrapper
*/
export default class baaPagination<T> {
readonly table: string;
readonly key: string;
constructor(table: string, key: string) {
this.table = table;
this.key = key;
}
}

11
server/utils/database.ts Normal file
View file

@ -0,0 +1,11 @@
import mysql from "mysql2/promise";
export const database = await mysql.createConnection({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_SCHEMA,
});
export type data<T> = [T[], mysql.FieldPacket[]];

3
server/utils/isString.ts Normal file
View file

@ -0,0 +1,3 @@
export function isString(value: unknown): value is string {
return typeof value === "string";
}

View file

@ -0,0 +1 @@
export * from "../../utils/cookieSettings";

43
server/utils/snowflake.ts Normal file
View file

@ -0,0 +1,43 @@
export default class Snowflake {
static current_increment = 0n;
public static increment() {
this.current_increment = BigInt.asUintN(12, this.current_increment + 1n);
return this.current_increment;
}
state = 0n;
public set_timestamp(value: number | bigint) {
value = BigInt.asUintN(64 - 22, BigInt(value));
const state = BigInt.asUintN(22, this.state);
this.state = state + (value << 22n);
}
public set_machineid(value: number | bigint) {
value = BigInt.asUintN(12 - 17, BigInt(value));
const state = BigInt.asUintN(17, this.state) + (this.state >> 22n) << 22n;
this.state = state + (value << 12n);
}
public set_processid(value: number | bigint) {
value = BigInt.asUintN(17 - 12, BigInt(value));
const state = BigInt.asUintN(12, this.state) + (this.state >> 17n) << 17n;
this.state = state + (value << 12n);
}
public set_increment(value: number | bigint) {
value = BigInt.asUintN(12 - 0, BigInt(value));
const state = (this.state >> 12n) << 12n;
this.state = state + (value << 0n);
}
constructor() {
this.set_timestamp(Date.now());
this.set_processid(1);
this.set_increment(Snowflake.increment());
}
public toString() {
return this.state.toString();
}
}