mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 00:18:19 +01:00
refactor: Rewrite functions into packages
This commit is contained in:
parent
847e679a10
commit
78f216092b
2
index.ts
2
index.ts
|
|
@ -162,8 +162,6 @@ const logRequest = async (req: Request) => {
|
|||
);
|
||||
|
||||
// Add headers
|
||||
// @ts-expect-error TypeScript is missing entries for some reason
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const headers = req.headers.entries();
|
||||
|
||||
for (const [key, value] of headers) {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@
|
|||
"prisma": "^5.6.0",
|
||||
"prisma-redis-middleware": "^4.8.0",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.0-rc.2"
|
||||
"sharp": "^0.33.0-rc.2",
|
||||
"request-parser": "file:packages/request-parser",
|
||||
"config-manager": "file:packages/config-manager"
|
||||
}
|
||||
}
|
||||
8
packages/cli-parser/cli-builder.type.ts
Normal file
8
packages/cli-parser/cli-builder.type.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface CliParameter {
|
||||
name: string;
|
||||
// If not positioned, the argument will need to be called with --name value instead of just value
|
||||
positioned?: boolean;
|
||||
// Whether the argument needs a value (requires positioned to be false)
|
||||
needsValue?: boolean;
|
||||
type: "string" | "number" | "boolean" | "array";
|
||||
}
|
||||
203
packages/cli-parser/index.ts
Normal file
203
packages/cli-parser/index.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import type { CliParameter } from "./cli-builder.type";
|
||||
|
||||
export function startsWithArray(fullArray: any[], startArray: any[]) {
|
||||
if (startArray.length > fullArray.length) {
|
||||
return false;
|
||||
}
|
||||
return fullArray
|
||||
.slice(0, startArray.length)
|
||||
.every((value, index) => value === startArray[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a CLI
|
||||
* @param commands Array of commands to register
|
||||
*/
|
||||
export class CliBuilder {
|
||||
constructor(public commands: CliCommand[] = []) {}
|
||||
|
||||
/**
|
||||
* Add command to the CLI
|
||||
* @throws Error if command already exists
|
||||
* @param command Command to add
|
||||
*/
|
||||
registerCommand(command: CliCommand) {
|
||||
if (this.checkIfCommandAlreadyExists(command)) {
|
||||
throw new Error(
|
||||
`Command category '${command.categories.join(" ")}' already exists`
|
||||
);
|
||||
}
|
||||
this.commands.push(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple commands to the CLI
|
||||
* @throws Error if command already exists
|
||||
* @param commands Commands to add
|
||||
*/
|
||||
registerCommands(commands: CliCommand[]) {
|
||||
const existingCommand = commands.find(command =>
|
||||
this.checkIfCommandAlreadyExists(command)
|
||||
);
|
||||
if (existingCommand) {
|
||||
throw new Error(
|
||||
`Command category '${existingCommand.categories.join(" ")}' already exists`
|
||||
);
|
||||
}
|
||||
this.commands.push(...commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove command from the CLI
|
||||
* @param command Command to remove
|
||||
*/
|
||||
deregisterCommand(command: CliCommand) {
|
||||
this.commands = this.commands.filter(
|
||||
registeredCommand => registeredCommand !== command
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove multiple commands from the CLI
|
||||
* @param commands Commands to remove
|
||||
*/
|
||||
deregisterCommands(commands: CliCommand[]) {
|
||||
this.commands = this.commands.filter(
|
||||
registeredCommand => !commands.includes(registeredCommand)
|
||||
);
|
||||
}
|
||||
|
||||
checkIfCommandAlreadyExists(command: CliCommand) {
|
||||
return this.commands.some(
|
||||
registeredCommand =>
|
||||
registeredCommand.categories.length ==
|
||||
command.categories.length &&
|
||||
registeredCommand.categories.every(
|
||||
(category, index) => category === command.categories[index]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relevant args for the command (without executable or runtime)
|
||||
* @param args Arguments passed to the CLI
|
||||
*/
|
||||
private getRelevantArgs(args: string[]) {
|
||||
if (args[0].startsWith("./")) {
|
||||
// Formatted like ./cli.ts [command]
|
||||
return args.slice(1);
|
||||
} else if (args[0].includes("bun")) {
|
||||
// Formatted like bun cli.ts [command]
|
||||
return args.slice(2);
|
||||
} else {
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn raw system args into a CLI command and run it
|
||||
* @param args Args directly from process.argv
|
||||
*/
|
||||
processArgs(args: string[]) {
|
||||
const revelantArgs = this.getRelevantArgs(args);
|
||||
// Find revelant command
|
||||
// Search for a command with as many categories matching args as possible
|
||||
const matchingCommands = this.commands.filter(command =>
|
||||
startsWithArray(revelantArgs, command.categories)
|
||||
);
|
||||
|
||||
// Get command with largest category size
|
||||
const command = matchingCommands.reduce((prev, current) =>
|
||||
prev.categories.length > current.categories.length ? prev : current
|
||||
);
|
||||
|
||||
const argsWithoutCategories = args.slice(command.categories.length - 1);
|
||||
|
||||
command.run(argsWithoutCategories);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A command that can be executed from the command line
|
||||
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
|
||||
*/
|
||||
export class CliCommand {
|
||||
constructor(
|
||||
public categories: string[],
|
||||
public argTypes: CliParameter[],
|
||||
private execute: (args: Record<string, any>) => void
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parses string array arguments into a full JavaScript object
|
||||
* @param argsWithoutCategories
|
||||
* @returns
|
||||
*/
|
||||
private parseArgs(argsWithoutCategories: string[]): Record<string, any> {
|
||||
const parsedArgs: Record<string, any> = {};
|
||||
let currentParameter: CliParameter | null = null;
|
||||
|
||||
for (let i = 0; i < argsWithoutCategories.length; i++) {
|
||||
const arg = argsWithoutCategories[i];
|
||||
|
||||
if (arg.startsWith("--")) {
|
||||
const argName = arg.substring(2);
|
||||
currentParameter =
|
||||
this.argTypes.find(argType => argType.name === argName) ||
|
||||
null;
|
||||
if (currentParameter && !currentParameter.needsValue) {
|
||||
parsedArgs[argName] = true;
|
||||
currentParameter = null;
|
||||
} else if (currentParameter && currentParameter.needsValue) {
|
||||
parsedArgs[argName] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
currentParameter.type
|
||||
);
|
||||
i++;
|
||||
currentParameter = null;
|
||||
}
|
||||
} else if (currentParameter) {
|
||||
parsedArgs[currentParameter.name] = this.castArgValue(
|
||||
arg,
|
||||
currentParameter.type
|
||||
);
|
||||
currentParameter = null;
|
||||
} else {
|
||||
const positionedArgType = this.argTypes.find(
|
||||
argType => argType.positioned
|
||||
);
|
||||
if (positionedArgType) {
|
||||
parsedArgs[positionedArgType.name] = this.castArgValue(
|
||||
arg,
|
||||
positionedArgType.type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedArgs;
|
||||
}
|
||||
|
||||
private castArgValue(value: string, type: CliParameter["type"]): any {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return value;
|
||||
case "number":
|
||||
return Number(value);
|
||||
case "boolean":
|
||||
return value === "true";
|
||||
case "array":
|
||||
return value.split(",");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the execute function with the parsed parameters as an argument
|
||||
*/
|
||||
run(argsWithoutCategories: string[]) {
|
||||
const args = this.parseArgs(argsWithoutCategories);
|
||||
this.execute(args);
|
||||
}
|
||||
}
|
||||
6
packages/cli-parser/package.json
Normal file
6
packages/cli-parser/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "arg-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
217
packages/cli-parser/tests/cli-builder.test.ts
Normal file
217
packages/cli-parser/tests/cli-builder.test.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
||||
import { CliCommand, CliBuilder, startsWithArray } from "..";
|
||||
import { describe, beforeEach, it, expect, jest } from "bun:test";
|
||||
|
||||
describe("startsWithArray", () => {
|
||||
it("should return true when fullArray starts with startArray", () => {
|
||||
const fullArray = ["a", "b", "c", "d", "e"];
|
||||
const startArray = ["a", "b", "c"];
|
||||
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when fullArray does not start with startArray", () => {
|
||||
const fullArray = ["a", "b", "c", "d", "e"];
|
||||
const startArray = ["b", "c", "d"];
|
||||
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when startArray is empty", () => {
|
||||
const fullArray = ["a", "b", "c", "d", "e"];
|
||||
const startArray: any[] = [];
|
||||
expect(startsWithArray(fullArray, startArray)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when fullArray is shorter than startArray", () => {
|
||||
const fullArray = ["a", "b", "c"];
|
||||
const startArray = ["a", "b", "c", "d", "e"];
|
||||
expect(startsWithArray(fullArray, startArray)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CliCommand", () => {
|
||||
let cliCommand: CliCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{ name: "arg1", type: "string", needsValue: true },
|
||||
{ name: "arg2", type: "number", needsValue: true },
|
||||
{ name: "arg3", type: "boolean", needsValue: false },
|
||||
{ name: "arg4", type: "array", needsValue: true },
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse string arguments correctly", () => {
|
||||
const args = cliCommand["parseArgs"]([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
]);
|
||||
expect(args).toEqual({
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should cast argument values correctly", () => {
|
||||
expect(cliCommand["castArgValue"]("42", "number")).toBe(42);
|
||||
expect(cliCommand["castArgValue"]("true", "boolean")).toBe(true);
|
||||
expect(cliCommand["castArgValue"]("value1,value2", "array")).toEqual([
|
||||
"value1",
|
||||
"value2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should run the execute function with the parsed parameters", () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{ name: "arg1", type: "string", needsValue: true },
|
||||
{ name: "arg2", type: "number", needsValue: true },
|
||||
{ name: "arg3", type: "boolean", needsValue: false },
|
||||
{ name: "arg4", type: "array", needsValue: true },
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
|
||||
cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith({
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with a mix of positioned and non-positioned arguments", () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{ name: "arg1", type: "string", needsValue: true },
|
||||
{ name: "arg2", type: "number", needsValue: true },
|
||||
{ name: "arg3", type: "boolean", needsValue: false },
|
||||
{ name: "arg4", type: "array", needsValue: true },
|
||||
{
|
||||
name: "arg5",
|
||||
type: "string",
|
||||
needsValue: true,
|
||||
positioned: true,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
|
||||
cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
"value5",
|
||||
]);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith({
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
arg5: "value5",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CliBuilder", () => {
|
||||
let cliBuilder: CliBuilder;
|
||||
let mockCommand1: CliCommand;
|
||||
let mockCommand2: CliCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommand1 = new CliCommand(["category1"], [], jest.fn());
|
||||
mockCommand2 = new CliCommand(["category2"], [], jest.fn());
|
||||
cliBuilder = new CliBuilder([mockCommand1]);
|
||||
});
|
||||
|
||||
it("should register a command correctly", () => {
|
||||
cliBuilder.registerCommand(mockCommand2);
|
||||
expect(cliBuilder.commands).toContain(mockCommand2);
|
||||
});
|
||||
|
||||
it("should register multiple commands correctly", () => {
|
||||
const mockCommand3 = new CliCommand(["category3"], [], jest.fn());
|
||||
cliBuilder.registerCommands([mockCommand2, mockCommand3]);
|
||||
expect(cliBuilder.commands).toContain(mockCommand2);
|
||||
expect(cliBuilder.commands).toContain(mockCommand3);
|
||||
});
|
||||
|
||||
it("should error when adding duplicates", () => {
|
||||
expect(() => {
|
||||
cliBuilder.registerCommand(mockCommand1);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
cliBuilder.registerCommands([mockCommand1]);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should deregister a command correctly", () => {
|
||||
cliBuilder.deregisterCommand(mockCommand1);
|
||||
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
||||
});
|
||||
|
||||
it("should deregister multiple commands correctly", () => {
|
||||
cliBuilder.registerCommand(mockCommand2);
|
||||
cliBuilder.deregisterCommands([mockCommand1, mockCommand2]);
|
||||
expect(cliBuilder.commands).not.toContain(mockCommand1);
|
||||
expect(cliBuilder.commands).not.toContain(mockCommand2);
|
||||
});
|
||||
|
||||
it("should process args correctly", () => {
|
||||
const mockExecute = jest.fn();
|
||||
const mockCommand = new CliCommand(
|
||||
["category1", "sub1"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: "string",
|
||||
needsValue: true,
|
||||
positioned: false,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
cliBuilder.registerCommand(mockCommand);
|
||||
cliBuilder.processArgs([
|
||||
"./cli.ts",
|
||||
"category1",
|
||||
"sub1",
|
||||
"--arg1",
|
||||
"value1",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith({
|
||||
arg1: "value1",
|
||||
});
|
||||
});
|
||||
});
|
||||
359
packages/config-manager/config-type.type.ts
Normal file
359
packages/config-manager/config-type.type.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
export interface ConfigType {
|
||||
database: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
|
||||
redis: {
|
||||
queue: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
database: number | null;
|
||||
};
|
||||
cache: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
database: number | null;
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
meilisearch: {
|
||||
host: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
signups: {
|
||||
tos_url: string;
|
||||
rules: string[];
|
||||
registration: boolean;
|
||||
};
|
||||
|
||||
oidc: {
|
||||
providers: {
|
||||
name: string;
|
||||
id: string;
|
||||
url: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
icon: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
http: {
|
||||
base_url: string;
|
||||
bind: string;
|
||||
bind_port: string;
|
||||
banned_ips: string[];
|
||||
banned_user_agents: string[];
|
||||
};
|
||||
|
||||
instance: {
|
||||
name: string;
|
||||
description: string;
|
||||
banner: string;
|
||||
logo: string;
|
||||
};
|
||||
|
||||
smtp: {
|
||||
server: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
tls: boolean;
|
||||
};
|
||||
|
||||
validation: {
|
||||
max_displayname_size: number;
|
||||
max_bio_size: number;
|
||||
max_username_size: number;
|
||||
max_note_size: number;
|
||||
max_avatar_size: number;
|
||||
max_header_size: number;
|
||||
max_media_size: number;
|
||||
max_media_attachments: number;
|
||||
max_media_description_size: number;
|
||||
max_poll_options: number;
|
||||
max_poll_option_size: number;
|
||||
min_poll_duration: number;
|
||||
max_poll_duration: number;
|
||||
|
||||
username_blacklist: string[];
|
||||
blacklist_tempmail: boolean;
|
||||
email_blacklist: string[];
|
||||
url_scheme_whitelist: string[];
|
||||
|
||||
enforce_mime_types: boolean;
|
||||
allowed_mime_types: string[];
|
||||
};
|
||||
|
||||
media: {
|
||||
backend: string;
|
||||
deduplicate_media: boolean;
|
||||
conversion: {
|
||||
convert_images: boolean;
|
||||
convert_to: string;
|
||||
};
|
||||
};
|
||||
|
||||
s3: {
|
||||
endpoint: string;
|
||||
access_key: string;
|
||||
secret_access_key: string;
|
||||
region: string;
|
||||
bucket_name: string;
|
||||
public_url: string;
|
||||
};
|
||||
|
||||
defaults: {
|
||||
visibility: string;
|
||||
language: string;
|
||||
avatar: string;
|
||||
header: string;
|
||||
};
|
||||
|
||||
email: {
|
||||
send_on_report: boolean;
|
||||
send_on_suspend: boolean;
|
||||
send_on_unsuspend: boolean;
|
||||
};
|
||||
|
||||
activitypub: {
|
||||
use_tombstones: boolean;
|
||||
reject_activities: string[];
|
||||
force_followers_only: string[];
|
||||
discard_reports: string[];
|
||||
discard_deletes: string[];
|
||||
discard_banners: string[];
|
||||
discard_avatars: string[];
|
||||
discard_updates: string[];
|
||||
discard_follows: string[];
|
||||
force_sensitive: string[];
|
||||
remove_media: string[];
|
||||
fetch_all_collection_members: boolean;
|
||||
authorized_fetch: boolean;
|
||||
};
|
||||
|
||||
filters: {
|
||||
note_filters: string[];
|
||||
username_filters: string[];
|
||||
displayname_filters: string[];
|
||||
bio_filters: string[];
|
||||
emoji_filters: string[];
|
||||
};
|
||||
|
||||
logging: {
|
||||
log_requests: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
export const configDefaults: ConfigType = {
|
||||
http: {
|
||||
bind: "http://0.0.0.0",
|
||||
bind_port: "8000",
|
||||
base_url: "http://lysand.localhost:8000",
|
||||
banned_ips: [],
|
||||
banned_user_agents: [],
|
||||
},
|
||||
database: {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "lysand",
|
||||
},
|
||||
redis: {
|
||||
queue: {
|
||||
host: "localhost",
|
||||
port: 6379,
|
||||
password: "",
|
||||
database: 0,
|
||||
},
|
||||
cache: {
|
||||
host: "localhost",
|
||||
port: 6379,
|
||||
password: "",
|
||||
database: 1,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
meilisearch: {
|
||||
host: "localhost",
|
||||
port: 1491,
|
||||
api_key: "",
|
||||
enabled: false,
|
||||
},
|
||||
signups: {
|
||||
tos_url: "",
|
||||
rules: [],
|
||||
registration: false,
|
||||
},
|
||||
oidc: {
|
||||
providers: [],
|
||||
},
|
||||
instance: {
|
||||
banner: "",
|
||||
description: "",
|
||||
logo: "",
|
||||
name: "",
|
||||
},
|
||||
smtp: {
|
||||
password: "",
|
||||
port: 465,
|
||||
server: "",
|
||||
tls: true,
|
||||
username: "",
|
||||
},
|
||||
media: {
|
||||
backend: "local",
|
||||
deduplicate_media: true,
|
||||
conversion: {
|
||||
convert_images: false,
|
||||
convert_to: "webp",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
send_on_report: false,
|
||||
send_on_suspend: false,
|
||||
send_on_unsuspend: false,
|
||||
},
|
||||
s3: {
|
||||
access_key: "",
|
||||
bucket_name: "",
|
||||
endpoint: "",
|
||||
public_url: "",
|
||||
region: "",
|
||||
secret_access_key: "",
|
||||
},
|
||||
validation: {
|
||||
max_displayname_size: 50,
|
||||
max_bio_size: 6000,
|
||||
max_note_size: 5000,
|
||||
max_avatar_size: 5_000_000,
|
||||
max_header_size: 5_000_000,
|
||||
max_media_size: 40_000_000,
|
||||
max_media_attachments: 10,
|
||||
max_media_description_size: 1000,
|
||||
max_poll_options: 20,
|
||||
max_poll_option_size: 500,
|
||||
min_poll_duration: 60,
|
||||
max_poll_duration: 1893456000,
|
||||
max_username_size: 30,
|
||||
|
||||
username_blacklist: [
|
||||
".well-known",
|
||||
"~",
|
||||
"about",
|
||||
"activities",
|
||||
"api",
|
||||
"auth",
|
||||
"dev",
|
||||
"inbox",
|
||||
"internal",
|
||||
"main",
|
||||
"media",
|
||||
"nodeinfo",
|
||||
"notice",
|
||||
"oauth",
|
||||
"objects",
|
||||
"proxy",
|
||||
"push",
|
||||
"registration",
|
||||
"relay",
|
||||
"settings",
|
||||
"status",
|
||||
"tag",
|
||||
"users",
|
||||
"web",
|
||||
"search",
|
||||
"mfa",
|
||||
],
|
||||
|
||||
blacklist_tempmail: false,
|
||||
|
||||
email_blacklist: [],
|
||||
|
||||
url_scheme_whitelist: [
|
||||
"http",
|
||||
"https",
|
||||
"ftp",
|
||||
"dat",
|
||||
"dweb",
|
||||
"gopher",
|
||||
"hyper",
|
||||
"ipfs",
|
||||
"ipns",
|
||||
"irc",
|
||||
"xmpp",
|
||||
"ircs",
|
||||
"magnet",
|
||||
"mailto",
|
||||
"mumble",
|
||||
"ssb",
|
||||
],
|
||||
|
||||
enforce_mime_types: false,
|
||||
allowed_mime_types: [],
|
||||
},
|
||||
defaults: {
|
||||
visibility: "public",
|
||||
language: "en",
|
||||
avatar: "",
|
||||
header: "",
|
||||
},
|
||||
activitypub: {
|
||||
use_tombstones: true,
|
||||
reject_activities: [],
|
||||
force_followers_only: [],
|
||||
discard_reports: [],
|
||||
discard_deletes: [],
|
||||
discard_banners: [],
|
||||
discard_avatars: [],
|
||||
force_sensitive: [],
|
||||
discard_updates: [],
|
||||
discard_follows: [],
|
||||
remove_media: [],
|
||||
fetch_all_collection_members: false,
|
||||
authorized_fetch: false,
|
||||
},
|
||||
filters: {
|
||||
note_filters: [],
|
||||
username_filters: [],
|
||||
displayname_filters: [],
|
||||
bio_filters: [],
|
||||
emoji_filters: [],
|
||||
},
|
||||
logging: {
|
||||
log_requests: false,
|
||||
log_requests_verbose: false,
|
||||
log_filters: true,
|
||||
},
|
||||
ratelimits: {
|
||||
duration_coeff: 1,
|
||||
max_coeff: 1,
|
||||
},
|
||||
custom_ratelimits: {},
|
||||
};
|
||||
118
packages/config-manager/index.ts
Normal file
118
packages/config-manager/index.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* @file index.ts
|
||||
* @summary ConfigManager system to retrieve and modify system configuration
|
||||
* @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml
|
||||
* Fuses both and provides a way to retrieve individual values
|
||||
*/
|
||||
|
||||
import { parse, stringify, type JsonMap } from "@iarna/toml";
|
||||
import type { ConfigType } from "./config-type.type";
|
||||
import merge from "merge-deep-ts";
|
||||
|
||||
export class ConfigManager {
|
||||
constructor(
|
||||
public config: {
|
||||
configPathOverride?: string;
|
||||
internalConfigPathOverride?: string;
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @summary Reads the config files and returns the merge as a JSON object
|
||||
* @returns {Promise<T = ConfigType>} The merged config file as a JSON object
|
||||
*/
|
||||
async getConfig<T = ConfigType>() {
|
||||
const config = await this.readConfig<T>();
|
||||
const internalConfig = await this.readInternalConfig<T>();
|
||||
|
||||
return this.mergeConfigs<T>(config, internalConfig);
|
||||
}
|
||||
|
||||
getConfigPath() {
|
||||
return (
|
||||
this.config.configPathOverride ||
|
||||
process.cwd() + "/config/config.toml"
|
||||
);
|
||||
}
|
||||
|
||||
getInternalConfigPath() {
|
||||
return (
|
||||
this.config.internalConfigPathOverride ||
|
||||
process.cwd() + "/config/config.internal.toml"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Reads the internal config file and returns it as a JSON object
|
||||
* @returns {Promise<T = ConfigType>} The internal config file as a JSON object
|
||||
*/
|
||||
private async readInternalConfig<T = ConfigType>() {
|
||||
const config = Bun.file(this.getInternalConfigPath());
|
||||
|
||||
if (!(await config.exists())) {
|
||||
await Bun.write(config, "");
|
||||
}
|
||||
|
||||
return this.parseConfig<T>(await config.text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Reads the config file and returns it as a JSON object
|
||||
* @returns {Promise<T = ConfigType>} The config file as a JSON object
|
||||
*/
|
||||
private async readConfig<T = ConfigType>() {
|
||||
const config = Bun.file(this.getConfigPath());
|
||||
|
||||
if (!(await config.exists())) {
|
||||
throw new Error(
|
||||
`Error while reading config at path ${this.getConfigPath()}: Config file not found`
|
||||
);
|
||||
}
|
||||
|
||||
return this.parseConfig<T>(await config.text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Parses a TOML string and returns it as a JSON object
|
||||
* @param text The TOML string to parse
|
||||
* @returns {T = ConfigType} The parsed TOML string as a JSON object
|
||||
* @throws {Error} If the TOML string is invalid
|
||||
* @private
|
||||
*/
|
||||
private parseConfig<T = ConfigType>(text: string) {
|
||||
try {
|
||||
// To all [Symbol] keys from the object
|
||||
return JSON.parse(JSON.stringify(parse(text))) as T;
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`Error while parsing config at path ${this.getConfigPath()}: ${e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes changed values to the internal config
|
||||
* @param config The new config object
|
||||
*/
|
||||
async writeConfig<T = ConfigType>(config: T) {
|
||||
const path = this.getInternalConfigPath();
|
||||
const file = Bun.file(path);
|
||||
|
||||
await Bun.write(
|
||||
file,
|
||||
`# THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT IT MANUALLY, EDIT THE STANDARD CONFIG.TOML INSTEAD.\n${stringify(
|
||||
config as JsonMap
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Merges two config objects together, with
|
||||
* the latter configs' values taking precedence
|
||||
* @param configs
|
||||
* @returns
|
||||
*/
|
||||
private mergeConfigs<T = ConfigType>(...configs: T[]) {
|
||||
return merge(configs) as T;
|
||||
}
|
||||
}
|
||||
6
packages/config-manager/package.json
Normal file
6
packages/config-manager/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "config-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
96
packages/config-manager/tests/config-manager.test.ts
Normal file
96
packages/config-manager/tests/config-manager.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/config-manager/config-manager.test.ts
|
||||
import { stringify } from "@iarna/toml";
|
||||
import { ConfigManager } from "..";
|
||||
import { describe, beforeEach, spyOn, it, expect } from "bun:test";
|
||||
|
||||
describe("ConfigManager", () => {
|
||||
let configManager: ConfigManager;
|
||||
|
||||
beforeEach(() => {
|
||||
configManager = new ConfigManager({
|
||||
configPathOverride: "./config/config.toml",
|
||||
internalConfigPathOverride: "./config/config.internal.toml",
|
||||
});
|
||||
});
|
||||
|
||||
it("should get the correct config path", () => {
|
||||
expect(configManager.getConfigPath()).toEqual("./config/config.toml");
|
||||
});
|
||||
|
||||
it("should get the correct internal config path", () => {
|
||||
expect(configManager.getInternalConfigPath()).toEqual(
|
||||
"./config/config.internal.toml"
|
||||
);
|
||||
});
|
||||
|
||||
it("should read the config file correctly", async () => {
|
||||
const mockConfig = { key: "value" };
|
||||
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () =>
|
||||
new Promise(resolve => {
|
||||
resolve(true);
|
||||
}),
|
||||
text: () =>
|
||||
new Promise(resolve => {
|
||||
resolve(stringify(mockConfig));
|
||||
}),
|
||||
}));
|
||||
|
||||
const config = await configManager.getConfig<typeof mockConfig>();
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it("should read the internal config file correctly", async () => {
|
||||
const mockConfig = { key: "value" };
|
||||
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () =>
|
||||
new Promise(resolve => {
|
||||
resolve(true);
|
||||
}),
|
||||
text: () =>
|
||||
new Promise(resolve => {
|
||||
resolve(stringify(mockConfig));
|
||||
}),
|
||||
}));
|
||||
|
||||
const config =
|
||||
// @ts-expect-error Force call private function for testing
|
||||
await configManager.readInternalConfig<typeof mockConfig>();
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it("should write to the internal config file correctly", async () => {
|
||||
const mockConfig = { key: "value" };
|
||||
|
||||
spyOn(Bun, "write").mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolve(10);
|
||||
})
|
||||
);
|
||||
|
||||
await configManager.writeConfig(mockConfig);
|
||||
});
|
||||
|
||||
it("should merge configs correctly", () => {
|
||||
const config1 = { key1: "value1", key2: "value2" };
|
||||
const config2 = { key2: "newValue2", key3: "value3" };
|
||||
// @ts-expect-error Force call private function for testing
|
||||
const mergedConfig = configManager.mergeConfigs<Record<string, string>>(
|
||||
config1,
|
||||
config2
|
||||
);
|
||||
|
||||
expect(mergedConfig).toEqual({
|
||||
key1: "value1",
|
||||
key2: "newValue2",
|
||||
key3: "value3",
|
||||
});
|
||||
});
|
||||
});
|
||||
170
packages/request-parser/index.ts
Normal file
170
packages/request-parser/index.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* RequestParser
|
||||
* @file index.ts
|
||||
* @module request-parser
|
||||
* @description Parses Request object into a JavaScript object based on the content type
|
||||
*/
|
||||
|
||||
/**
|
||||
* RequestParser
|
||||
* Parses Request object into a JavaScript object
|
||||
* based on the Content-Type header
|
||||
* @param request Request object
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
export class RequestParser {
|
||||
constructor(public request: Request) {}
|
||||
|
||||
/**
|
||||
* Parse request body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
async toObject<T>() {
|
||||
try {
|
||||
switch (await this.determineContentType()) {
|
||||
case "application/json":
|
||||
return this.parseJson<T>();
|
||||
case "application/x-www-form-urlencoded":
|
||||
return this.parseFormUrlencoded<T>();
|
||||
case "multipart/form-data":
|
||||
return this.parseFormData<T>();
|
||||
default:
|
||||
return this.parseQuery<T>();
|
||||
}
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine body content type
|
||||
* If there is no Content-Type header, automatically
|
||||
* guess content type. Cuts off after ";" character
|
||||
* @returns Content-Type header value, or empty string if there is no body
|
||||
* @throws Error if body is invalid
|
||||
* @private
|
||||
*/
|
||||
private async determineContentType() {
|
||||
if (this.request.headers.get("Content-Type")) {
|
||||
return (
|
||||
this.request.headers.get("Content-Type")?.split(";")[0] ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
// Check if body is valid JSON
|
||||
try {
|
||||
await this.request.json();
|
||||
return "application/json";
|
||||
} catch {
|
||||
// This is not JSON
|
||||
}
|
||||
|
||||
// Check if body is valid FormData
|
||||
try {
|
||||
await this.request.formData();
|
||||
return "multipart/form-data";
|
||||
} catch {
|
||||
// This is not FormData
|
||||
}
|
||||
|
||||
if (this.request.body) {
|
||||
throw new Error("Invalid body");
|
||||
}
|
||||
|
||||
// If there is no body, return query parameters
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse FormData body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
result[key as keyof T] = value as any;
|
||||
} else if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseJson<T>(): Promise<Partial<T>> {
|
||||
try {
|
||||
return (await this.request.json()) as T;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query parameters into a JavaScript object
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
private parseQuery<T>(): Partial<T> {
|
||||
const result: Partial<T> = {};
|
||||
const url = new URL(this.request.url);
|
||||
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
(result[arrayKey] as string[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
6
packages/request-parser/package.json
Normal file
6
packages/request-parser/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "request-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
158
packages/request-parser/tests/request-parser.test.ts
Normal file
158
packages/request-parser/tests/request-parser.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { describe, it, expect, test } from "bun:test";
|
||||
import { RequestParser } from "..";
|
||||
|
||||
describe("RequestParser", () => {
|
||||
describe("Should parse query parameters correctly", () => {
|
||||
test("With text parameters", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
test("With Array", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?test[]=value1&test[]=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result.test).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
test("With both at once", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result).toEqual({
|
||||
param1: "value1",
|
||||
param2: "value2",
|
||||
test: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse JSON body correctly", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ param1: "value1", param2: "value2" }),
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
it("should handle invalid JSON body", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "invalid json",
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
describe("should parse form data correctly", () => {
|
||||
test("With basic text parameters", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("param1", "value1");
|
||||
formData.append("param2", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
test("With File object", async () => {
|
||||
const file = new File(["content"], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
file: File;
|
||||
}>();
|
||||
expect(result.file).toBeInstanceOf(File);
|
||||
expect(await result.file?.text()).toEqual("content");
|
||||
});
|
||||
|
||||
test("With Array", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("test[]", "value1");
|
||||
formData.append("test[]", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result.test).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
test("With all three at once", async () => {
|
||||
const file = new File(["content"], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("param1", "value1");
|
||||
formData.append("param2", "value2");
|
||||
formData.append("file", file);
|
||||
formData.append("test[]", "value1");
|
||||
formData.append("test[]", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
file: File;
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result).toEqual({
|
||||
param1: "value1",
|
||||
param2: "value2",
|
||||
file: file,
|
||||
test: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
|
||||
test("URL Encoded", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "param1=value1¶m2=value2",
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { getConfig } from "~classes/configmanager";
|
||||
import { parseRequest } from "@request";
|
||||
import { jsonResponse } from "@response";
|
||||
import { tempmailDomains } from "@tempmail";
|
||||
import { applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { createNewLocalUser } from "~database/entities/User";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import type { RouteHandler } from "~server/api/routes.type";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -19,20 +19,17 @@ export const meta = applyConfig({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
const handler: RouteHandler<{
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
agreement: boolean;
|
||||
locale: string;
|
||||
reason: string;
|
||||
}> = async (req, matchedRoute, extraData) => {
|
||||
// TODO: Add Authorization check
|
||||
|
||||
const body = await parseRequest<{
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
agreement: boolean;
|
||||
locale: string;
|
||||
reason: string;
|
||||
}>(req);
|
||||
const body = extraData.parsedRequest;
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
|
@ -94,8 +91,8 @@ export default async (req: Request): Promise<Response> => {
|
|||
|
||||
// Check if username doesnt match filters
|
||||
if (
|
||||
config.filters.username_filters.some(
|
||||
filter => body.username?.match(filter)
|
||||
config.filters.username_filters.some(filter =>
|
||||
body.username?.match(filter)
|
||||
)
|
||||
) {
|
||||
errors.details.username.push({
|
||||
|
|
@ -204,3 +201,8 @@ export default async (req: Request): Promise<Response> => {
|
|||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
export default handler;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import {
|
||||
getFromRequest,
|
||||
userRelations,
|
||||
userToAPI,
|
||||
} from "~database/entities/User";
|
||||
import { userRelations, userToAPI } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
import { parseRequest } from "@request";
|
||||
import { client } from "~database/datasource";
|
||||
import type { RouteHandler } from "~server/api/routes.type";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -20,10 +16,16 @@ export const meta = applyConfig({
|
|||
},
|
||||
});
|
||||
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
const handler: RouteHandler<{
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
}> = async (req, matchedRoute, extraData) => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
|
||||
const { user } = await getFromRequest(req);
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
@ -32,13 +34,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
limit = 40,
|
||||
offset,
|
||||
q,
|
||||
} = await parseRequest<{
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
}>(req);
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 80) {
|
||||
return errorResponse("Limit must be between 1 and 80", 400);
|
||||
|
|
@ -66,7 +62,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
ownerId: user.id,
|
||||
following,
|
||||
},
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
take: Number(limit),
|
||||
|
|
@ -76,3 +72,5 @@ export default async (req: Request): Promise<Response> => {
|
|||
|
||||
return jsonResponse(accounts.map(acct => userToAPI(acct)));
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { getConfig } from "~classes/configmanager";
|
||||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import {
|
||||
userRelations,
|
||||
userToAPI,
|
||||
type AuthData,
|
||||
} from "~database/entities/User";
|
||||
import { userRelations, userToAPI } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
|
|
@ -15,7 +10,7 @@ import { parseEmojis } from "~database/entities/Emoji";
|
|||
import { client } from "~database/datasource";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import { convertTextToHtml } from "@formatting";
|
||||
import type { MatchedRoute } from "bun";
|
||||
import type { RouteHandler } from "~server/api/routes.type";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["PATCH"],
|
||||
|
|
@ -29,15 +24,19 @@ export const meta = applyConfig({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patches a user
|
||||
*/
|
||||
export default async (
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute,
|
||||
auth: AuthData
|
||||
): Promise<Response> => {
|
||||
const { user } = auth;
|
||||
const handler: RouteHandler<{
|
||||
display_name: string;
|
||||
note: string;
|
||||
avatar: File;
|
||||
header: File;
|
||||
locked: string;
|
||||
bot: string;
|
||||
discoverable: string;
|
||||
"source[privacy]": string;
|
||||
"source[sensitive]": string;
|
||||
"source[language]": string;
|
||||
}> = async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
@ -54,18 +53,7 @@ export default async (
|
|||
"source[privacy]": source_privacy,
|
||||
"source[sensitive]": source_sensitive,
|
||||
"source[language]": source_language,
|
||||
} = await parseRequest<{
|
||||
display_name: string;
|
||||
note: string;
|
||||
avatar: File;
|
||||
header: File;
|
||||
locked: string;
|
||||
bot: string;
|
||||
discoverable: string;
|
||||
"source[privacy]": string;
|
||||
"source[sensitive]": string;
|
||||
"source[language]": string;
|
||||
}>(req);
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||
|
||||
|
|
@ -147,7 +135,7 @@ export default async (
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(user.source as any).privacy = source_privacy;
|
||||
user.source.privacy = source_privacy;
|
||||
}
|
||||
|
||||
if (source_sensitive && user.source) {
|
||||
|
|
@ -157,7 +145,7 @@ export default async (
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(user.source as any).sensitive = source_sensitive === "true";
|
||||
user.source.sensitive = source_sensitive === "true";
|
||||
}
|
||||
|
||||
if (source_language && user.source) {
|
||||
|
|
@ -169,7 +157,7 @@ export default async (
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(user.source as any).language = source_language;
|
||||
user.source.language = source_language;
|
||||
}
|
||||
|
||||
if (avatar) {
|
||||
|
|
@ -264,3 +252,5 @@ export default async (
|
|||
|
||||
return jsonResponse(userToAPI(output));
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { getFromRequest, userToAPI } from "~database/entities/User";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { applyConfig } from "@api";
|
||||
import type { RouteHandler } from "~server/api/routes.type";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -14,10 +15,12 @@ export const meta = applyConfig({
|
|||
},
|
||||
});
|
||||
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
const handler: RouteHandler<> = (req, matchedRoute, extraData) => {};
|
||||
|
||||
const handler: RouteHandler<""> = (req, matchedRoute, extraData) => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
|
||||
const { user } = await getFromRequest(req);
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
@ -25,3 +28,5 @@ export default async (req: Request): Promise<Response> => {
|
|||
...userToAPI(user, true),
|
||||
});
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
|||
13
server/api/routes.type.ts
Normal file
13
server/api/routes.type.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { MatchedRoute } from "bun";
|
||||
import type { ConfigManager } from "config-manager";
|
||||
import type { AuthData } from "~database/entities/User";
|
||||
|
||||
export type RouteHandler<T> = (
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute,
|
||||
extraData: {
|
||||
auth: AuthData;
|
||||
parsedRequest: Partial<T>;
|
||||
configManager: ConfigManager;
|
||||
}
|
||||
) => Response | Promise<Response>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* either FormData, query parameters, or JSON in the request
|
||||
* @param request The request to parse
|
||||
*/
|
||||
export async function parseRequest<T>(request: Request): Promise<Partial<T>> {
|
||||
/* export async function parseRequest<T>(request: Request): Promise<Partial<T>> {
|
||||
const query = new URL(request.url).searchParams;
|
||||
let output: Partial<T> = {};
|
||||
|
||||
|
|
@ -93,3 +93,4 @@ export async function parseRequest<T>(request: Request): Promise<Partial<T>> {
|
|||
|
||||
return output;
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue