This commit is contained in:
Jesse Wierzbinski 2023-09-14 15:22:27 -10:00
parent aad3ee78d1
commit 8162a5050c
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
19 changed files with 922 additions and 84 deletions

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getConfig } from "@config";
import { errorResponse, jsonResponse } from "@response";
import {
APAccept,
@ -8,6 +9,7 @@ import {
APFollow,
APObject,
APReject,
APTombstone,
APUpdate,
} from "activitypub-types";
import { MatchedRoute } from "bun";
@ -26,6 +28,8 @@ export default async (
return errorResponse("Method not allowed", 405);
}
const config = getConfig();
// Process request body
const body: APActivity = await req.json();
@ -113,7 +117,35 @@ export default async (
if (!object) return errorResponse("Object not found", 404);
await object.remove();
const activities = await RawActivity.findBy({
objects: {
id: (body.object as RawObject).id,
},
});
if (config.activitypub.use_tombstones) {
object.data = {
...object.data,
type: "Tombstone",
deleted: new Date(),
formerType: object.data.type,
} as APTombstone;
await object.save();
} else {
activities.forEach(
activity =>
(activity.objects = activity.objects.filter(
o => o.id !== object.id
))
);
await Promise.all(
activities.map(async activity => await activity.save())
);
await object.remove();
}
break;
}
case "Accept" as APAccept: {

View file

@ -0,0 +1,25 @@
import { MatchedRoute } from "bun";
/**
* Returns an HTML login form
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const html = Bun.file("./pages/login.html");
const css = Bun.file("./pages/uno.css");
return new Response(
(await html.text())
.replace(
"{{URL}}",
`/auth/login?redirect_uri=${matchedRoute.query.redirect_uri}&response_type=${matchedRoute.query.response_type}&client_id=${matchedRoute.query.client_id}&scopes=${matchedRoute.query.scopes}`
)
.replace("{{STYLES}}", `<style>${await css.text()}</style>`),
{
headers: {
"Content-Type": "text/html",
},
}
);
};

View file

@ -1,3 +1,4 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { Token } from "~database/entities/Token";
@ -5,14 +6,15 @@ import { Token } from "~database/entities/Token";
* Allows getting token from OAuth code
*/
export default async (req: Request): Promise<Response> => {
const body = await req.formData();
const grant_type = body.get("grant_type")?.toString() || null;
const code = body.get("code")?.toString() || "";
const redirect_uri = body.get("redirect_uri")?.toString() || "";
const client_id = body.get("client_id")?.toString() || "";
const client_secret = body.get("client_secret")?.toString() || "";
const scope = body.get("scope")?.toString() || null;
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
await parseRequest<{
grant_type: string;
code: string;
redirect_uri: string;
client_id: string;
client_secret: string;
scope: string;
}>(req);
if (grant_type !== "authorization_code")
return errorResponse(

View file

@ -1,4 +1,5 @@
import { getConfig } from "@config";
import { parseRequest } from "@request";
import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail";
import { User } from "~database/entities/User";
@ -9,14 +10,14 @@ import { User } from "~database/entities/User";
export default async (req: Request): Promise<Response> => {
// TODO: Add Authorization check
const body: {
const body = await parseRequest<{
username: string;
email: string;
password: string;
agreement: boolean;
locale: string;
reason: string;
} = await req.json();
}>(req);
const config = getConfig();
@ -63,28 +64,28 @@ export default async (req: Request): Promise<Response> => {
config.validation.max_username_size;
// Check if username is valid
if (!body.username.match(/^[a-zA-Z0-9_]+$/))
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
errors.details.username.push({
error: "ERR_INVALID",
description: `must only contain letters, numbers, and underscores`,
});
// Check if username is too long
if (body.username.length > config.validation.max_username_size)
if ((body.username?.length ?? 0) > config.validation.max_username_size)
errors.details.username.push({
error: "ERR_TOO_LONG",
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
});
// Check if username is too short
if (body.username.length < 3)
if ((body.username?.length ?? 0) < 3)
errors.details.username.push({
error: "ERR_TOO_SHORT",
description: `is too short (minimum is 3 characters)`,
});
// Check if username is reserved
if (config.validation.username_blacklist.includes(body.username))
if (config.validation.username_blacklist.includes(body.username ?? ""))
errors.details.username.push({
error: "ERR_RESERVED",
description: `is reserved`,
@ -99,7 +100,7 @@ export default async (req: Request): Promise<Response> => {
// Check if email is valid
if (
!body.email.match(
!body.email?.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)
)
@ -110,9 +111,9 @@ export default async (req: Request): Promise<Response> => {
// Check if email is blocked
if (
config.validation.email_blacklist.includes(body.email) ||
config.validation.email_blacklist.includes(body.email ?? "") ||
(config.validation.blacklist_tempmail &&
tempmailDomains.domains.includes(body.email.split("@")[1]))
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
)
errors.details.email.push({
error: "ERR_BLOCKED",
@ -148,9 +149,9 @@ export default async (req: Request): Promise<Response> => {
const newUser = new User();
newUser.username = body.username;
newUser.email = body.email;
newUser.password = await Bun.password.hash(body.password);
newUser.username = body.username ?? "";
newUser.email = body.email ?? "";
newUser.password = await Bun.password.hash(body.password ?? "");
// TODO: Return access token
return new Response();

View file

@ -1,5 +1,6 @@
import { getUserByToken } from "@auth";
import { getConfig } from "@config";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
/**
@ -21,16 +22,18 @@ export default async (req: Request): Promise<Response> => {
if (!user) return errorResponse("Unauthorized", 401);
const config = getConfig();
const body = await req.formData();
const display_name = body.get("display_name")?.toString() || null;
const note = body.get("note")?.toString() || null;
// Avatar is a file element
const avatar = (body.get("avatar") as File | null) || null;
const header = (body.get("header") as File | null) || null;
const locked = body.get("locked")?.toString() || null;
const bot = body.get("bot")?.toString() || null;
const discoverable = body.get("discoverable")?.toString() || null;
const { display_name, note, avatar, header, locked, bot, discoverable } =
await parseRequest<{
display_name: string;
note: string;
avatar: File;
header: File;
locked: string;
bot: string;
discoverable: string;
}>(req);
// TODO: Implement other options like field or source
// const source_privacy = body.get("source[privacy]")?.toString() || null;
// const source_sensitive = body.get("source[sensitive]")?.toString() || null;

View file

@ -1,3 +1,4 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application";
@ -6,12 +7,12 @@ import { Application } from "~database/entities/Application";
* Creates a new application to obtain OAuth 2 credentials
*/
export default async (req: Request): Promise<Response> => {
const body = await req.formData();
const client_name = body.get("client_name")?.toString() || null;
const redirect_uris = body.get("redirect_uris")?.toString() || null;
const scopes = body.get("scopes")?.toString() || null;
const website = body.get("website")?.toString() || null;
const { client_name, redirect_uris, scopes, website } = await parseRequest<{
client_name: string;
redirect_uris: string;
scopes: string;
website: string;
}>(req);
const application = new Application();

View file

@ -1,22 +0,0 @@
import { MatchedRoute } from "bun";
/**
* Returns an HTML login form
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const html = Bun.file("./pages/login.html");
return new Response(
(await html.text()).replace(
"{{URL}}",
`/auth/login?redirect_uri=${matchedRoute.query.redirect_uri}&response_type=${matchedRoute.query.response_type}&client_id=${matchedRoute.query.client_id}&scopes=${matchedRoute.query.scopes}`
),
{
headers: {
"Content-Type": "text/html",
},
}
);
};