API
This commit is contained in:
parent
16cd5b70e3
commit
ac8b5a911e
|
@ -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)[];
|
||||||
|
}
|
||||||
|
>;
|
|
@ -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));
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
Loading…
Reference in New Issue