mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-25 07:06:25 -08:00
hhhh
This commit is contained in:
parent
86f5727d83
commit
00fcf4580f
|
@ -1,6 +1,7 @@
|
||||||
import { createTransport } from "nodemailer"
|
import { createTransport } from "nodemailer"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import config from "../../../config.json" assert {type:"json"}
|
import config from "../../../config.json" assert {type:"json"}
|
||||||
|
import { generateFileId } from "./files.js"
|
||||||
|
|
||||||
let mailConfig = config.mail,
|
let mailConfig = config.mail,
|
||||||
transport = createTransport({
|
transport = createTransport({
|
||||||
|
@ -34,3 +35,63 @@ export function sendMail(to: string, subject: string, content: string) {
|
||||||
)}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard this message.</span>`,
|
)}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard this message.</span>`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace CodeMgr {
|
||||||
|
|
||||||
|
export const Intents = [
|
||||||
|
"verifyEmail",
|
||||||
|
"recoverAccount"
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type Intent = typeof Intents[number]
|
||||||
|
|
||||||
|
export function isIntent(intent: string): intent is Intent { return intent in Intents }
|
||||||
|
|
||||||
|
export let codes = Object.fromEntries(
|
||||||
|
Intents.map(e => [
|
||||||
|
e,
|
||||||
|
{byId: new Map<string, Code>(), byUser: new Map<string, Code[]>()}
|
||||||
|
])) as Record<Intent, { byId: Map<string, Code>, byUser: Map<string, Code[]> }>
|
||||||
|
|
||||||
|
// this is stupid whyd i write this
|
||||||
|
|
||||||
|
export class Code {
|
||||||
|
readonly id: string = generateFileId(12)
|
||||||
|
readonly for: string
|
||||||
|
|
||||||
|
readonly intent: Intent
|
||||||
|
|
||||||
|
readonly expiryClear: NodeJS.Timeout
|
||||||
|
|
||||||
|
readonly data: any
|
||||||
|
|
||||||
|
constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) {
|
||||||
|
this.for = forUser;
|
||||||
|
this.intent = intent
|
||||||
|
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
||||||
|
this.data = data
|
||||||
|
|
||||||
|
codes[intent].byId.set(this.id, this);
|
||||||
|
|
||||||
|
let byUser = codes[intent].byUser.get(this.for)
|
||||||
|
if (!byUser) {
|
||||||
|
byUser = []
|
||||||
|
codes[intent].byUser.set(this.for, byUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
byUser.push(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
codes[this.intent].byId.delete(this.id);
|
||||||
|
let bu = codes[this.intent].byUser.get(this.id)!
|
||||||
|
bu.splice(bu.indexOf(this), 1)
|
||||||
|
clearTimeout(this.expiryClear)
|
||||||
|
}
|
||||||
|
|
||||||
|
check(forUser: string) {
|
||||||
|
return forUser === this.for
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import * as Accounts from "./accounts.js"
|
import * as Accounts from "./accounts.js"
|
||||||
import { Handler as RequestHandler } from "hono"
|
import type { Context, Handler as RequestHandler } from "hono"
|
||||||
import ServeError from "../lib/errors.js"
|
import ServeError from "../lib/errors.js"
|
||||||
import * as auth from "./auth.js"
|
import * as auth from "./auth.js"
|
||||||
|
import { setCookie } from "hono/cookie"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware which adds an account, if any, to ctx.get("account")
|
* @description Middleware which adds an account, if any, to ctx.get("account")
|
||||||
|
@ -92,6 +93,15 @@ export const assertAPI = function (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not really middleware but a utility
|
||||||
|
|
||||||
|
export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
||||||
|
path: "/",
|
||||||
|
sameSite: "Strict",
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true
|
||||||
|
})
|
||||||
|
|
||||||
type SchemeType = "array" | "object" | "string" | "number" | "boolean"
|
type SchemeType = "array" | "object" | "string" | "number" | "boolean"
|
||||||
|
|
||||||
interface SchemeObject {
|
interface SchemeObject {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Modules
|
// Modules
|
||||||
|
|
||||||
|
|
||||||
import { Hono } from "hono"
|
import { type Context, Hono } from "hono"
|
||||||
import { getCookie, setCookie } from "hono/cookie"
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
@ -12,12 +12,13 @@ import * as auth from "../../../lib/auth.js"
|
||||||
import {
|
import {
|
||||||
assertAPI,
|
assertAPI,
|
||||||
getAccount,
|
getAccount,
|
||||||
|
login,
|
||||||
noAPIAccess,
|
noAPIAccess,
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../../lib/middleware.js"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
import { sendMail } from "../../../lib/mail.js"
|
import { CodeMgr, sendMail } from "../../../lib/mail.js"
|
||||||
|
|
||||||
import Configuration from "../../../../../config.json" assert {type:"json"}
|
import Configuration from "../../../../../config.json" assert {type:"json"}
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ const router = new Hono<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
|
type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
|
||||||
type Message = [200 | 400 | 401 | 403 | 501, string]
|
type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
|
||||||
|
|
||||||
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
||||||
// @Jack5079 make typings better if possible
|
// @Jack5079 make typings better if possible
|
||||||
|
@ -42,12 +43,15 @@ type Validator<T extends keyof Partial<Accounts.Account>, ValueNotNull extends b
|
||||||
*/
|
*/
|
||||||
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? {
|
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? {
|
||||||
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
|
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
|
||||||
} : {})) => Accounts.Account[T] | Message
|
} : {}), ctx: Context) => Accounts.Account[T] | Message
|
||||||
|
|
||||||
// this type is so stupid stg
|
// this type is so stupid stg
|
||||||
interface ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> {
|
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> = {
|
||||||
acceptsNull?: boolean,
|
acceptsNull: true,
|
||||||
validator: Validator<T, this["acceptsNull"] extends true ? true : false> // i give upp ill fix this later
|
validator: Validator<T, false>
|
||||||
|
} | {
|
||||||
|
acceptsNull?: false,
|
||||||
|
validator: Validator<T, true>
|
||||||
}
|
}
|
||||||
|
|
||||||
const validators: {
|
const validators: {
|
||||||
|
@ -61,7 +65,7 @@ const validators: {
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
acceptsNull: true,
|
acceptsNull: true,
|
||||||
validator: (actor, target, params) => {
|
validator: (actor, target, params, ctx) => {
|
||||||
if (!params.currentPassword // actor on purpose here to allow admins
|
if (!params.currentPassword // actor on purpose here to allow admins
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
||||||
return [401, "current password incorrect"]
|
return [401, "current password incorrect"]
|
||||||
|
@ -76,6 +80,34 @@ const validators: {
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof params.email !== "string") return [400, "email must be string"]
|
||||||
|
if (actor.admin)
|
||||||
|
return params.email
|
||||||
|
|
||||||
|
// send verification email
|
||||||
|
|
||||||
|
if ((CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length || 0) >= 2) return [429, "you have too many active codes"]
|
||||||
|
|
||||||
|
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
|
||||||
|
|
||||||
|
sendMail(
|
||||||
|
params.email,
|
||||||
|
`Hey there, ${target.username} - let's connect your email`,
|
||||||
|
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
|
||||||
|
params.email.split("@")[0]
|
||||||
|
}<span style="opacity:0.5">@${
|
||||||
|
params.email.split("@")[1]
|
||||||
|
}</span></span>, to your account, <span username>${
|
||||||
|
target.username
|
||||||
|
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/go/verify/${code.id}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/go/verify/${code.id}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return [200, "please check your inbox"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
password(actor, target, params) {
|
password(actor, target, params) {
|
||||||
|
@ -144,6 +176,7 @@ const validators: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
embed(actor, target, params) {
|
embed(actor, target, params) {
|
||||||
|
if (typeof params.embed !== "object") return [400, "must use an object for embed"]
|
||||||
if (params.embed.color === undefined) {
|
if (params.embed.color === undefined) {
|
||||||
params.embed.color = target.embed?.color
|
params.embed.color = target.embed?.color
|
||||||
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
||||||
|
@ -236,13 +269,8 @@ export default function (files: Files) {
|
||||||
|
|
||||||
return Accounts.create(body.username, body.password)
|
return Accounts.create(body.username, body.password)
|
||||||
.then((account) => {
|
.then((account) => {
|
||||||
setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
login(ctx, account)
|
||||||
path: "/",
|
return ctx.text("logged in")
|
||||||
sameSite: "Strict",
|
|
||||||
secure: true,
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
return ctx.status(200)
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return ServeError(ctx, 500, "internal server error")
|
return ServeError(ctx, 500, "internal server error")
|
||||||
|
@ -280,7 +308,7 @@ export default function (files: Files) {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
x,
|
x,
|
||||||
validator.validator(actor, target, body)
|
validator.validator(actor, target, body as any, ctx)
|
||||||
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
|
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import * as Accounts from "../../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../../lib/auth.js"
|
||||||
import {
|
import {
|
||||||
getAccount,
|
getAccount,
|
||||||
|
login,
|
||||||
requiresAccount
|
requiresAccount
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../../lib/middleware.js"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
|
@ -45,13 +46,9 @@ export default function (files: Files) {
|
||||||
ServeError(ctx, 400, "username or password incorrect")
|
ServeError(ctx, 400, "username or password incorrect")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setCookie(ctx, "auth", auth.create(account.id, 3 * 24 * 60 * 60 * 1000), {
|
|
||||||
path: "/",
|
login(ctx, account.id)
|
||||||
sameSite: "Strict",
|
return ctx.text("logged in")
|
||||||
secure: true,
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
ctx.status(200)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/", requiresAccount, ctx => {
|
router.get("/", requiresAccount, ctx => {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"baseURL": "/",
|
"baseURL": "/",
|
||||||
"mount": [
|
"mount": [
|
||||||
{ "file": "preview", "to": "/download" }
|
{ "file": "preview", "to": "/download" },
|
||||||
|
"go"
|
||||||
]
|
]
|
||||||
}
|
}
|
41
src/server/routes/api/web/go.ts
Normal file
41
src/server/routes/api/web/go.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import bytes from "bytes"
|
||||||
|
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 { CodeMgr } from "../../../lib/mail.js"
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import { getAccount, login } from "../../../lib/middleware.js"
|
||||||
|
export let router = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export default function (files: Files) {
|
||||||
|
router.get("/verify/:code", getAccount, async (ctx) => {
|
||||||
|
let currentAccount = ctx.get("account")
|
||||||
|
let code = CodeMgr.codes.verifyEmail.byId.get(ctx.req.param("code"))
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
if (currentAccount != undefined && !code.check(currentAccount.id)) {
|
||||||
|
return ServeError(ctx, 403, "you are logged in on a different account")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentAccount) {
|
||||||
|
login(ctx, code.for)
|
||||||
|
let ac = Accounts.getFromId(code.for)
|
||||||
|
if (ac) currentAccount = ac
|
||||||
|
else return ServeError(ctx, 401, "could not locate account")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAccount.email = code.data
|
||||||
|
await Accounts.save()
|
||||||
|
|
||||||
|
return ctx.redirect('/')
|
||||||
|
} else return ServeError(ctx, 404, "code not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import * as Accounts from "../../../lib/accounts.js"
|
||||||
import type Files from "../../../lib/files.js"
|
import type Files from "../../../lib/files.js"
|
||||||
import pkg from "../../../../../package.json" assert {type:"json"}
|
import pkg from "../../../../../package.json" assert {type:"json"}
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
import { getAccount } from "../../../lib/middleware.js"
|
||||||
export let router = new Hono<{
|
export let router = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
account: Accounts.Account
|
account: Accounts.Account
|
||||||
|
@ -12,7 +13,7 @@ export let router = new Hono<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
export default function (files: Files) {
|
export default function (files: Files) {
|
||||||
router.get("/:fileId", async (ctx) => {
|
router.get("/:fileId", getAccount, async (ctx) => {
|
||||||
let acc = ctx.get("account") as Accounts.Account
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
const fileId = ctx.req.param("fileId")
|
const fileId = ctx.req.param("fileId")
|
||||||
const host = ctx.req.header("Host")
|
const host = ctx.req.header("Host")
|
||||||
|
|
Loading…
Reference in a new issue