mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 13:36:25 -08:00
initial commit
This commit is contained in:
parent
e4a4e24d6c
commit
8a26ace11f
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -26,7 +26,8 @@
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
|
"zod": "^3.23.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.6",
|
"@sveltejs/vite-plugin-svelte": "^2.4.6",
|
||||||
|
@ -1941,6 +1942,14 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
|
"zod": "^3.23.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.6",
|
"@sveltejs/vite-plugin-svelte": "^2.4.6",
|
||||||
|
|
|
@ -8,22 +8,23 @@ import config, { Configuration } from "./config.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
|
|
||||||
import * as Accounts from "./accounts.js"
|
import * as Accounts from "./accounts.js"
|
||||||
|
import { z } from "zod"
|
||||||
|
import * as schemas from "./schemas/files.js"
|
||||||
|
|
||||||
export let id_check_regex = /[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/
|
|
||||||
export let alphanum = Array.from(
|
export let alphanum = Array.from(
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bad solution but whatever
|
// bad solution but whatever
|
||||||
|
|
||||||
export type FileVisibility = "public" | "anonymous" | "private"
|
export type FileVisibility = z.infer<typeof schemas.FileVisibility>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Generates an alphanumeric string, used for files
|
* @description Generates an alphanumeric string, used for files
|
||||||
* @param length Length of the ID
|
* @param length Length of the ID
|
||||||
* @returns a random alphanumeric string
|
* @returns a random alphanumeric string
|
||||||
*/
|
*/
|
||||||
export function generateFileId(length: number = 5) {
|
export function generateFileId(length: number = 5): z.infer<typeof schemas.FileId> {
|
||||||
let fid = ""
|
let fid = ""
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
fid += alphanum[crypto.randomInt(0, alphanum.length)]
|
fid += alphanum[crypto.randomInt(0, alphanum.length)]
|
||||||
|
@ -31,35 +32,7 @@ export function generateFileId(length: number = 5) {
|
||||||
return fid
|
return fid
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export type FilePointer = z.infer<typeof schemas.FilePointer>
|
||||||
* @description Assert multiple conditions... this exists out of pure laziness
|
|
||||||
* @param conditions
|
|
||||||
*/
|
|
||||||
|
|
||||||
function multiAssert(
|
|
||||||
conditions: Map<boolean, { message: string; status: number }>
|
|
||||||
) {
|
|
||||||
for (let [cond, err] of conditions.entries()) {
|
|
||||||
if (cond) return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FileUploadSettings = Partial<Pick<FilePointer, "mime" | "owner">> &
|
|
||||||
Pick<FilePointer, "mime" | "filename"> & { uploadId?: string }
|
|
||||||
|
|
||||||
export interface FilePointer {
|
|
||||||
filename: string
|
|
||||||
mime: string
|
|
||||||
messageids: string[]
|
|
||||||
owner?: string
|
|
||||||
sizeInBytes?: number
|
|
||||||
tag?: string
|
|
||||||
visibility?: FileVisibility
|
|
||||||
reserved?: boolean
|
|
||||||
chunkSize?: number
|
|
||||||
lastModified?: number
|
|
||||||
md5?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StatusCodeError {
|
export interface StatusCodeError {
|
||||||
status: number
|
status: number
|
||||||
|
@ -470,9 +443,9 @@ export class UploadStream extends Writable {
|
||||||
sizeInBytes: this.filled,
|
sizeInBytes: this.filled,
|
||||||
visibility: ogf
|
visibility: ogf
|
||||||
? ogf.visibility
|
? ogf.visibility
|
||||||
: this.owner
|
: this.owner
|
||||||
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
&& Accounts.getFromId(this.owner)?.defaultFileVisibility
|
||||||
: undefined,
|
|| "public",
|
||||||
// so that json.stringify doesnt include tag:undefined
|
// so that json.stringify doesnt include tag:undefined
|
||||||
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
|
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
|
||||||
|
|
||||||
|
@ -527,12 +500,11 @@ export class UploadStream extends Writable {
|
||||||
return this.destroy(
|
return this.destroy(
|
||||||
new WebError(400, "duplicate attempt to set upload ID")
|
new WebError(400, "duplicate attempt to set upload ID")
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
!id ||
|
let check = schemas.FileId.safeParse(id);
|
||||||
id.match(id_check_regex)?.[0] != id ||
|
|
||||||
id.length > this.files.config.maxUploadIdLength
|
if (!check.success)
|
||||||
)
|
return this.destroy(new WebError(400, check.error.message))
|
||||||
return this.destroy(new WebError(400, "invalid file ID"))
|
|
||||||
|
|
||||||
if (this.files.files[id] && this.files.files[id].owner != this.owner)
|
if (this.files.files[id] && this.files.files[id].owner != this.owner)
|
||||||
return this.destroy(new WebError(403, "you don't own this file"))
|
return this.destroy(new WebError(403, "you don't own this file"))
|
||||||
|
@ -717,4 +689,4 @@ export default class Files {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ 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"
|
import { setCookie } from "hono/cookie"
|
||||||
|
import { ZodObject } from "zod"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware which adds an account, if any, to ctx.get("account")
|
* @description Middleware which adds an account, if any, to ctx.get("account")
|
||||||
|
@ -102,20 +103,10 @@ export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", a
|
||||||
httpOnly: true
|
httpOnly: true
|
||||||
})
|
})
|
||||||
|
|
||||||
type SchemeType = "array" | "object" | "string" | "number" | "boolean"
|
export const scheme = function(scheme: ZodObject<any>): RequestHandler {
|
||||||
|
return function(ctx, next) {
|
||||||
interface SchemeObject {
|
let chk = scheme.safeParse(ctx.req.json())
|
||||||
type: "object"
|
if (chk.success) next()
|
||||||
children: {
|
else ServeError(ctx, 400, chk.error.message)
|
||||||
[key: string]: SchemeParameter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemeArray {
|
|
||||||
type: "array"
|
|
||||||
children:
|
|
||||||
| SchemeParameter /* All children of the array must be this type */
|
|
||||||
| SchemeParameter[] /* Array must match this pattern */
|
|
||||||
}
|
|
||||||
|
|
||||||
type SchemeParameter = SchemeType | SchemeObject | SchemeArray
|
|
21
src/server/lib/schemas/accounts.ts
Normal file
21
src/server/lib/schemas/accounts.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {z} from "zod"
|
||||||
|
import { FileVisibility } from "./files.js"
|
||||||
|
|
||||||
|
export const StringPassword = z.string().min(8,"password must be at least 8 characters")
|
||||||
|
export const Password =
|
||||||
|
z.object({
|
||||||
|
hash: z.string(),
|
||||||
|
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")
|
||||||
|
export const Account =
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: Username,
|
||||||
|
email: z.optional(z.string().email("must be an email")),
|
||||||
|
password: Password,
|
||||||
|
files: z.array(z.string()),
|
||||||
|
admin: z.boolean(),
|
||||||
|
defaultFileVisibility: FileVisibility
|
||||||
|
})
|
18
src/server/lib/schemas/files.ts
Normal file
18
src/server/lib/schemas/files.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {z} from "zod"
|
||||||
|
import config from "../config.js"
|
||||||
|
|
||||||
|
export const FileId = z.string().regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/).max(config.maxUploadIdLength)
|
||||||
|
export const FileVisibility = z.enum(["public", "anonymous", "private"])
|
||||||
|
export const FileTag = z.string().toLowerCase().max(30, "tag length too long")
|
||||||
|
export const FilePointer = z.object({
|
||||||
|
filename: z.string().max(256, "filename too long"),
|
||||||
|
mime: z.string().max(256, "mimetype too long"),
|
||||||
|
messageids: z.array(z.string()),
|
||||||
|
owner: z.optional(z.string()),
|
||||||
|
sizeInBytes: z.optional(z.number()),
|
||||||
|
tag: z.optional(FileTag),
|
||||||
|
visibility: z.optional(FileVisibility).default("public"),
|
||||||
|
chunkSize: z.optional(z.number()),
|
||||||
|
lastModified: z.optional(z.number()),
|
||||||
|
md5: z.optional(z.string())
|
||||||
|
})
|
2
src/server/lib/schemas/index.ts
Normal file
2
src/server/lib/schemas/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * as AccountSchemas from "./accounts.js"
|
||||||
|
export * as FileSchemas from "./files.js"
|
|
@ -14,8 +14,7 @@ import config from "../../../lib/config.js"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
import Files, {
|
import Files, {
|
||||||
FileVisibility,
|
FileVisibility,
|
||||||
generateFileId,
|
generateFileId
|
||||||
id_check_regex,
|
|
||||||
} from "../../../lib/files.js"
|
} from "../../../lib/files.js"
|
||||||
|
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile } from "fs/promises"
|
||||||
|
|
|
@ -68,10 +68,6 @@ export default function (files: Files) {
|
||||||
|
|
||||||
let fp = files.files[e]
|
let fp = files.files[e]
|
||||||
|
|
||||||
if (fp.reserved) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (body.action) {
|
switch (body.action) {
|
||||||
case "delete":
|
case "delete":
|
||||||
files.unlink(e, true)
|
files.unlink(e, true)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files.js"
|
import Files from "../../../lib/files.js"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../../lib/auth.js"
|
||||||
import {
|
import {
|
||||||
|
@ -15,11 +15,14 @@ import {
|
||||||
noAPIAccess,
|
noAPIAccess,
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
|
scheme,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../../lib/middleware.js"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
import { CodeMgr, sendMail } from "../../../lib/mail.js"
|
import { CodeMgr, sendMail } from "../../../lib/mail.js"
|
||||||
|
|
||||||
import Configuration from "../../../lib/config.js"
|
import Configuration from "../../../lib/config.js"
|
||||||
|
import { AccountSchemas, FileSchemas } from "../../../lib/schemas/index.js"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
const router = new Hono<{
|
const router = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
|
@ -80,13 +83,7 @@ const validators: {
|
||||||
| ValidatorWithSettings<T>
|
| ValidatorWithSettings<T>
|
||||||
} = {
|
} = {
|
||||||
defaultFileVisibility(actor, target, params) {
|
defaultFileVisibility(actor, target, params) {
|
||||||
if (
|
return params.defaultFileVisibility
|
||||||
["public", "private", "anonymous"].includes(
|
|
||||||
params.defaultFileVisibility
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return params.defaultFileVisibility
|
|
||||||
else return [400, "invalid file visibility"]
|
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
acceptsNull: true,
|
acceptsNull: true,
|
||||||
|
@ -109,8 +106,8 @@ const validators: {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof params.email !== "string")
|
if (!z.string().email().safeParse(typeof params.email).success)
|
||||||
return [400, "email must be string"]
|
return [400, "bad email"]
|
||||||
if (actor.admin) return params.email
|
if (actor.admin) return params.email
|
||||||
|
|
||||||
// send verification email
|
// send verification email
|
||||||
|
@ -150,9 +147,6 @@ const validators: {
|
||||||
)
|
)
|
||||||
return [401, "current password incorrect"]
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
if (typeof params.password != "string" || params.password.length < 8)
|
|
||||||
return [400, "password must be 8 characters or longer"]
|
|
||||||
|
|
||||||
if (target.email) {
|
if (target.email) {
|
||||||
sendMail(
|
sendMail(
|
||||||
target.email,
|
target.email,
|
||||||
|
@ -173,25 +167,9 @@ const validators: {
|
||||||
)
|
)
|
||||||
return [401, "current password incorrect"]
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
if (
|
|
||||||
typeof params.username != "string" ||
|
|
||||||
params.username.length < 3 ||
|
|
||||||
params.username.length > 20
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
400,
|
|
||||||
"username must be between 3 and 20 characters in length",
|
|
||||||
]
|
|
||||||
|
|
||||||
if (Accounts.getFromUsername(params.username))
|
if (Accounts.getFromUsername(params.username))
|
||||||
return [400, "account with this username already exists"]
|
return [400, "account with this username already exists"]
|
||||||
|
|
||||||
if (
|
|
||||||
(params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
|
|
||||||
params.username
|
|
||||||
)
|
|
||||||
return [400, "username has invalid characters"]
|
|
||||||
|
|
||||||
if (target.email) {
|
if (target.email) {
|
||||||
sendMail(
|
sendMail(
|
||||||
target.email,
|
target.email,
|
||||||
|
@ -207,12 +185,7 @@ const validators: {
|
||||||
customCSS: {
|
customCSS: {
|
||||||
acceptsNull: true,
|
acceptsNull: true,
|
||||||
validator: (actor, target, params) => {
|
validator: (actor, target, params) => {
|
||||||
if (
|
if (FileSchemas.FileId.safeParse(params.customCSS).success)
|
||||||
!params.customCSS ||
|
|
||||||
(params.customCSS.match(id_check_regex)?.[0] ==
|
|
||||||
params.customCSS &&
|
|
||||||
params.customCSS.length <= Configuration.maxUploadIdLength)
|
|
||||||
)
|
|
||||||
return params.customCSS
|
return params.customCSS
|
||||||
else return [400, "bad file id"]
|
else return [400, "bad file id"]
|
||||||
},
|
},
|
||||||
|
@ -272,7 +245,10 @@ function isMessage(object: any): object is Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (files: Files) {
|
export default function (files: Files) {
|
||||||
router.post("/", async (ctx) => {
|
router.post("/", scheme(z.object({
|
||||||
|
username: AccountSchemas.Username,
|
||||||
|
password: AccountSchemas.StringPassword
|
||||||
|
})), async (ctx) => {
|
||||||
const body = await ctx.req.json()
|
const body = await ctx.req.json()
|
||||||
if (!Configuration.accounts.registrationEnabled) {
|
if (!Configuration.accounts.registrationEnabled) {
|
||||||
return ServeError(ctx, 403, "account registration disabled")
|
return ServeError(ctx, 403, "account registration disabled")
|
||||||
|
@ -290,28 +266,6 @@ export default function (files: Files) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.username.length < 3 || body.username.length > 20) {
|
|
||||||
return ServeError(
|
|
||||||
ctx,
|
|
||||||
400,
|
|
||||||
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
|
||||||
) {
|
|
||||||
return ServeError(ctx, 400, "username contains invalid characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.password.length < 8) {
|
|
||||||
return ServeError(
|
|
||||||
ctx,
|
|
||||||
400,
|
|
||||||
"password must be 8 characters or longer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accounts.create(body.username, body.password)
|
return Accounts.create(body.username, body.password)
|
||||||
.then((account) => {
|
.then((account) => {
|
||||||
login(ctx, account)
|
login(ctx, account)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files.js"
|
import Files from "../../../lib/files.js"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../../lib/auth.js"
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { stat } from "fs/promises"
|
import { stat } from "fs/promises"
|
||||||
import Files from "./lib/files.js"
|
import Files from "../lib/files.js"
|
||||||
import { program } from "commander"
|
import { program } from "commander"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Writable } from "node:stream"
|
import { Writable } from "node:stream"
|
||||||
import config from "./lib/config.js"
|
import config from "../lib/config.js"
|
||||||
import pkg from "../../package.json" assert { type: "json" }
|
import pkg from "../../../package.json" assert { type: "json" }
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
|
|
||||||
// init data
|
// init data
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
if (!fs.existsSync(__dirname + "/../../.data/"))
|
if (!fs.existsSync(__dirname + "/../../../.data/"))
|
||||||
fs.mkdirSync(__dirname + "/../../.data/")
|
fs.mkdirSync(__dirname + "/../../../.data/")
|
||||||
|
|
||||||
// discord
|
// discord
|
||||||
let files = new Files(config)
|
let files = new Files(config)
|
Loading…
Reference in a new issue