From 04dba575208485c856a82d4be11b3611432113a4 Mon Sep 17 00:00:00 2001 From: cyclane Date: Wed, 7 Jul 2021 19:47:00 +0100 Subject: [PATCH] lib --- src/lib/api.ts | 121 ++++++++++++++++++++++++++++++++++++++++++++ src/lib/butil.ts | 122 +++++++++++++++++++++++++++++++++++++++++++++ src/lib/hypixel.ts | 8 +++ src/lib/mojang.ts | 24 +++++++++ src/lib/util.ts | 4 +- 5 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/lib/api.ts create mode 100644 src/lib/butil.ts create mode 100644 src/lib/mojang.ts diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..f59a4ca --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,121 @@ +import type { Graph, Tracker } from "../routes/api/_types"; +import type { Profile } from "./hypixel"; +import { requestWithDefaults } from "./util"; + +const request = requestWithDefaults(); + +/** + * Login into SBDataTracker backend + * @param params Login parameters (no parameters = refresh) + * @returns Login details + */ +export async function postLogin(params?: { + username: string; + password: string; +}): Promise<{ + token: string; + username: string; + expires: number; +}> { + let url = "/api/login"; + if (params) { + url += `?username=${encodeURIComponent( + params.username + )}&password=${encodeURIComponent(params.password)}`; + } + let response = await request(url, { method: "POST" }); + return response.json(); +} + +/** + * Logout + * @param token Authorization token + */ +export async function postLogout(token: string) { + await request("/api/logout", { + method: "POST", + headers: { Authorization: token } + }); +} + +/** + * Get a list of profiles + * @param token Authorization token + * @returns A list of profiles + */ +export async function getProfiles( + token?: string +): Promise<(Tracker & { profile: string })[]> { + let response = await request( + "/api/profiles", + token ? { headers: { Authorization: token } } : {} + ); + return response.json(); +} + +/** + * Get a list of skyblock profiles by player + * @param player Player's UUID + * @returns A list of profiles + */ +export async function getPlayerProfiles(player: string): Promise { + let response = await request(`/api/profiles/${player}`); + return response.json(); +} + +/** + * Get a list of graphs + * @param token Authorization token + * @returns A list of profiles + */ +export async function getGraphs( + token?: string +): Promise<(Graph & { graph: string })[]> { + let response = await request( + "/api/graphs", + token ? { headers: { Authorization: token } } : {} + ); + return response.json(); +} + +/** + * Get a player username by UUID + * @param uuid Player's UUID + * @returns Player's username + */ +export async function getUsername(uuid: string): Promise { + let response = await request(`/api/username?uuid=${uuid}`); + return response.json(); +} + +/** + * Get a player UUID by username + * @param username Player's username + * @returns Player's UUID + */ +export async function getUUID(username: string): Promise { + let response = await request(`/api/uuid?username=${username}`); + return response.json(); +} + +/** + * Create a profile + * @param token Authorization token + * @param uuid Profile UUID + * @param member Member to assign the profile to + * @returns Tracker information + */ +export async function putProfiles( + token: string | undefined, + uuid: string, + member?: string +): Promise { + let response = await request( + `/api/profiles?uuid=${uuid}` + (member ? `&member=${member}` : ""), + { + method: "PUT", + ...(token ? { headers: { Authorization: token } } : {}) + } + ); + return response.json(); +} diff --git a/src/lib/butil.ts b/src/lib/butil.ts new file mode 100644 index 0000000..6b0c3ff --- /dev/null +++ b/src/lib/butil.ts @@ -0,0 +1,122 @@ +import { aql, Database } from "arangojs"; +import type { Document } from "arangojs/documents"; +import { compare } from "bcrypt"; +import type { IncomingMessage, ServerResponse } from "http"; +import type { Session, User } from "../routes/api/_types"; +import { checkPermissions } from "./permissions"; + +/** + * Find a session in a database + * @param db Arango database + * @param token Session token + * @returns The session data + */ +export async function findSession( + db: Database, + token: string +): Promise | null> { + if (token) { + const cursor = await db.query(aql` + FOR session IN sessions + RETURN session + `); + while (cursor.hasNext) { + let doc = await cursor.next(); + if (await compare(token, doc.token)) { + return doc; + } + } + } + return null; +} + +/** + * Find authorizing user + * @param db Arango database + * @param token Session token + * @returns The user data + */ +export async function findAuthorizer( + db: Database, + token?: string +): Promise<(Document & { _session?: Document }) | null> { + const session = await findSession(db, token || ""); + if (!session?.user) { + const cursor = await db.query(aql` + FOR user IN users + FILTER user.username == "_guest" + LIMIT 1 + RETURN user + `); + if (cursor.hasNext) { + return cursor.next(); + } else { + return null; + } + } else { + const users = db.collection("users"); + try { + let user = await users.document(session?.user); + user._session = session; + return user; + } catch (e) { + throw new Error("Invalid user"); + } + } +} + +type AuthorizationReturn = T extends true + ? [null, T] + : [Document & { _token?: string; _session?: Document }, T]; +/** + * Authorize a user + * @param req Request + * @param res Response + * @param db Arango database + * @param permissions The permissions to check against + * @returns The user and whether to return + */ +export async function authorizeRequest( + req: IncomingMessage, + res: ServerResponse, + db: Database, + permissions: string[] +): Promise { + const token = req.headers.authorization; + let user: + | (Document & { _token?: string; _session?: Document }) + | null; + try { + user = await findAuthorizer(db, token); + } catch (e) { + res.writeHead(401); + res.end( + JSON.stringify({ + message: "Invalid user" + }) + ); + return [null, true]; + } + if (user === null) { + res.writeHead(500); + res.end( + JSON.stringify({ + message: "_guest user not found" + }) + ); + return [null, true]; + } + + if (!checkPermissions(permissions, user.permissions)) { + res.writeHead(403); + res.end( + JSON.stringify({ + message: "Missing permissions" + }) + ); + return [null, true]; + } + + user._token = token; + return [user, false]; +} diff --git a/src/lib/hypixel.ts b/src/lib/hypixel.ts index 30e89a0..76f5f0e 100644 --- a/src/lib/hypixel.ts +++ b/src/lib/hypixel.ts @@ -31,6 +31,14 @@ export async function getAPIKeyInformation(key: string): Promise< }; } +export type Profile = { + profile_id: string; + members: any; + community_upgrades: any | null; + cute_name: string; + banking: any | null; + game_mode: string | null; +}; /** * Get Skyblock profiles by player * @param key API key diff --git a/src/lib/mojang.ts b/src/lib/mojang.ts new file mode 100644 index 0000000..c2d0fc3 --- /dev/null +++ b/src/lib/mojang.ts @@ -0,0 +1,24 @@ +import { requestWithDefaults } from "./util"; + +export const ENDPOINT = "https://api.mojang.com/"; +const request = requestWithDefaults(ENDPOINT); + +/** + * Get a Minecraft username by UUID + * @param uuid Player's UUID + * @returns A username + */ +export async function getUsernameByUUID(uuid: string): Promise { + let response = await request(`/user/profile/${uuid}`); + return (await response.json()).name; +} + +/** + * Get a Minecraft UUID by username + * @param name Player's username + * @returns A UUID + */ +export async function getUUIDByUsername(name: string): Promise { + let response = await request(`/users/profiles/minecraft/${name}`); + return (await response.json()).id; +} diff --git a/src/lib/util.ts b/src/lib/util.ts index c859bf0..c7ccc5d 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -16,7 +16,7 @@ export function getFetch(): typeof fetch { * @returns A fetch-like function */ export function requestWithDefaults( - baseURL: string, + baseURL?: string, defaultInit?: RequestInit ): (endpoint: string, init?: RequestInit) => Promise { return async function ( @@ -24,7 +24,7 @@ export function requestWithDefaults( overrideInit?: RequestInit ): Promise { let response = await getFetch()( - new URL(endpoint, baseURL).href, + baseURL ? new URL(endpoint, baseURL).href : endpoint, defaultInit ? deepMerge(defaultInit, overrideInit) : overrideInit ); if (response.ok) {