diff --git a/src/download.html b/src/download.html index 80a2093..dd0847a 100644 --- a/src/download.html +++ b/src/download.html @@ -19,7 +19,7 @@ - + ) ) -// serve download page - -app.get("/download/:fileId", getAccount, preview(files)) - /* routes should be in this order: diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts index 647982b..7f7f266 100644 --- a/src/server/lib/mail.ts +++ b/src/server/lib/mail.ts @@ -11,8 +11,6 @@ let mailConfig = config.mail, }, }) -// lazy but - /** * @description Sends an email * @param to Target email address @@ -35,4 +33,4 @@ export function sendMail(to: string, subject: string, content: string) { `` )}

If you do not believe that you are the intended recipient of this email, please disregard this message.`, }) -} +} \ No newline at end of file diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts index 0d5ebe1..ab4ca43 100644 --- a/src/server/routes/api.ts +++ b/src/server/routes/api.ts @@ -74,7 +74,6 @@ export default class APIRouter { async loadAPIMethods() { let files = await readdir(APIDirectory) for (let version of files) { - /// temporary (hopefully). need to figure out something else for this let def = JSON.parse( ( await readFile( diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts index 4e561d7..d251b1f 100644 --- a/src/server/routes/api/v0/adminRoutes.ts +++ b/src/server/routes/api/v0/adminRoutes.ts @@ -31,12 +31,12 @@ export default function (files: Files) { typeof body.target !== "string" || typeof body.password !== "string" ) { - return ctx.status(404) + return ctx.text("not found", 404) } let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - return ctx.status(404) + return ctx.text("not found", 404) } Accounts.password.set(targetAccount.id, body.password) @@ -53,7 +53,7 @@ export default function (files: Files) { `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.` ) .then(() => ctx.text("OK")) - .catch(() => ctx.status(500)) + .catch(() => ctx.text("err while sending email", 500)) } }) @@ -62,12 +62,12 @@ export default function (files: Files) { let acc = ctx.get("account") as Accounts.Account if (typeof body.target !== "string") { - return ctx.status(404) + return ctx.text("not found", 404) } let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - return ctx.status(404) + return ctx.text("not found", 404) } Accounts.save() @@ -77,32 +77,31 @@ export default function (files: Files) { adminRoutes.post("/delete", async (ctx) => { const body = await ctx.req.json() if (typeof body.target !== "string") { - return ctx.status(404) + return ctx.text("not found", 404) } let targetFile = files.files[body.target] if (!targetFile) { - return ctx.status(404) + return ctx.text("not found", 404) } return files .unlink(body.target) - .then(() => ctx.status(200)) - .catch(() => ctx.status(500)) - .finally(() => ctx.status(200)) + .then(() => ctx.text("ok", 200)) + .catch(() => ctx.text("err", 500)) }) adminRoutes.post("/delete_account", async (ctx) => { let acc = ctx.get("account") as Accounts.Account const body = await ctx.req.json() if (typeof body.target !== "string") { - return ctx.status(404) + return ctx.text("not found", 404) } let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - return ctx.status(404) + return ctx.text("not found", 404) } let accId = targetAccount.id @@ -149,12 +148,12 @@ export default function (files: Files) { adminRoutes.post("/transfer", async (ctx) => { const body = await ctx.req.json() if (typeof body.target !== "string" || typeof body.owner !== "string") { - return ctx.status(404) + return ctx.text("not found", 404) } let targetFile = files.files[body.target] if (!targetFile) { - return ctx.status(404) + return ctx.text("not found", 404) } let newOwner = Accounts.getFromUsername(body.owner || "") @@ -173,21 +172,21 @@ export default function (files: Files) { } targetFile.owner = newOwner ? newOwner.id : undefined - files + return files .write() - .then(() => ctx.status(200)) - .catch(() => ctx.status(500)) + .then(() => ctx.text("ok", 200)) + .catch(() => ctx.text("error", 500)) }) adminRoutes.post("/idchange", async (ctx) => { const body = await ctx.req.json() if (typeof body.target !== "string" || typeof body.new !== "string") { - return ctx.status(400) + return ctx.text("inappropriate body", 400) } let targetFile = files.files[body.target] if (!targetFile) { - return ctx.status(404) + return ctx.text("not found", 404) } if (files.files[body.new]) { diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index b44b717..4bb3726 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -105,7 +105,6 @@ export default function (files: Files) { } } ) - // upload handlers primaryApi.post( "/upload", diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index cf3b36a..5b2da93 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -33,28 +33,54 @@ type Message = [200 | 400 | 401 | 403 | 501, string] // there's probably a less stupid way to do this than `K in keyof Pick` // @Jack5079 make typings better if possible + +type Validator, ValueNotNull extends boolean> = + /** + * @param actor The account performing this action + * @param target The target account for this action + * @param params Changes being patched in by the user + */ + (actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? { + [K in keyof Pick]-? : UserUpdateParameters[K] + } : {})) => Accounts.Account[T] | Message + +// this type is so stupid stg +interface ValidatorWithSettings> { + acceptsNull?: boolean, + validator: Validator // i give upp ill fix this later +} + const validators: { [T in keyof Partial]: - /** - * @param actor The account performing this action - * @param target The target account for this action - * @param params Changes being patched in by the user - */ - (actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & { - [K in keyof Pick]-? : UserUpdateParameters[K] - }) => Accounts.Account[T] | Message + Validator | ValidatorWithSettings } = { defaultFileVisibility(actor, target, params) { if (["public", "private", "anonymous"].includes(params.defaultFileVisibility)) return params.defaultFileVisibility else return [400, "invalid file visibility"] }, - email(actor, target, params) { - return [501, "not implemented"] + email: { + acceptsNull: true, + validator: (actor, target, params) => { + if (!params.currentPassword // actor on purpose here to allow admins + || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))) + return [401, "current password incorrect"] + + if (!params.email) { + if (target.email) { + sendMail( + target.email, + `Email disconnected`, + `Hello there! Your email address (${target.email}) has been disconnected from the monofile account ${target.username}. Thank you for using monofile.` + ).catch() + } + return undefined + } + } }, password(actor, target, params) { if ( - !params.currentPassword + !params.currentPassword // actor on purpose here to allow admins || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)) ) return [401, "current password incorrect"] @@ -77,7 +103,7 @@ const validators: { }, username(actor, target, params) { - if (!params.currentPassword + if (!params.currentPassword // actor on purpose here to allow admins || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))) return [401, "current password incorrect"] @@ -106,13 +132,30 @@ const validators: { return params.username }, - customCSS(actor, target, params) { - if ( - !params.customCSS || - (params.customCSS.match(id_check_regex)?.[0] == params.customCSS && - params.customCSS.length <= Configuration.maxUploadIdLength) - ) return params.customCSS - else return [400, "bad file id"] + customCSS: { + acceptsNull: true, + validator: (actor, target, params) => { + if ( + !params.customCSS || + (params.customCSS.match(id_check_regex)?.[0] == params.customCSS && + params.customCSS.length <= Configuration.maxUploadIdLength) + ) return params.customCSS + else return [400, "bad file id"] + } + }, + embed(actor, target, params) { + if (params.embed.color === undefined) { + params.embed.color = target.embed?.color + } else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] == + params.embed.color.toLowerCase() && + params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"] + + + if (params.embed.largeImage === undefined) { + params.embed.largeImage = target.embed?.largeImage + } else params.embed.largeImage = Boolean(params.embed.largeImage) + + return params.embed }, admin(actor, target, params) { if (actor.admin && !target.admin) return params.admin @@ -217,24 +260,41 @@ export default function (files: Files) { if (Array.isArray(body)) return ServeError(ctx, 400, "invalid body") - let results: [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]|Message][] = Object.entries(body).filter(e => e[1] && e[0] !== "currentPassword").map(([x]) => - [ - x as keyof Accounts.Account, - x in validators - ? validators[x as keyof Accounts.Account]!(actor, target, body as any) - : [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message - ] - ) + let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] = + (Object.entries(body) + .filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][]) + .map(([x, v]) => { + if (!validators[x]) + return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message - let allMsgs = results.map(([x,v]) => { + let validator = + (typeof validators[x] == "object" + ? validators[x] + : { + validator: validators[x] as Validator, + acceptsNull: false + }) as ValidatorWithSettings + + if (!validator.acceptsNull && !v) + return [400, `the ${x} validator does not accept null values`] as Message + + return [ + x, + validator.validator(actor, target, body) + ] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]] + }) + + let allMsgs = results.map((v) => { if (isMessage(v)) return v - target[x] = v as never // lol + target[v[0]] = v[1] as never // lol return [200, "OK"] as Message }) + await Accounts.save() + if (allMsgs.length == 1) - return ctx.body(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry + return ctx.text(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry else return ctx.json(allMsgs) } ) @@ -264,7 +324,33 @@ export default function (files: Files) { return ctx.text("account deleted") }) - router.get("/:user") + router.get("/:user", requiresAccount, async (ctx) => { + let acc = ctx.get("target") + let sessionToken = auth.tokenFor(ctx)! + + return ctx.json({ + ...acc, + password: undefined, + email: + auth.getType(sessionToken) == "User" || + auth.getPermissions(sessionToken)?.includes("email") + ? acc.email + : undefined, + activeSessions: auth.AuthTokens.filter( + (e) => + e.type != "App" && + e.account == acc.id && + (e.expire > Date.now() || !e.expire) + ).length, + }) + }) + + router.get("/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/v1/session.ts b/src/server/routes/api/v1/session.ts index 3299282..a0a0c15 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -11,6 +11,7 @@ import * as Accounts from "../../../lib/accounts.js" import * as auth from "../../../lib/auth.js" import { getAccount, + requiresAccount } from "../../../lib/middleware.js" import ServeError from "../../../lib/errors.js" @@ -53,6 +54,15 @@ export default function (files: Files) { ctx.status(200) }) + router.get("/", requiresAccount, ctx => { + let sessionToken = auth.tokenFor(ctx) + return ctx.json({ + expiry: auth.AuthTokens.find( + (e) => e.token == sessionToken + )?.expire, + }) + }) + router.delete("/", (ctx) => { if (!auth.validate(getCookie(ctx, "auth")!)) { return ServeError(ctx, 401, "not logged in") diff --git a/src/server/routes/api/web/api.json b/src/server/routes/api/web/api.json new file mode 100644 index 0000000..b517aca --- /dev/null +++ b/src/server/routes/api/web/api.json @@ -0,0 +1,7 @@ +{ + "name": "web", + "baseURL": "/", + "mount": [ + { "file": "preview", "to": "/download" } + ] +} \ No newline at end of file diff --git a/src/server/routes/preview.ts b/src/server/routes/api/web/preview.ts similarity index 91% rename from src/server/routes/preview.ts rename to src/server/routes/api/web/preview.ts index dfdd763..909f4cf 100644 --- a/src/server/routes/preview.ts +++ b/src/server/routes/api/web/preview.ts @@ -1,13 +1,18 @@ import fs from "fs/promises" import bytes from "bytes" -import ServeError from "../lib/errors.js" -import * as Accounts from "../lib/accounts.js" -import type { Handler } from "hono" -import type Files from "../lib/files.js" -import pkg from "../../../package.json" assert {type:"json"} +import ServeError from "../../../lib/errors.js" +import * as Accounts from "../../../lib/accounts.js" +import type Files from "../../../lib/files.js" +import pkg from "../../../../../package.json" assert {type:"json"} +import { Hono } from "hono" +export let router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() -export default (files: Files): Handler => - async (ctx) => { +export default function (files: Files) { + router.get("/:fileId", async (ctx) => { let acc = ctx.get("account") as Accounts.Account const fileId = ctx.req.param("fileId") const host = ctx.req.header("Host") @@ -104,4 +109,7 @@ export default (files: Files): Handler => } else { ServeError(ctx, 404, "file not found") } - } + }) + + return router +} \ No newline at end of file