mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
New API route format to make code cleaner
This commit is contained in:
parent
c7b2f5b741
commit
ca7d325cb1
15
.dockerignore
Normal file
15
.dockerignore
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
node_modules
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
Makefile
|
||||
helm-charts
|
||||
.env
|
||||
.editorconfig
|
||||
.idea
|
||||
coverage*
|
||||
|
|
@ -14,5 +14,7 @@ module.exports = {
|
|||
root: true,
|
||||
rules: {
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export class RawActivity extends BaseEntity {
|
|||
* Returns the ActivityPub representation of the activity.
|
||||
* @returns The ActivityPub representation of the activity.
|
||||
*/
|
||||
makeActivityPubRepresentation() {
|
||||
getActivityPubRepresentation() {
|
||||
return {
|
||||
...this.data,
|
||||
object: this.objects[0].data,
|
||||
|
|
|
|||
|
|
@ -156,6 +156,13 @@ export class User extends BaseEntity {
|
|||
@JoinTable()
|
||||
pinned_notes!: RawObject[];
|
||||
|
||||
static async getFromRequest(req: Request) {
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||
|
||||
return { user: await User.retrieveFromToken(token), token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this user data from its actor
|
||||
* @returns The updated user.
|
||||
|
|
|
|||
108
index.ts
108
index.ts
|
|
@ -1,8 +1,12 @@
|
|||
import { getConfig } from "@config";
|
||||
import { jsonResponse } from "@response";
|
||||
import { MatchedRoute } from "bun";
|
||||
import { appendFile } from "fs/promises";
|
||||
import { matches } from "ip-matching";
|
||||
import "reflect-metadata";
|
||||
import { AppDataSource } from "~database/datasource";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
const router = new Bun.FileSystemRouter({
|
||||
style: "nextjs",
|
||||
|
|
@ -25,6 +29,88 @@ Bun.serve({
|
|||
port: config.http.bind_port,
|
||||
hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0"
|
||||
async fetch(req) {
|
||||
/* Check for banned IPs */
|
||||
const request_ip = this.requestIP(req)?.address ?? "";
|
||||
|
||||
for (const ip of config.http.banned_ips) {
|
||||
try {
|
||||
if (matches(ip, request_ip)) {
|
||||
return new Response(undefined, {
|
||||
status: 403,
|
||||
statusText: "Forbidden",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[-] Error while parsing banned IP "${ip}" `);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await logRequest(req);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return jsonResponse({});
|
||||
}
|
||||
|
||||
const matchedRoute = router.match(req);
|
||||
|
||||
if (matchedRoute) {
|
||||
const file: {
|
||||
meta: APIRouteMeta;
|
||||
default: (
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute
|
||||
) => Response | Promise<Response>;
|
||||
} = await import(matchedRoute.filePath);
|
||||
|
||||
const meta = file.meta;
|
||||
|
||||
// Check for allowed requests
|
||||
if (!meta.allowedMethods.includes(req.method as any)) {
|
||||
return new Response(undefined, {
|
||||
status: 405,
|
||||
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
|
||||
", "
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Check for ratelimits
|
||||
|
||||
// Check for authentication if required
|
||||
if (meta.auth.required) {
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return new Response(undefined, {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
(meta.auth.requiredOnMethods ?? []).includes(req.method as any)
|
||||
) {
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return new Response(undefined, {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return file.default(req, matchedRoute);
|
||||
} else {
|
||||
return new Response(undefined, {
|
||||
status: 404,
|
||||
statusText: "Route not found",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const logRequest = async (req: Request) => {
|
||||
if (config.logging.log_requests_verbose) {
|
||||
await appendFile(
|
||||
`${process.cwd()}/logs/requests.log`,
|
||||
|
|
@ -56,26 +142,6 @@ Bun.serve({
|
|||
`[${new Date().toISOString()}] ${req.method} ${req.url}\n`
|
||||
);
|
||||
}
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return jsonResponse({});
|
||||
}
|
||||
|
||||
const matchedRoute = router.match(req);
|
||||
|
||||
if (matchedRoute) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
return (await import(matchedRoute.filePath)).default(
|
||||
req,
|
||||
matchedRoute
|
||||
) as Response | Promise<Response>;
|
||||
} else {
|
||||
return new Response(undefined, {
|
||||
status: 404,
|
||||
statusText: "Route not found",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
console.log("[+] Lysand started!");
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ip-matching": "^2.1.2",
|
||||
"jsonld": "^8.3.1",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/block",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blocks a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/follow",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Follow a user
|
||||
|
|
@ -13,13 +26,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { MatchedRoute } from "bun";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
|
|
@ -11,13 +24,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1];
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
let foundUser: User | null;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/mute",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mute a user
|
||||
|
|
@ -13,13 +26,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/note",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets a user note
|
||||
|
|
@ -13,13 +26,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/pin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Pin a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/remove_from_followers",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Removes an account from your followers list
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Status } from "~database/entities/Status";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/statuses",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all statuses for a user
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unblock",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blocks a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unfollow",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unfollows a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unmute",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unmute a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { MatchedRoute } from "bun";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unpin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unpin a user
|
||||
|
|
@ -12,13 +25,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,25 @@ import { parseRequest } from "@request";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIAccount } from "~types/entities/account";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/familiar_followers",
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Find familiar followers (followers of a user that you also follow)
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ import { parseRequest } from "@request";
|
|||
import { jsonResponse } from "@response";
|
||||
import { tempmailDomains } from "@tempmail";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/accounts",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
|
|
|
|||
|
|
@ -2,18 +2,25 @@ import { parseRequest } from "@request";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { Relationship } from "~database/entities/Relationship";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/relationships",
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Find relationships
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const self = await User.retrieveFromToken(token);
|
||||
const { user: self } = await User.getFromRequest(req);
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,22 +2,25 @@ import { getConfig } from "@config";
|
|||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["PATCH"],
|
||||
route: "/api/v1/accounts/update_credentials",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patches a user
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// Check if request is a PATCH request
|
||||
if (req.method !== "PATCH")
|
||||
return errorResponse("This method requires a PATCH request", 405);
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { User } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patches a user
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
// Check if request is a PATCH request
|
||||
if (req.method !== "GET")
|
||||
return errorResponse("This method requires a GET request", 405);
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { randomBytes } from "crypto";
|
||||
import { Application } from "~database/entities/Application";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/apps",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new application to obtain OAuth 2 credentials
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { Application } from "~database/entities/Application";
|
||||
import { User } from "~database/entities/User";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/apps/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns OAuth2 credentials
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user, token } = await User.getFromRequest(req);
|
||||
const application = await Application.getFromToken(token);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import { IsNull } from "typeorm";
|
||||
import { Emoji } from "~database/entities/Emoji";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/custom_emojis",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
* S
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export default async (): Promise<Response> => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { getConfig } from "@config";
|
||||
import { jsonResponse } from "@response";
|
||||
import { Status } from "~database/entities/Status";
|
||||
import { User } from "~database/entities/User";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/instance",
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { MatchedRoute } from "bun";
|
||||
import { RawObject } from "~database/entities/RawObject";
|
||||
import { Status } from "~database/entities/Status";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const meta: APIRouteMeta = applyConfig({
|
||||
allowedMethods: ["GET", "DELETE"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id",
|
||||
auth: {
|
||||
required: false,
|
||||
requiredOnMethods: ["DELETE"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
|
|
@ -13,13 +28,7 @@ export default async (
|
|||
): Promise<Response> => {
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user } = await User.getFromRequest(req);
|
||||
|
||||
// TODO: Add checks for user's permissions to view this status
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { applyConfig } from "@api";
|
||||
import { getConfig } from "@config";
|
||||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
|
|
@ -9,22 +10,25 @@ import { Application } from "~database/entities/Application";
|
|||
import { RawObject } from "~database/entities/RawObject";
|
||||
import { Status } from "~database/entities/Status";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const meta: APIRouteMeta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Post new status
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
// Check if request is a PATCH request
|
||||
if (req.method !== "POST")
|
||||
return errorResponse("This method requires a POST request", 405);
|
||||
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||
|
||||
if (!token)
|
||||
return errorResponse("This method requires an authenticated user", 422);
|
||||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
const { user, token } = await User.getFromRequest(req);
|
||||
const application = await Application.getFromToken(token);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { RawObject } from "~database/entities/RawObject";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const meta: APIRouteMeta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/home",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch home timeline statuses
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { RawObject } from "~database/entities/RawObject";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const meta: APIRouteMeta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/public",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch public timeline statuses
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { errorResponse } from "@response";
|
||||
import { MatchedRoute } from "bun";
|
||||
import { randomBytes } from "crypto";
|
||||
import { Application } from "~database/entities/Application";
|
||||
import { Token } from "~database/entities/Token";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const meta: APIRouteMeta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 4,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/auth/login",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* OAuth Code flow
|
||||
|
|
|
|||
12
types/api.ts
Normal file
12
types/api.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export interface APIRouteMeta {
|
||||
allowedMethods: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
|
||||
ratelimits: {
|
||||
max: number;
|
||||
duration: number;
|
||||
};
|
||||
route: string;
|
||||
auth: {
|
||||
required: boolean;
|
||||
requiredOnMethods?: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
|
||||
};
|
||||
}
|
||||
18
utils/api.ts
Normal file
18
utils/api.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { getConfig } from "@config";
|
||||
import { APIRouteMeta } from "~types/api";
|
||||
|
||||
export const applyConfig = (routeMeta: APIRouteMeta) => {
|
||||
const config = getConfig();
|
||||
const newMeta = routeMeta;
|
||||
|
||||
// Apply ratelimits from config
|
||||
newMeta.ratelimits.duration *= config.ratelimits.duration_coeff;
|
||||
newMeta.ratelimits.max *= config.ratelimits.max_coeff;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (config.custom_ratelimits[routeMeta.route]) {
|
||||
newMeta.ratelimits = config.custom_ratelimits[routeMeta.route];
|
||||
}
|
||||
|
||||
return newMeta;
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@ export interface ConfigType {
|
|||
base_url: string;
|
||||
bind: string;
|
||||
bind_port: string;
|
||||
banned_ips: string[];
|
||||
};
|
||||
|
||||
validation: {
|
||||
|
|
@ -67,6 +68,19 @@ export interface ConfigType {
|
|||
log_requests_verbose: boolean;
|
||||
log_filters: boolean;
|
||||
};
|
||||
|
||||
ratelimits: {
|
||||
duration_coeff: number;
|
||||
max_coeff: number;
|
||||
};
|
||||
|
||||
custom_ratelimits: Record<
|
||||
string,
|
||||
{
|
||||
duration: number;
|
||||
max: number;
|
||||
}
|
||||
>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +89,7 @@ export const configDefaults: ConfigType = {
|
|||
bind: "http://0.0.0.0",
|
||||
bind_port: "8000",
|
||||
base_url: "http://fediproject.localhost:8000",
|
||||
banned_ips: [],
|
||||
},
|
||||
database: {
|
||||
host: "localhost",
|
||||
|
|
@ -178,6 +193,11 @@ export const configDefaults: ConfigType = {
|
|||
log_requests_verbose: false,
|
||||
log_filters: true,
|
||||
},
|
||||
ratelimits: {
|
||||
duration_coeff: 1,
|
||||
max_coeff: 1,
|
||||
},
|
||||
custom_ratelimits: {},
|
||||
};
|
||||
|
||||
export const getConfig = () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue