diff --git a/src/server/lib/DiscordAPI/index.ts b/src/server/lib/DiscordAPI/index.ts index 0d3decb..1694da5 100644 --- a/src/server/lib/DiscordAPI/index.ts +++ b/src/server/lib/DiscordAPI/index.ts @@ -7,7 +7,7 @@ import type { Configuration } from "../config.js" const EXPIRE_AFTER = 20 * 60 * 1000 const DISCORD_EPOCH = 1420070400000 // Converts a snowflake ID string into a JS Date object using the provided epoch (in ms), or Discord's epoch if not provided -function convertSnowflakeToDate( +export function convertSnowflakeToDate( snowflake: string | number, epoch = DISCORD_EPOCH ) { diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index efbbe03..7cd1386 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -2,12 +2,14 @@ import crypto from "crypto" import * as auth from "./auth.js"; import { readFile, writeFile } from "fs/promises" import { FileVisibility } from "./files.js"; +import { AccountSchemas } from "./schemas/index.js"; +import { z } from "zod" // this is probably horrible // but i don't even care anymore export let Accounts: Account[] = [] - +/* export interface Account { id : string username : string @@ -25,7 +27,9 @@ export interface Account { color? : string largeImage? : boolean } -} +}*/ + +export type Account = z.infer /** * @description Create a new account. @@ -45,7 +49,8 @@ export async function create(username:string,pwd:string,admin:boolean=false):Pro password: password.hash(pwd), files: [], admin: admin, - defaultFileVisibility: "public" + defaultFileVisibility: "public", + settings: AccountSchemas.Settings.User.parse({}) } ) diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index bd38a4c..3ada911 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises" import { Readable, Writable } from "node:stream" import crypto from "node:crypto" import { files } from "./accounts.js" -import { Client as API } from "./DiscordAPI/index.js" +import { Client as API, convertSnowflakeToDate } from "./DiscordAPI/index.js" import type { APIAttachment } from "discord-api-types/v10" import config, { Configuration } from "./config.js" import "dotenv/config" @@ -624,28 +624,37 @@ export default class Files { } /** - * @description Update a file from monofile 1.2 to allow for range requests with Content-Length to that file. + * @description Update a file from monofile 1.x to 2.x * @param uploadId Target file's ID */ async update(uploadId: string) { let target_file = this.files[uploadId] - let attachment_sizes = [] + let attachments: APIAttachment[] = [] for (let message of target_file.messageids) { let attachments = (await this.api.fetchMessage(message)).attachments for (let attachment of attachments) { - attachment_sizes.push(attachment.size) + attachments.push(attachment) } } if (!target_file.sizeInBytes) - target_file.sizeInBytes = attachment_sizes.reduce( - (a, b) => a + b, + target_file.sizeInBytes = attachments.reduce( + (a, b) => a + b.size, 0 ) - if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0] + if (!target_file.chunkSize) target_file.chunkSize = attachments[0].size + + if (!target_file.lastModified) target_file.lastModified = convertSnowflakeToDate(target_file.messageids[target_file.messageids.length-1]).getTime() + + // this feels like needlessly heavy + // we should probably just do this in an actual readFile idk + if (!target_file.md5) { + let hash = crypto.createHash("md5"); + (await this.readFileStream(uploadId)).pipe(hash).once("end", () => target_file.md5 = hash.digest("hex")) + } } /** diff --git a/src/server/lib/schemas/accounts.ts b/src/server/lib/schemas/accounts.ts index 4d85cc8..57b87ae 100644 --- a/src/server/lib/schemas/accounts.ts +++ b/src/server/lib/schemas/accounts.ts @@ -1,5 +1,5 @@ import {z} from "zod" -import { FileVisibility } from "./files.js" +import { FileId, FileVisibility } from "./files.js" export const StringPassword = z.string().min(8,"password must be at least 8 characters") export const Password = @@ -8,7 +8,52 @@ export const Password = salt: z.string() }) export const Username = - z.string().min(3, "username too short").max(20, "username too long").regex(/[A-Za-z0-9_\-\.]+/, "username contains invalid characters") + z.string() + .min(3, "username too short") + .max(20, "username too long") + .regex(/^[A-Za-z0-9_\-\.]+$/, "username contains invalid characters") + +export namespace Settings { + export const Theme = z.discriminatedUnion("theme", [ + z.object({ + theme: z.literal("catppuccin"), + variant: z.enum(["latte","frappe","macchiato","mocha","adaptive"]), + accent: z.enum([ + "rosewater", + "flamingo", + "pink", + "mauve", + "red", + "maroon", + "peach", + "yellow", + "green", + "teal", + "sky", + "sapphire", + "blue", + "lavender" + ]) + }), + z.object({ + theme: z.literal("custom"), + id: FileId + }) + ]) + export const BarSide = z.enum(["top","left","bottom","right"]) + export const Interface = z.object({ + theme: Theme.default({theme: "catppuccin", variant: "adaptive", accent: "sky"}), + barSide: BarSide.default("left") + }) + export const Links = z.object({ + color: z.string().toLowerCase().length(6).regex(/^[a-f0-9]+$/,"illegal characters").optional(), + largeImage: z.boolean().default(false) + }) + export const User = z.object({ + interface: Interface.default({}), links: Links.default({}) + }) +} + export const Account = z.object({ id: z.string(), @@ -17,5 +62,7 @@ export const Account = password: Password, files: z.array(z.string()), admin: z.boolean(), - defaultFileVisibility: FileVisibility + defaultFileVisibility: FileVisibility, + + settings: Settings.User }) \ No newline at end of file diff --git a/src/server/lib/schemas/files.ts b/src/server/lib/schemas/files.ts index 453e2ac..63800b9 100644 --- a/src/server/lib/schemas/files.ts +++ b/src/server/lib/schemas/files.ts @@ -2,7 +2,7 @@ import {z} from "zod" import config from "../config.js" export const FileId = z.string() - .regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/,"file ID uses invalid characters") + .regex(/^[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+$/,"file ID uses invalid characters") .max(config.maxUploadIdLength,"file ID too long") .min(1, "you... *need* a file ID") export const FileVisibility = z.enum(["public", "anonymous", "private"]) diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index e4ea5a5..1009b19 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -81,7 +81,7 @@ const validators: { } }, email: { - schema: AccountSchemas.Account.shape.email.optional(), + schema: AccountSchemas.Account.shape.email.nullable(), validator: (actor, target, params, ctx) => { if ( !params.currentPassword || // actor on purpose here to allow admins @@ -191,6 +191,23 @@ const validators: { else return [400, "cannot demote an admin"] } }, + settings: { + schema: AccountSchemas.Settings.User.partial(), + validator: (actor, target, params) => { + let base = AccountSchemas.Settings.User.default({}).parse(target.settings) + + let visit = (bse: Record, nw: Record) => { + for (let [key,value] of Object.entries(nw)) { + if (typeof value == "object") visit(bse[key], value) + else bse[key] = value + } + } + + visit(base, params.settings) + + return AccountSchemas.Settings.User.parse(base) // so that toLowerCase is called again... yeah that's it + } + }, } router.use(getAccount) @@ -246,8 +263,9 @@ export default function (files: Files) { login(ctx, account) return ctx.text("logged in") }) - .catch(() => { - return ServeError(ctx, 500, "internal server error") + .catch((e) => { + console.error(e) + return ServeError(ctx, 500, e instanceof z.ZodError ? issuesToMessage(e.issues) : "internal server error") }) }) @@ -354,11 +372,5 @@ export default function (files: Files) { }) }) - router.get("/:user/css", async (ctx) => { - let acc = ctx.get("account") - if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`) - else return ctx.text("") - }) - return router } diff --git a/src/server/routes/api/web/preview.ts b/src/server/routes/api/web/preview.ts index 052a2cc..38d12ad 100644 --- a/src/server/routes/api/web/preview.ts +++ b/src/server/routes/api/web/preview.ts @@ -75,18 +75,18 @@ export default function (files: Files) { ` : "") : "") + - (fileOwner?.embed?.largeImage && + (fileOwner?.settings?.links?.largeImage && file.visibility != "anonymous" && file.mime.startsWith("image/") ? `` : "") + `\n` )