mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-24 22:56:26 -08:00
Merge pull request #13 from nbitzz/token-permissions
Add permissions to tokens
This commit is contained in:
commit
aa840e176f
|
@ -1,19 +1,46 @@
|
|||
import crypto from "crypto"
|
||||
import express from "express"
|
||||
import { readFile, writeFile } from "fs/promises"
|
||||
export let AuthTokens: AuthToken[] = []
|
||||
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
|
||||
|
||||
export const ValidTokenPermissions = [
|
||||
"user", // permissions to /auth/me, with email docked
|
||||
"email", // adds email back to /auth/me
|
||||
"private", // allows app to read private files
|
||||
"upload", // allows an app to upload under an account
|
||||
"manage", // allows an app to manage an account's files
|
||||
"customize", // allows an app to change customization settings
|
||||
"admin" // only available for accounts with admin
|
||||
// gives an app access to all admin tools
|
||||
] as const
|
||||
|
||||
export type TokenType = "User" | "App"
|
||||
export type TokenPermission = typeof ValidTokenPermissions[number]
|
||||
|
||||
export interface AuthToken {
|
||||
account: string,
|
||||
token: string,
|
||||
expire: number
|
||||
expire: number,
|
||||
|
||||
type?: TokenType, // if !type, assume User
|
||||
tokenPermissions?: TokenPermission[] // default to user if type is App,
|
||||
// give full permissions if type is User
|
||||
}
|
||||
|
||||
export function create(id:string,expire:number=(24*60*60*1000)) {
|
||||
export function create(
|
||||
id:string,
|
||||
expire:number=(24*60*60*1000),
|
||||
type:TokenType="User",
|
||||
tokenPermissions?:TokenPermission[]
|
||||
) {
|
||||
let token = {
|
||||
account:id,
|
||||
token:crypto.randomBytes(36).toString('hex'),
|
||||
expire:Date.now()+expire
|
||||
expire: expire ? Date.now()+expire : 0,
|
||||
|
||||
type,
|
||||
tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined
|
||||
}
|
||||
|
||||
AuthTokens.push(token)
|
||||
|
@ -24,11 +51,32 @@ export function create(id:string,expire:number=(24*60*60*1000)) {
|
|||
return token.token
|
||||
}
|
||||
|
||||
export function tokenFor(req: express.Request) {
|
||||
return req.cookies.auth || (
|
||||
req.header("authorization")?.startsWith("Bearer ")
|
||||
? req.header("authorization")?.split(" ")[1]
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
function getToken(token:string) {
|
||||
return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire))
|
||||
}
|
||||
|
||||
export function validate(token:string) {
|
||||
return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account
|
||||
return getToken(token)?.account
|
||||
}
|
||||
|
||||
export function getType(token:string): TokenType | undefined {
|
||||
return getToken(token)?.type
|
||||
}
|
||||
|
||||
export function getPermissions(token:string): TokenPermission[] | undefined {
|
||||
return getToken(token)?.tokenPermissions
|
||||
}
|
||||
|
||||
export function tokenTimer(token:AuthToken) {
|
||||
if (!token.expire) return // justincase
|
||||
if (Date.now() >= token.expire) {
|
||||
invalidate(token.token)
|
||||
return
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import * as Accounts from "./accounts";
|
||||
import express, { type RequestHandler } from "express"
|
||||
import ServeError from "../lib/errors";
|
||||
import * as auth from "./auth";
|
||||
|
||||
export let getAccount: RequestHandler = function(req, res, next) {
|
||||
res.locals.acc = Accounts.getFromToken(req.cookies.auth || (
|
||||
req.header("authorization")?.startsWith("Bearer ")
|
||||
? req.header("authorization")?.split(" ")[1]
|
||||
: undefined
|
||||
))
|
||||
export const getAccount: RequestHandler = function(req, res, next) {
|
||||
res.locals.acc = Accounts.getFromToken(auth.tokenFor(req))
|
||||
next()
|
||||
}
|
||||
|
||||
export let requiresAccount: RequestHandler = function(_req, res, next) {
|
||||
export const requiresAccount: RequestHandler = function(_req, res, next) {
|
||||
if (!res.locals.acc) {
|
||||
ServeError(res, 401, "not logged in")
|
||||
return
|
||||
|
@ -19,10 +16,49 @@ export let requiresAccount: RequestHandler = function(_req, res, next) {
|
|||
next()
|
||||
}
|
||||
|
||||
export let requiresAdmin: RequestHandler = function(_req, res, next) {
|
||||
export const requiresAdmin: RequestHandler = function(_req, res, next) {
|
||||
if (!res.locals.acc.admin) {
|
||||
ServeError(res, 403, "you are not an administrator")
|
||||
return
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Blocks requests based on the permissions which a token has. Does not apply to routes being accessed with a token of type `User`
|
||||
* @param tokenPermissions Permissions which your route requires.
|
||||
* @returns Express middleware
|
||||
*/
|
||||
|
||||
export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler {
|
||||
return function(req, res, next) {
|
||||
let token = auth.tokenFor(req)
|
||||
let type = auth.getType(token)
|
||||
|
||||
if (type == "App") {
|
||||
let permissions = auth.getPermissions(token)
|
||||
|
||||
if (!permissions) ServeError(res, 403, "insufficient permissions")
|
||||
else {
|
||||
|
||||
for (let v of tokenPermissions) {
|
||||
if (!permissions.includes(v as auth.TokenPermission)) {
|
||||
ServeError(res,403,"insufficient permissions")
|
||||
return
|
||||
}
|
||||
}
|
||||
next()
|
||||
|
||||
}
|
||||
} else next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(req, res, next) {
|
||||
if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint")
|
||||
else next()
|
||||
}
|
|
@ -5,7 +5,7 @@ import * as auth from "../lib/auth";
|
|||
import bytes from "bytes"
|
||||
import {writeFile} from "fs";
|
||||
import { sendMail } from "../lib/mail";
|
||||
import { getAccount, requiresAccount, requiresAdmin } from "../lib/middleware"
|
||||
import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../lib/middleware"
|
||||
|
||||
import ServeError from "../lib/errors";
|
||||
import Files from "../lib/files";
|
||||
|
@ -19,6 +19,7 @@ adminRoutes
|
|||
.use(getAccount)
|
||||
.use(requiresAccount)
|
||||
.use(requiresAdmin)
|
||||
.use(requiresPermissions("admin"))
|
||||
let files:Files
|
||||
|
||||
export function setFilesObj(newFiles:Files) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from "express";
|
|||
import * as Accounts from "../lib/accounts";
|
||||
import * as auth from "../lib/auth";
|
||||
import { sendMail } from "../lib/mail";
|
||||
import { getAccount, requiresAccount } from "../lib/middleware"
|
||||
import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../lib/middleware"
|
||||
import { accountRatelimit } from "../lib/ratelimit"
|
||||
|
||||
import ServeError from "../lib/errors";
|
||||
|
@ -123,7 +123,7 @@ authRoutes.post("/logout", (req,res) => {
|
|||
res.send("logged out")
|
||||
})
|
||||
|
||||
authRoutes.post("/dfv", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
|
||||
|
@ -136,7 +136,7 @@ authRoutes.post("/dfv", requiresAccount, parser, (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/customcss", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
|
||||
|
@ -158,7 +158,7 @@ authRoutes.post("/customcss", requiresAccount, parser, (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/embedcolor", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.color != "string") req.body.color = undefined;
|
||||
|
@ -181,7 +181,7 @@ authRoutes.post("/embedcolor", requiresAccount, parser, (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/embedsize", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.largeImage != "boolean") req.body.color = false;
|
||||
|
@ -193,7 +193,7 @@ authRoutes.post("/embedsize", requiresAccount, parser, (req,res) => {
|
|||
res.send(`custom embed image size saved`)
|
||||
})
|
||||
|
||||
authRoutes.post("/delete_account", requiresAccount, parser, async (req,res) => {
|
||||
authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let accId = acc.id
|
||||
|
@ -217,7 +217,7 @@ authRoutes.post("/delete_account", requiresAccount, parser, async (req,res) => {
|
|||
} else cpl()
|
||||
})
|
||||
|
||||
authRoutes.post("/change_username", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
|
||||
|
@ -253,7 +253,7 @@ authRoutes.post("/change_username", requiresAccount, parser, (req,res) => {
|
|||
|
||||
let verificationCodes = new Map<string, {code: string, email: string, expiry: NodeJS.Timeout}>()
|
||||
|
||||
authRoutes.post("/request_email_change", requiresAccount, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
|
||||
authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
||||
|
@ -292,7 +292,7 @@ authRoutes.post("/request_email_change", requiresAccount, accountRatelimit({ req
|
|||
})
|
||||
})
|
||||
|
||||
authRoutes.get("/confirm_email/:code", requiresAccount, (req,res) => {
|
||||
authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
||||
|
@ -314,7 +314,7 @@ authRoutes.get("/confirm_email/:code", requiresAccount, (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/remove_email", requiresAccount, (req,res) => {
|
||||
authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (acc.email) {
|
||||
|
@ -404,7 +404,7 @@ authRoutes.get("/emergency_login/:code", (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/change_password", requiresAccount, parser, (req,res) => {
|
||||
authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.password != "string" || req.body.password.length < 8) {
|
||||
|
@ -429,7 +429,7 @@ authRoutes.post("/change_password", requiresAccount, parser, (req,res) => {
|
|||
res.send("password changed - logged out all sessions")
|
||||
})
|
||||
|
||||
authRoutes.post("/logout_sessions", requiresAccount, (req,res) => {
|
||||
authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let accId = acc.id
|
||||
|
@ -441,15 +441,20 @@ authRoutes.post("/logout_sessions", requiresAccount, (req,res) => {
|
|||
res.send("logged out all sessions")
|
||||
})
|
||||
|
||||
authRoutes.get("/me", requiresAccount, (req,res) => {
|
||||
authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let sessionToken = auth.tokenFor(req)
|
||||
let accId = acc.id
|
||||
res.send({
|
||||
...acc,
|
||||
sessionCount: auth.AuthTokens.filter(e => e.account == accId && e.expire > Date.now()).length,
|
||||
sessionExpires: auth.AuthTokens.find(e => e.token == req.cookies.auth)?.expire,
|
||||
password: undefined
|
||||
sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length,
|
||||
sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire,
|
||||
password: undefined,
|
||||
email:
|
||||
auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email")
|
||||
? acc.email
|
||||
: undefined
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {writeFile} from "fs";
|
|||
|
||||
import ServeError from "../lib/errors";
|
||||
import Files from "../lib/files";
|
||||
import { getAccount, requiresAccount } from "../lib/middleware";
|
||||
import { getAccount, requiresAccount, requiresPermissions } from "../lib/middleware";
|
||||
|
||||
let parser = bodyParser.json({
|
||||
type: ["text/plain","application/json"]
|
||||
|
@ -24,7 +24,7 @@ let config = require(`${process.cwd()}/config.json`)
|
|||
|
||||
fileApiRoutes.use(getAccount);
|
||||
|
||||
fileApiRoutes.get("/list", requiresAccount, (req,res) => {
|
||||
fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
@ -44,7 +44,7 @@ fileApiRoutes.get("/list", requiresAccount, (req,res) => {
|
|||
|
||||
})
|
||||
|
||||
fileApiRoutes.post("/manage", parser, (req,res) => {
|
||||
fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import multer, {memoryStorage} from "multer"
|
|||
|
||||
import ServeError from "../lib/errors";
|
||||
import Files from "../lib/files";
|
||||
import { getAccount } from "../lib/middleware";
|
||||
import { getAccount, requiresPermissions } from "../lib/middleware";
|
||||
|
||||
let parser = bodyParser.json({
|
||||
type: ["text/plain","application/json"]
|
||||
|
@ -115,7 +115,7 @@ primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.R
|
|||
|
||||
// upload handlers
|
||||
|
||||
primaryApi.post("/upload",multerSetup.single('file'),async (req,res) => {
|
||||
primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
@ -149,7 +149,7 @@ primaryApi.post("/upload",multerSetup.single('file'),async (req,res) => {
|
|||
}
|
||||
})
|
||||
|
||||
primaryApi.post("/clone", bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
|
||||
primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
|
Loading…
Reference in a new issue