This commit is contained in:
Gleb Koval 2021-07-07 19:47:40 +01:00
parent 16cd5b70e3
commit ac8b5a911e
No known key found for this signature in database
GPG Key ID: 120F2F6DA9D995FB
8 changed files with 418 additions and 0 deletions

34
src/routes/api/_types.ts Normal file
View File

@ -0,0 +1,34 @@
export interface Config<TS = string, T = any> {
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)[];
}
>;

29
src/routes/api/graphs.ts Normal file
View File

@ -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<Graph> = await cursor.next();
graphs.push({
graph: next._key,
type: next.type,
data: next.data
});
}
res.writeHead(200);
res.end(JSON.stringify(graphs));
}

171
src/routes/api/login.ts Normal file
View File

@ -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<User>;
// 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
};
}

36
src/routes/api/logout.ts Normal file
View File

@ -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();
}

View File

@ -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<Tracker> = 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));
}

View File

@ -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"
})
);
}
}

View File

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

18
src/routes/api/uuid.ts Normal file
View File

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