From ac8b5a911e3b7a52a0f181570c0134d79b778088 Mon Sep 17 00:00:00 2001 From: cyclane Date: Wed, 7 Jul 2021 19:47:40 +0100 Subject: [PATCH] API --- src/routes/api/_types.ts | 34 ++++++ src/routes/api/graphs.ts | 29 +++++ src/routes/api/login.ts | 171 ++++++++++++++++++++++++++++ src/routes/api/logout.ts | 36 ++++++ src/routes/api/profiles.ts | 91 +++++++++++++++ src/routes/api/profiles/[player].ts | 21 ++++ src/routes/api/username.ts | 18 +++ src/routes/api/uuid.ts | 18 +++ 8 files changed, 418 insertions(+) create mode 100644 src/routes/api/_types.ts create mode 100644 src/routes/api/graphs.ts create mode 100644 src/routes/api/login.ts create mode 100644 src/routes/api/logout.ts create mode 100644 src/routes/api/profiles.ts create mode 100644 src/routes/api/profiles/[player].ts create mode 100644 src/routes/api/username.ts create mode 100644 src/routes/api/uuid.ts diff --git a/src/routes/api/_types.ts b/src/routes/api/_types.ts new file mode 100644 index 0000000..6cc1240 --- /dev/null +++ b/src/routes/api/_types.ts @@ -0,0 +1,34 @@ +export interface Config { + type: TS; + data: T; +} + +export interface User { + username: string; + password?: string; + permissions: string[]; +} + +export interface Session { + token: string; + exp: number; + type: string; + user: string; +} + +// Configs +export type Tracker = Config< + "tracker", + { + name: string; + members: string[]; + } +>; + +export type Graph = Config< + "graph", + { + name: string; + value: (string | number)[]; + } +>; diff --git a/src/routes/api/graphs.ts b/src/routes/api/graphs.ts new file mode 100644 index 0000000..62208d3 --- /dev/null +++ b/src/routes/api/graphs.ts @@ -0,0 +1,29 @@ +import { aql } from "arangojs"; +import type { Document } from "arangojs/documents"; +import type { IncomingMessage, ServerResponse } from "http"; +import { authorizeRequest } from "../../lib/butil"; +import { Permissions } from "../../lib/permissions"; +import { db } from "../../server"; +import type { Graph } from "./_types"; + +export async function get(req: IncomingMessage, res: ServerResponse) { + const [_, exit] = await authorizeRequest(req, res, db, [Permissions.VIEW]); + if (exit) return; + + let graphs: (Graph & { graph: string })[] = []; + const cursor = await db.query(aql` + FOR doc IN config + FILTER doc.type == "graph" + RETURN doc + `); + while (cursor.hasNext) { + const next: Document = await cursor.next(); + graphs.push({ + graph: next._key, + type: next.type, + data: next.data + }); + } + res.writeHead(200); + res.end(JSON.stringify(graphs)); +} diff --git a/src/routes/api/login.ts b/src/routes/api/login.ts new file mode 100644 index 0000000..f59b8cd --- /dev/null +++ b/src/routes/api/login.ts @@ -0,0 +1,171 @@ +import type { IncomingMessage, ServerResponse } from "http"; +import { parse, serialize } from "cookie"; +import hat from "hat"; +import { hash, compare } from "bcrypt"; +import { + accessTokenExpire, + db, + refreshTokenExpire, + saltRounds +} from "../../server"; +import { aql, Database } from "arangojs"; +import { findSession } from "../../lib/butil"; +import type { Document } from "arangojs/documents"; +import type { User } from "./_types"; + +export async function post(req: IncomingMessage, res: ServerResponse) { + const cookies = parse(req.headers.cookie || ""); + let user: Document; + + // Check for existing refresh session + let session = await findSession(db, cookies.refresh); + // If session exists, refresh it + if (session !== null && session.exp > Date.now()) { + // Check user + try { + user = await db.collection("users").document(session.user); + } catch (e) { + res.writeHead(401); + res.end( + JSON.stringify({ + message: "Non-existent user" + }) + ); + return; + } + + const sessions = db.collection("sessions"); + await sessions.remove(session, { silent: true }); + const { token, refreshToken, tokenExp, refreshTokenExp } = + await newSession(db, session.user); + + res.writeHead(200, { + "Set-Cookie": serialize("refresh", refreshToken, { + expires: new Date(refreshTokenExp), + httpOnly: true, + sameSite: true, + secure: true + }) + }); + res.end( + JSON.stringify({ + token, + username: user.username, + expires: tokenExp + }) + ); + return; + } + + // Check for parameters + let url = new URL(req.url!, "https://example.com/"); + if ( + url.searchParams.get("username") === null || + url.searchParams.get("password") === null + ) { + res.writeHead(400); + res.end( + JSON.stringify({ + message: + "Both username and password query parameters should be set" + }) + ); + return; + } + + // Check user + const cursor = await db.query(aql` + FOR user IN users + FILTER user.username == ${url.searchParams.get("username")} + LIMIT 1 + RETURN user + `); + if (!cursor.hasNext) { + res.writeHead(401); + res.end( + JSON.stringify({ + message: "Non-existent user" + }) + ); + return; + } + user = await cursor.next(); + + // Check password + if (!user.password) { + res.writeHead(401); + res.end( + JSON.stringify({ + message: "Cannot authenticate with this user" + }) + ); + return; + } else if ( + !(await compare(url.searchParams.get("password")!, user.password)) + ) { + res.writeHead(401); + res.end( + JSON.stringify({ + message: "Invalid password" + }) + ); + return; + } + + // Creat new session and return + const { token, refreshToken, tokenExp, refreshTokenExp } = await newSession( + db, + user._key + ); + res.writeHead(200, { + "Set-Cookie": serialize("refresh", refreshToken, { + expires: new Date(refreshTokenExp), + httpOnly: true, + sameSite: true, + secure: true + }) + }); + res.end( + JSON.stringify({ + token, + username: user.username, + expires: tokenExp + }) + ); +} + +async function newSession( + db: Database, + user: string +): Promise<{ + token: string; + tokenExp: number; + refreshToken: string; + refreshTokenExp: number; +}> { + const sessions = db.collection("sessions"); + const token = hat(); + let tokenExp = Date.now() + accessTokenExpire; + const refreshToken = hat(); + let refreshTokenExp = Date.now() + refreshTokenExpire; + await sessions.saveAll([ + { + token: await hash(token, saltRounds), + exp: tokenExp, + type: "access", + user: user + }, + { + token: await hash(refreshToken, saltRounds), + exp: refreshTokenExp, + type: "refresh", + user: user + } + ]); + return { + token, + tokenExp, + refreshToken, + refreshTokenExp + }; +} diff --git a/src/routes/api/logout.ts b/src/routes/api/logout.ts new file mode 100644 index 0000000..24916d5 --- /dev/null +++ b/src/routes/api/logout.ts @@ -0,0 +1,36 @@ +import { parse, serialize } from "cookie"; +import type { IncomingMessage, ServerResponse } from "http"; +import { authorizeRequest, findSession } from "../../lib/butil"; +import { db } from "../../server"; + +export async function post(req: IncomingMessage, res: ServerResponse) { + const [user, exit] = await authorizeRequest(req, res, db, []); + if (exit) return; + + const cookies = parse(req.headers.cookie || ""); + const refresh = await findSession(db, cookies["refresh"] || ""); + + if (!user?._session || !refresh) { + res.writeHead(400); + res.end( + JSON.stringify({ + message: "No session to logout" + }) + ); + return; + } + + const sessions = db.collection("sessions"); + await sessions.remove(refresh!); + await sessions.remove(user!._session!); + + res.writeHead(204, { + "Set-Cookie": serialize("refresh", "deleted", { + expires: new Date(0), + httpOnly: true, + sameSite: true, + secure: true + }) + }); + res.end(); +} diff --git a/src/routes/api/profiles.ts b/src/routes/api/profiles.ts new file mode 100644 index 0000000..0dbb3c3 --- /dev/null +++ b/src/routes/api/profiles.ts @@ -0,0 +1,91 @@ +import { aql } from "arangojs"; +import type { Document } from "arangojs/documents"; +import type { IncomingMessage, ServerResponse } from "http"; +import { getProfileByUUID, getProfilesByPlayer } from "../../lib/hypixel"; +import { Permissions } from "../../lib/permissions"; +import { authorizeRequest } from "../../lib/butil"; +import { db, API_KEY } from "../../server"; +import type { Tracker } from "./_types"; + +export async function get(req: IncomingMessage, res: ServerResponse) { + const [_, exit] = await authorizeRequest(req, res, db, [Permissions.VIEW]); + if (exit) return; + + let profiles: (Tracker & { profile: string })[] = []; + const cursor = await db.query(aql` + FOR doc IN config + FILTER doc.type == "tracker" + RETURN doc + `); + while (cursor.hasNext) { + const next: Document = await cursor.next(); + profiles.push({ + profile: next._key, + type: next.type, + data: next.data + }); + } + res.writeHead(200); + res.end(JSON.stringify(profiles)); +} + +export async function put(req: IncomingMessage, res: ServerResponse) { + const [_, exit] = await authorizeRequest(req, res, db, [ + Permissions.ADD_PROFILE + ]); + if (exit) return; + + const url = new URL(req.url!, "https://example.com/"); + if (!url.searchParams.get("uuid")) { + res.writeHead(400); + res.end( + JSON.stringify({ + message: "uuid query parameter should be set" + }) + ); + return; + } + + const profile = await getProfileByUUID( + API_KEY || "", + url.searchParams.get("uuid")! + ); + if (!profile.profile) { + res.writeHead(profile._response.status); + res.end( + JSON.stringify({ + message: "Hypixel error" + }) + ); + return; + } + const config = db.collection("config"); + let tracker: Tracker & { _key: string } = { + _key: profile.profile!.profile_id, + type: "tracker", + data: { + name: "", + members: Object.keys(profile.profile!.members) + } + }; + const profiles = await getProfilesByPlayer( + API_KEY || "", + url.searchParams.get("member") || tracker.data.members[0] + ); + tracker.data.name = + profiles.profiles?.find( + v => v.profile_id === profile.profile?.profile_id + )?.cute_name || ""; + + if (url.searchParams.get("member")) { + tracker.data.members = [url.searchParams.get("member")!].concat( + tracker.data.members.filter( + m => m !== url.searchParams.get("member") + ) + ); + } + + await config.save(tracker); + res.writeHead(200); + res.end(JSON.stringify(tracker)); +} diff --git a/src/routes/api/profiles/[player].ts b/src/routes/api/profiles/[player].ts new file mode 100644 index 0000000..453249a --- /dev/null +++ b/src/routes/api/profiles/[player].ts @@ -0,0 +1,21 @@ +import type { IncomingMessage, ServerResponse } from "http"; +import { getProfilesByPlayer } from "../../../lib/hypixel"; +import { API_KEY } from "../../../server"; + +export async function get( + req: IncomingMessage & { params: { player: string } }, + res: ServerResponse +) { + let response = await getProfilesByPlayer(API_KEY || "", req.params.player); + if (response.success && response.profiles !== undefined) { + res.writeHead(200); + res.end(JSON.stringify(response.profiles)); + } else { + res.writeHead(response._response.status); + res.end( + JSON.stringify({ + message: "Unsuccessful Hypixel API response" + }) + ); + } +} diff --git a/src/routes/api/username.ts b/src/routes/api/username.ts new file mode 100644 index 0000000..2e9d2fb --- /dev/null +++ b/src/routes/api/username.ts @@ -0,0 +1,18 @@ +import type { IncomingMessage, ServerResponse } from "http"; +import { getUsernameByUUID } from "../../lib/mojang"; + +export async function get(req: IncomingMessage, res: ServerResponse) { + const url = new URL(req.url!, "https://example.com/"); + if (!url.searchParams.get("uuid")) { + res.writeHead(400); + res.end( + JSON.stringify({ + message: "uuid query parameter should be set" + }) + ); + return; + } + let response = await getUsernameByUUID(url.searchParams.get("uuid")!); + res.writeHead(200); + res.end(JSON.stringify(response)); +} diff --git a/src/routes/api/uuid.ts b/src/routes/api/uuid.ts new file mode 100644 index 0000000..7c4ad0c --- /dev/null +++ b/src/routes/api/uuid.ts @@ -0,0 +1,18 @@ +import type { IncomingMessage, ServerResponse } from "http"; +import { getUUIDByUsername } from "../../lib/mojang"; + +export async function get(req: IncomingMessage, res: ServerResponse) { + const url = new URL(req.url!, "https://example.com/"); + if (!url.searchParams.get("username")) { + res.writeHead(400); + res.end( + JSON.stringify({ + message: "username query parameter should be set" + }) + ); + return; + } + let response = await getUUIDByUsername(url.searchParams.get("username")!); + res.writeHead(200); + res.end(JSON.stringify(response)); +}