Merge pull request #70 from mollersuite/jwt

switch to JSON web tokens
This commit is contained in:
May 2024-05-24 22:11:15 -07:00 committed by GitHub
commit eb981afe2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 205 additions and 164 deletions

View file

@ -18,4 +18,6 @@ MAIL__PORT=
MAIL__SECURE= MAIL__SECURE=
MAIL__SEND_FROM= MAIL__SEND_FROM=
MAIL__USER= MAIL__USER=
MAIL__PASS= MAIL__PASS=
JWT_SECRET=

9
package-lock.json generated
View file

@ -16,6 +16,7 @@
"dotenv": "^16.0.2", "dotenv": "^16.0.2",
"formidable": "^3.5.1", "formidable": "^3.5.1",
"hono": "^4.0.10", "hono": "^4.0.10",
"jose": "^5.2.4",
"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",
@ -786,6 +787,14 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
}, },
"node_modules/jose": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
"integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",

View file

@ -25,6 +25,7 @@
"dotenv": "^16.0.2", "dotenv": "^16.0.2",
"formidable": "^3.5.1", "formidable": "^3.5.1",
"hono": "^4.0.10", "hono": "^4.0.10",
"jose": "^5.2.4",
"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",

View file

@ -21,23 +21,21 @@ export type Account = z.infer<typeof AccountSchemas.Account>
* @returns A Promise which returns the new account's ID * @returns A Promise which returns the new account's ID
*/ */
export async function create(username:string,pwd:string,admin:boolean=false):Promise<string> { export async function create(username:string,pwd:string,admin:boolean=false):Promise<Account> {
let accId = crypto.randomBytes(12).toString("hex") let acc: Account = {
id: crypto.randomUUID(),
Db.data.push( username: username,
{ password: password.hash(pwd),
id: accId, files: [],
username: username, admin: admin,
password: password.hash(pwd), defaultFileVisibility: "public",
files: [], settings: AccountSchemas.Settings.User.parse({})
admin: admin, }
defaultFileVisibility: "public",
settings: AccountSchemas.Settings.User.parse({})
}
)
Db.data.push(acc)
await Db.save() await Db.save()
return accId
return acc
} }
/** /**
@ -161,6 +159,16 @@ export namespace files {
} }
} }
export type AccountResolvable = Account | string | `@${string}`
export function resolve(obj: AccountResolvable) {
return typeof obj == "object"
? obj
: obj.startsWith("@")
? getFromUsername(obj.slice(1))
: getFromId(obj)
}
Db.read() Db.read()
.then(() => { .then(() => {
if (!Db.data.find(e => e.admin)) { if (!Db.data.find(e => e.admin)) {

View file

@ -5,29 +5,37 @@ import { readFile, writeFile } from "fs/promises"
import { z } from "zod" import { z } from "zod"
import { AuthSchemas } from "./schemas/index.js" import { AuthSchemas } from "./schemas/index.js"
import DbFile from "./dbfile.js" import DbFile from "./dbfile.js"
import * as jose from "jose"
import { AccountResolvable } from "./accounts.js"
import config from "./config.js"
export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {}
export type TokenPermission = z.infer<typeof AuthSchemas.Scope> export type Scope = z.infer<typeof AuthSchemas.Scope>
export type TokenType = z.infer<typeof AuthSchemas.TokenType> export type TokenType = z.infer<typeof AuthSchemas.TokenType>
export type AuthToken = z.infer<typeof AuthSchemas.AuthToken> export type AuthToken = z.infer<typeof AuthSchemas.AuthToken>
export type TokenResolvable = string | AuthToken
export const Db = new DbFile<AuthToken[]>("tokens", []) export const Db = new DbFile<AuthToken[]>("tokens", [])
export function resolve(token: TokenResolvable) {
let resolved = typeof token == "object" ? token : Db.data.find(e => e.id == token)
if (resolved && (resolved.expire == null || Date.now() < resolved.expire))
return resolved
}
export function create( export function create(
id: string, account: AccountResolvable,
expire: number | null = 24 * 60 * 60 * 1000, expire: number | null = 24 * 60 * 60 * 1000,
type: TokenType = "User", type: TokenType = "User",
tokenPermissions?: TokenPermission[], scopes?: Scope[]
issuer?: string
) { ) {
let token = AuthSchemas.AuthToken.parse({ let token = AuthSchemas.AuthToken.parse({
account: id, account,
token: crypto.randomBytes(36).toString("hex"), id: crypto.randomUUID(),
expire: typeof expire == "number" ? Date.now() + expire : null, expire: typeof expire == "number" ? Date.now() + expire : null,
issuer,
type, type,
tokenPermissions: scopes:
type != "User" ? tokenPermissions || ["user"] : undefined, type != "User" ? scopes || ["user"] : undefined
}) })
Db.data.push(token) Db.data.push(token)
@ -35,59 +43,75 @@ export function create(
Db.save() Db.save()
return token.token return token
} }
export function tokenFor(ctx: Context) {
return ( export async function getJwtId(jwt: string) {
getCookie(ctx, "auth") || let result = await jose.jwtVerify(jwt, config.jwtSecret).catch(e => null)
(ctx.req.header("authorization")?.startsWith("Bearer ") return result ? result.payload.jti : undefined
}
export function makeJwt(_token: TokenResolvable) {
let token = resolve(_token)!
let jwt = new jose.SignJWT({
exp: token.expire || undefined,
sub: token.account,
jti: token.id,
...(token.type != "User" ? { scope: token.scopes } : {})
}).setProtectedHeader({ alg: "HS256" })
return jwt.sign(config.jwtSecret)
}
export async function tokenFor(ctx: Context) {
let token =
getCookie(ctx, "auth")
|| (ctx.req.header("authorization")?.startsWith("Bearer ")
? ctx.req.header("authorization")?.split(" ")[1] ? ctx.req.header("authorization")?.split(" ")[1]
: undefined) : undefined)
) if (!token) return
let jti = await getJwtId(token)
return jti
} }
function getToken(token: string) { export function validate(token: TokenResolvable) {
return Db.data.find( return resolve(token)?.account
(e) => e.token == token && (e.expire == null || Date.now() < e.expire)
)
} }
export function validate(token: string) { export function getType(token: TokenResolvable) {
return getToken(token)?.account return resolve(token)?.type
} }
export function getType(token: string): TokenType | undefined { export function getScopes(token: TokenResolvable): Scope[] | undefined {
return getToken(token)?.type let tok = resolve(token)
} if (tok && "scopes" in tok)
return tok.scopes
export function getPermissions(token: string): TokenPermission[] | undefined {
let tok = getToken(token)
if (tok && "tokenPermissions" in tok)
return tok.tokenPermissions
} }
export function tokenTimer(token: AuthToken) { export function tokenTimer(token: AuthToken) {
if (!token.expire) return if (!token.expire) return
if (Date.now() >= token.expire) { if (Date.now() >= token.expire) {
invalidate(token.token) invalidate(token)
return return
} }
AuthTokenTO[token.token] = setTimeout( AuthTokenTO[token.id] = setTimeout(
() => invalidate(token.token), () => invalidate(token),
token.expire - Date.now() token.expire - Date.now()
) )
} }
export function invalidate(token: string) { export function invalidate(_token: TokenResolvable) {
if (AuthTokenTO[token]) { let token = resolve(_token)!
clearTimeout(AuthTokenTO[token]) if (AuthTokenTO[token.id]) {
clearTimeout(AuthTokenTO[token.id])
} }
Db.data.splice( Db.data.splice(
Db.data.findIndex((e) => e.token == token), Db.data.indexOf(token),
1 1
) )
Db.save() Db.save()

View file

@ -26,7 +26,9 @@ export interface Configuration {
} }
user: string user: string
pass: string pass: string
} },
jwtSecret: Buffer
} }
export interface ClientConfiguration { export interface ClientConfiguration {
@ -72,4 +74,6 @@ export default {
user: process.env.MAIL__USER, user: process.env.MAIL__USER,
pass: process.env.MAIL__PASS, pass: process.env.MAIL__PASS,
}, },
jwtSecret: Buffer.from(process.env.JWT_SECRET!)
} as Configuration } as Configuration

View file

@ -1,5 +1,5 @@
import * as Accounts from "./accounts.js" import * as Accounts from "./accounts.js"
import type { Context, Handler as RequestHandler } from "hono" import type { Context, Hono, 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"
@ -9,20 +9,19 @@ import { codes } from "./codes.js"
/** /**
* @description Middleware which adds an account, if any, to ctx.get("account") * @description Middleware which adds an account, if any, to ctx.get("account")
*/ */
export const getAccount: RequestHandler = function (ctx, next) { export const getAccount: RequestHandler = async function (ctx, next) {
let account = Accounts.getFromToken(auth.tokenFor(ctx)!) let uToken = (await auth.tokenFor(ctx))!
let account = Accounts.getFromToken(uToken)
if (account?.suspension) if (account?.suspension)
setCookie(ctx, "auth", "") auth.invalidate(uToken)
ctx.set("account", account) ctx.set("account", account)
return next() return next()
} }
export function resolveTarget(actor: Accounts.Account, targetString: string) { export function resolveTarget(actor: Accounts.Account, target: Accounts.AccountResolvable) {
return targetString == "me" return target == "me"
? actor ? actor
: targetString.startsWith("@") : Accounts.resolve(target)
? Accounts.getFromUsername(targetString.slice(1))
: Accounts.getFromId(targetString)
} }
/** /**
@ -30,13 +29,12 @@ export function resolveTarget(actor: Accounts.Account, targetString: string) {
*/ */
export const getTarget: RequestHandler = async (ctx, next) => { export const getTarget: RequestHandler = async (ctx, next) => {
let tok = auth.tokenFor(ctx) let tok = await auth.tokenFor(ctx)
let permissions let permissions
if (tok && auth.getType(tok) != "User") if (tok && auth.getType(tok) != "User")
permissions = auth.getPermissions(tok) permissions = auth.getScopes(tok)
let actor = ctx.get("account") let actor = ctx.get("account")
let target = resolveTarget(actor, ctx.req.param("user")) let target = resolveTarget(actor, ctx.req.param("user"))
if (!target) return ServeError(ctx, 404, "account does not exist") if (!target) return ServeError(ctx, 404, "account does not exist")
@ -44,11 +42,12 @@ export const getTarget: RequestHandler = async (ctx, next) => {
if (actor && ( if (actor && (
( (
target != actor // target is not the current account target != actor // target is not the current account
&& !actor?.admin // account is not admin && (
) !actor?.admin // account is not admin
|| ( || (
actor?.admin // account is admin permissions && !permissions.includes("manage_server") // account is admin but permissions does not include manage_server
&& permissions && !permissions.includes("manage_server") // permissions does not include manage_server )
)
) )
)) ))
return ServeError(ctx, 403, "you cannot manage this user") return ServeError(ctx, 403, "you cannot manage this user")
@ -62,10 +61,10 @@ export const getTarget: RequestHandler = async (ctx, next) => {
* @description Blocks routes with a target user set to the account performing the action from bot tokens which do not have the manage_account permission * @description Blocks routes with a target user set to the account performing the action from bot tokens which do not have the manage_account permission
*/ */
export const accountMgmtRoute: RequestHandler = async (ctx,next) => { export const accountMgmtRoute: RequestHandler = async (ctx,next) => {
let tok = auth.tokenFor(ctx) let tok = await auth.tokenFor(ctx)
let permissions let permissions
if (tok && auth.getType(tok) != "User") if (tok && auth.getType(tok) != "User")
permissions = auth.getPermissions(tok) permissions = auth.getScopes(tok)
if ( if (
( (
@ -113,26 +112,27 @@ export const requiresAdmin: RequestHandler = function (ctx, next) {
* @param tokenPermissions Permissions which your route requires. * @param tokenPermissions Permissions which your route requires.
* @returns Express middleware * @returns Express middleware
*/ */
export const requiresPermissions = function ( export const requiresScopes = function (
...tokenPermissions: auth.TokenPermission[] ...wantsScopes: auth.Scope[]
): RequestHandler { ): RequestHandler {
return function (ctx, next) { return async function (ctx, next) {
let token = auth.tokenFor(ctx)! let token = (await auth.tokenFor(ctx))!
let type = auth.getType(token) let type = auth.getType(token)
if (type != "User") { if (type != "User") {
let permissions = auth.getPermissions(token) let scopes = auth.getScopes(token)
if (!permissions) return ServeError(ctx, 403, "insufficient permissions") if (!scopes) return ServeError(ctx, 403, "insufficient permissions")
else { else {
for (let v of tokenPermissions) { for (let v of wantsScopes) {
if (!permissions.includes(v as auth.TokenPermission)) { if (!scopes.includes(v)) {
return ServeError(ctx, 403, "insufficient permissions") return ServeError(ctx, 403, "insufficient permissions")
} }
} }
return next()
} }
} else return next() }
return next()
} }
} }
@ -140,8 +140,8 @@ export const requiresPermissions = function (
* @description Blocks requests based on whether or not the token being used to access the route is of type `User`. * @description Blocks requests based on whether or not the token being used to access the route is of type `User`.
*/ */
export const noAPIAccess: RequestHandler = function (ctx, next) { export const noAPIAccess: RequestHandler = async function (ctx, next) {
if (auth.getType(auth.tokenFor(ctx)!) == "App") if (auth.getType((await auth.tokenFor(ctx))!) == "App")
return ServeError(ctx, 403, "apps are not allowed to access this endpoint") return ServeError(ctx, 403, "apps are not allowed to access this endpoint")
else return next() else return next()
} }
@ -153,8 +153,8 @@ export const noAPIAccess: RequestHandler = function (ctx, next) {
export const assertAPI = function ( export const assertAPI = function (
condition: (ctx: Context) => boolean condition: (ctx: Context) => boolean
): RequestHandler { ): RequestHandler {
return function (ctx, next) { return async function (ctx, next) {
let reqToken = auth.tokenFor(ctx)! let reqToken = (await auth.tokenFor(ctx))!
if ( if (
auth.getType(reqToken) != "User" && auth.getType(reqToken) != "User" &&
condition(ctx) condition(ctx)
@ -187,9 +187,9 @@ export function scheme(scheme: z.ZodTypeAny, transformer: (ctx: Context) => Prom
// Not really middleware but a utility // Not really middleware but a utility
export const login = (ctx: Context, account: string) => { export const login = async (ctx: Context, account: Accounts.AccountResolvable) => {
let token = auth.create(account, 3 * 24 * 60 * 60 * 1000) let token = auth.create(account, 3 * 24 * 60 * 60 * 1000)
setCookie(ctx, "auth", token, { setCookie(ctx, "auth", await auth.makeJwt(token), {
path: "/", path: "/",
sameSite: "Strict", sameSite: "Strict",
secure: true, secure: true,
@ -208,4 +208,15 @@ export const verifyPoi = (user: string, poi?: string, wantsMfaPoi: boolean = fal
poiCode.terminate() poiCode.terminate()
return true return true
} }
export const mirror = (apiRoot: Hono, ctx: Context, url: string, init: Partial<RequestInit>) => apiRoot.fetch(
new Request(
(new URL(url, ctx.req.raw.url)).href,
{
...ctx.req.raw,
...init
}
),
ctx.env
)

View file

@ -17,7 +17,7 @@ export const TokenType = z.enum([
const BaseAuthToken = z.object({ const BaseAuthToken = z.object({
account: z.string(), account: z.string(),
token: z.string(), id: z.string(),
expire: z.number() expire: z.number()
.nullable() .nullable()
.refine(e => e == null || e > Date.now(), "expiration must be after now"), .refine(e => e == null || e > Date.now(), "expiration must be after now"),
@ -31,11 +31,10 @@ export const AuthToken = z.discriminatedUnion("type",[
}), }),
BaseAuthToken.extend({ BaseAuthToken.extend({
type: z.literal("ApiKey"), type: z.literal("ApiKey"),
tokenPermissions: z.array(Scope).default(["user"]) scopes: z.array(Scope).default(["user"])
}), }),
BaseAuthToken.extend({ BaseAuthToken.extend({
type: z.literal("App"), type: z.literal("App"),
tokenPermissions: z.array(Scope).default(["user"]), scopes: z.array(Scope).default(["user"])
issuer: z.string()
}) })
]) ])

View file

@ -7,7 +7,7 @@ import {
getAccount, getAccount,
requiresAccount, requiresAccount,
requiresAdmin, requiresAdmin,
requiresPermissions, requiresScopes,
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
import Files from "../../../lib/files.js" import Files from "../../../lib/files.js"
@ -20,7 +20,7 @@ adminRoutes
.use(getAccount) .use(getAccount)
.use(requiresAccount) .use(requiresAccount)
.use(requiresAdmin) .use(requiresAdmin)
.use(requiresPermissions("manage_server")) .use(requiresScopes("manage_server"))
export default function (files: Files) { export default function (files: Files) {
adminRoutes.post("/reset", async (ctx) => { adminRoutes.post("/reset", async (ctx) => {
@ -42,7 +42,7 @@ export default function (files: Files) {
Accounts.password.set(targetAccount.id, body.password) Accounts.password.set(targetAccount.id, body.password)
auth.Db.data.filter((e) => e.account == targetAccount?.id).forEach( auth.Db.data.filter((e) => e.account == targetAccount?.id).forEach(
(v) => { (v) => {
auth.invalidate(v.token) auth.invalidate(v.id)
} }
) )
@ -107,7 +107,7 @@ export default function (files: Files) {
let accId = targetAccount.id let accId = targetAccount.id
auth.Db.data.filter((e) => e.account == accId).forEach((v) => { auth.Db.data.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token) auth.invalidate(v.id)
}) })
let cpl = () => let cpl = () =>

View file

@ -5,9 +5,10 @@ import * as auth from "../../../lib/auth.js"
import { sendMail } from "../../../lib/mail.js" import { sendMail } from "../../../lib/mail.js"
import { import {
getAccount, getAccount,
login,
noAPIAccess, noAPIAccess,
requiresAccount, requiresAccount,
requiresPermissions, requiresScopes,
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
import { accountRatelimit } from "../../../lib/ratelimit.js" import { accountRatelimit } from "../../../lib/ratelimit.js"
import config from "../../../lib/config.js" import config from "../../../lib/config.js"
@ -58,13 +59,7 @@ export default function (files: Files) {
assign token assign token
*/ */
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000), { login(ctx, acc.id)
path: "/",
sameSite: "Strict",
secure: true,
httpOnly: true,
maxAge: 3 * 24 * 60 * 60 * 1000,
})
return ctx.text("") return ctx.text("")
}) })
@ -116,11 +111,7 @@ export default function (files: Files) {
assign token assign token
*/ */
setCookie( login(ctx, newAcc)
ctx,
"auth",
auth.create(newAcc, 3 * 24 * 60 * 60 * 1000)
)
return ctx.text("") return ctx.text("")
}) })
.catch(() => ServeError(ctx, 500, "internal server error")) .catch(() => ServeError(ctx, 500, "internal server error"))
@ -138,7 +129,7 @@ export default function (files: Files) {
authRoutes.post( authRoutes.post(
"/dfv", "/dfv",
requiresAccount, requiresAccount,
requiresPermissions("manage_files"), requiresScopes("manage_files"),
// Used body-parser // Used body-parser
async (ctx) => { async (ctx) => {
const body = await ctx.req.json() const body = await ctx.req.json()
@ -171,7 +162,7 @@ export default function (files: Files) {
let accId = acc.id let accId = acc.id
auth.Db.data.filter((e) => e.account == accId).forEach((v) => { auth.Db.data.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token) auth.invalidate(v.id)
}) })
let cpl = () => let cpl = () =>
@ -462,7 +453,7 @@ export default function (files: Files) {
} }
if (typeof ctx.req.param("code") == "string" && vcode) { if (typeof ctx.req.param("code") == "string" && vcode) {
setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000)) login(ctx, vcode)
let e = pwReset.get(vcode)?.expiry let e = pwReset.get(vcode)?.expiry
if (e) clearTimeout(e) if (e) clearTimeout(e)
pwReset.delete(vcode) pwReset.delete(vcode)
@ -491,7 +482,7 @@ export default function (files: Files) {
Accounts.password.set(accId, body.password) Accounts.password.set(accId, body.password)
auth.Db.data.filter((e) => e.account == accId).forEach((v) => { auth.Db.data.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token) auth.invalidate(v.id)
}) })
if (acc.email) { if (acc.email) {
@ -518,7 +509,7 @@ export default function (files: Files) {
let accId = acc.id let accId = acc.id
auth.Db.data.filter((e) => e.account == accId).forEach((v) => { auth.Db.data.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token) auth.invalidate(v.id)
}) })
return ctx.text("logged out all sessions") return ctx.text("logged out all sessions")
@ -528,10 +519,10 @@ export default function (files: Files) {
authRoutes.get( authRoutes.get(
"/me", "/me",
requiresAccount, requiresAccount,
requiresPermissions("user"), requiresScopes("user"),
async (ctx) => { async (ctx) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
let sessionToken = auth.tokenFor(ctx)! let sessionToken = (await auth.tokenFor(ctx))!
let accId = acc.id let accId = acc.id
return ctx.json({ return ctx.json({
...acc, ...acc,
@ -542,12 +533,12 @@ export default function (files: Files) {
(e.expire == null || e.expire > Date.now()) (e.expire == null || e.expire > Date.now())
).length, ).length,
sessionExpires: auth.Db.data.find( sessionExpires: auth.Db.data.find(
(e) => e.token == sessionToken (e) => e.id == sessionToken
)?.expire, )?.expire,
password: undefined, password: undefined,
email: email:
auth.getType(sessionToken) == "User" || auth.getType(sessionToken) == "User" ||
auth.getPermissions(sessionToken)?.includes("email") auth.getScopes(sessionToken)?.includes("email")
? acc.email ? acc.email
: undefined, : undefined,
}) })

View file

@ -5,7 +5,7 @@ import Files from "../../../lib/files.js"
import { import {
getAccount, getAccount,
requiresAccount, requiresAccount,
requiresPermissions, requiresScopes,
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
export let fileApiRoutes = new Hono<{ export let fileApiRoutes = new Hono<{
@ -20,7 +20,7 @@ export default function (files: Files) {
fileApiRoutes.get( fileApiRoutes.get(
"/list", "/list",
requiresAccount, requiresAccount,
requiresPermissions("user"), requiresScopes("user"),
async (ctx) => { async (ctx) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
@ -49,7 +49,7 @@ export default function (files: Files) {
fileApiRoutes.post( fileApiRoutes.post(
"/manage", "/manage",
requiresPermissions("manage_files"), requiresScopes("manage_files"),
async (ctx) => { async (ctx) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json() const body = await ctx.req.json()

View file

@ -4,7 +4,7 @@ import * as auth from "../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser" import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../lib/errors.js" import ServeError from "../../../lib/errors.js"
import Files, { WebError } from "../../../lib/files.js" import Files, { WebError } from "../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../lib/middleware.js" import { getAccount, mirror, requiresScopes } from "../../../lib/middleware.js"
import {Readable} from "node:stream" import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web" import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable" import formidable from "formidable"
@ -37,17 +37,7 @@ export default function (files: Files, apiRoot: Hono) {
primaryApi.get("/cpt/:fileId/*", fileReader(apiRoot)) primaryApi.get("/cpt/:fileId/*", fileReader(apiRoot))
primaryApi.post("/upload", async (ctx) => primaryApi.post("/upload", async (ctx) =>
apiRoot.fetch( mirror(apiRoot, ctx, "/api/v1/file", {method: "PUT"})
new Request(
(new URL(
`/api/v1/file`, ctx.req.raw.url)).href,
{
...ctx.req.raw,
method: "PUT"
}
),
ctx.env
)
) )
return primaryApi return primaryApi

View file

@ -16,7 +16,7 @@ import {
login, login,
noAPIAccess, noAPIAccess,
requiresAccount, requiresAccount,
requiresPermissions, requiresScopes,
scheme, scheme,
} from "../../../../lib/middleware.js" } from "../../../../lib/middleware.js"
import ServeError from "../../../../lib/errors.js" import ServeError from "../../../../lib/errors.js"
@ -41,7 +41,7 @@ function getTargetToken(ctx: Context<HonoEnv, "/:token", BlankInput>) {
return auth.Db.data.find( return auth.Db.data.find(
e => e =>
e.account == ctx.get("target").id e.account == ctx.get("target").id
&& e.token == ctx.req.param("token") && e.id == ctx.req.param("token")
) )
} }
@ -79,7 +79,7 @@ export default function (files: Files) {
&& ctx.get("parsedScheme").has(e.type) && ctx.get("parsedScheme").has(e.type)
) )
targets.forEach(e => auth.invalidate(e.token)) targets.forEach(e => auth.invalidate(e.id))
return ctx.text(`deleted ${targets.length} tokens`) return ctx.text(`deleted ${targets.length} tokens`)
} }
@ -90,7 +90,7 @@ export default function (files: Files) {
}) })
router.delete("/:token", async (ctx) => { router.delete("/:token", async (ctx) => {
auth.invalidate(ctx.get("targetToken").token) auth.invalidate(ctx.get("targetToken"))
return ctx.text(`deleted token ${ctx.req.param("token")}`) return ctx.text(`deleted token ${ctx.req.param("token")}`)
}) })
@ -114,10 +114,9 @@ export default function (files: Files) {
"ApiKey", "ApiKey",
params.scopes == "all" params.scopes == "all"
? AuthSchemas.Scope.options ? AuthSchemas.Scope.options
: Array.from(new Set(params.scopes)), : Array.from(new Set(params.scopes))
"monofile"
) )
return ctx.text(token) return ctx.text(await auth.makeJwt(token.id))
} }
) )

View file

@ -17,7 +17,7 @@ import {
login, login,
noAPIAccess, noAPIAccess,
requiresAccount, requiresAccount,
requiresPermissions, requiresScopes,
scheme, scheme,
verifyPoi, verifyPoi,
} from "../../../../lib/middleware.js" } from "../../../../lib/middleware.js"
@ -190,7 +190,7 @@ const validators: {
if (params.suspension) if (params.suspension)
auth.Db.data auth.Db.data
.filter(e => e.account == target.id) .filter(e => e.account == target.id)
.forEach(e => auth.invalidate(e.token)) .forEach(e => auth.invalidate(e.id))
return params.suspension || undefined return params.suspension || undefined
} }
}, },
@ -217,7 +217,12 @@ router.use(getAccount)
router.on( router.on(
["GET","PATCH","DELETE"], ["GET","PATCH","DELETE"],
"/:user", "/:user",
requiresAccount, getTarget, accountMgmtRoute requiresAccount, getTarget
)
router.on(
["PATCH","DELETE"],
"/:user",
accountMgmtRoute
) )
function isMessage(object: any): object is Message { function isMessage(object: any): object is Message {
@ -278,7 +283,7 @@ export default function (files: Files) {
.then((account) => { .then((account) => {
if (!ctx.get("account")) if (!ctx.get("account"))
login(ctx, account) login(ctx, account)
return ctx.text(account) return ctx.text(account.id)
}) })
.catch((e) => { .catch((e) => {
console.error(e) console.error(e)
@ -338,7 +343,7 @@ export default function (files: Files) {
return ServeError(ctx, 403, "invalid proof of identity provided") return ServeError(ctx, 403, "invalid proof of identity provided")
auth.Db.data.filter((e) => e.account == target?.id).forEach((token) => { auth.Db.data.filter((e) => e.account == target?.id).forEach((token) => {
auth.invalidate(token.token) auth.invalidate(token.id)
}) })
await Accounts.deleteAccount(target.id) await Accounts.deleteAccount(target.id)
@ -357,14 +362,14 @@ export default function (files: Files) {
router.get("/:user", async (ctx) => { router.get("/:user", async (ctx) => {
let acc = ctx.get("target") let acc = ctx.get("target")
let sessionToken = auth.tokenFor(ctx)! let sessionToken = (await auth.tokenFor(ctx))!
return ctx.json({ return ctx.json({
...acc, ...acc,
password: undefined, password: undefined,
email: email:
auth.getType(sessionToken) == "User" || auth.getType(sessionToken) == "User" ||
auth.getPermissions(sessionToken)?.includes("email") auth.getScopes(sessionToken)?.includes("email")
? acc.email ? acc.email
: undefined, : undefined,
activeSessions: auth.Db.data.filter( activeSessions: auth.Db.data.filter(

View file

@ -16,7 +16,7 @@ import {
login, login,
noAPIAccess, noAPIAccess,
requiresAccount, requiresAccount,
requiresPermissions, requiresScopes,
requiresTarget, requiresTarget,
scheme, scheme,
} from "../../../../lib/middleware.js" } from "../../../../lib/middleware.js"

View file

@ -4,7 +4,7 @@ import * as auth from "../../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser" import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../../lib/errors.js" import ServeError from "../../../../lib/errors.js"
import Files, { WebError } from "../../../../lib/files.js" import Files, { WebError } from "../../../../lib/files.js"
import { getAccount, requiresAccount, requiresPermissions, scheme } from "../../../../lib/middleware.js" import { getAccount, requiresAccount, requiresScopes, scheme } from "../../../../lib/middleware.js"
import {Readable} from "node:stream" import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web" import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable" import formidable from "formidable"
@ -28,7 +28,7 @@ export default function(files: Files) {
router.on( router.on(
["PUT", "POST"], ["PUT", "POST"],
"/", "/",
requiresPermissions("manage_files"), requiresScopes("manage_files"),
(ctx) => { return new Promise((resolve,reject) => { (ctx) => { return new Promise((resolve,reject) => {
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering ctx.env.incoming.removeAllListeners("data") // remove hono's buffering

View file

@ -4,7 +4,7 @@ import * as auth from "../../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser" import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../../lib/errors.js" import ServeError from "../../../../lib/errors.js"
import Files, { WebError } from "../../../../lib/files.js" import Files, { WebError } from "../../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../../lib/middleware.js" import { getAccount, requiresScopes } from "../../../../lib/middleware.js"
import {Readable} from "node:stream" import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web" import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable" import formidable from "formidable"
@ -50,10 +50,12 @@ export default function(files: Files, apiRoot: Hono) {
return ServeError(ctx, 403, "you do not own this file") return ServeError(ctx, 403, "you do not own this file")
} }
let token = (await auth.tokenFor(ctx))!
if ( if (
auth.getType(auth.tokenFor(ctx)!) != "User" && auth.getType(token) != "User" &&
auth auth
.getPermissions(auth.tokenFor(ctx)!)! .getScopes(token)!
.includes("private") .includes("private")
) { ) {
return ServeError(ctx, 403, "insufficient permissions") return ServeError(ctx, 403, "insufficient permissions")

View file

@ -12,6 +12,7 @@ import * as auth from "../../../lib/auth.js"
import { import {
getAccount, getAccount,
login, login,
mirror,
requiresAccount, requiresAccount,
scheme scheme
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
@ -27,7 +28,7 @@ const router = new Hono<{
router.use(getAccount) router.use(getAccount)
export default function (files: Files) { export default function (files: Files, apiRoot: Hono) {
router.post("/",scheme(z.object({ router.post("/",scheme(z.object({
username: AccountSchemas.Username, username: AccountSchemas.Username,
password: AccountSchemas.StringPassword password: AccountSchemas.StringPassword
@ -58,17 +59,12 @@ export default function (files: Files) {
return ctx.text("logged in") return ctx.text("logged in")
}) })
router.get("/", requiresAccount, ctx => { router.get("/", requiresAccount, async ctx => {
let sessionToken = auth.tokenFor(ctx) return ctx.json(auth.resolve((await auth.tokenFor(ctx))!)!)
return ctx.json({
expiry: auth.Db.data.find(
(e) => e.token == sessionToken
)?.expire,
})
}) })
router.delete("/", requiresAccount, (ctx) => { router.delete("/", requiresAccount, async ctx => {
auth.invalidate(auth.tokenFor(ctx)!) auth.invalidate((await auth.tokenFor(ctx))!)
return ctx.text("logged out") return ctx.text("logged out")
}) })