mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-06-16 18:29:08 +00:00
add tests; resync files, forgot where i was.
This commit is contained in:
parent
6b36b1d5f2
commit
1a794e2d7e
30 changed files with 3654 additions and 534 deletions
148
rpc/_security.js
148
rpc/_security.js
|
@ -3,40 +3,156 @@ import { type AppContext } from '../Roleypoly'
|
|||
import { type Context } from 'koa'
|
||||
import RPCError from './_error'
|
||||
|
||||
// import logger from '../logger'
|
||||
// const log = logger(__filename)
|
||||
import logger from '../logger'
|
||||
const log = logger(__filename)
|
||||
|
||||
const PermissionError = new RPCError('User does not have permission to call this RPC.', 403)
|
||||
|
||||
// export const bot = (fn: Function) => (secret: string, ...args: any[]) => {
|
||||
// if (secret !== process.env.SHARED_SECRET) {
|
||||
// log.error('unauthenticated bot request', { secret })
|
||||
// return { err: 'unauthenticated' }
|
||||
// }
|
||||
const logFacts = (
|
||||
ctx: Context,
|
||||
extra: { [x:string]: any } = {}
|
||||
) => ({
|
||||
fn: (ctx.request.body: any).fn,
|
||||
ip: ctx.ip,
|
||||
user: ctx.session.userId,
|
||||
...extra
|
||||
})
|
||||
|
||||
// return fn(...args)
|
||||
// }
|
||||
export const authed = (
|
||||
$: AppContext,
|
||||
fn: (ctx: Context, ...args: any[]) => any,
|
||||
silent: boolean = false
|
||||
) => async (
|
||||
ctx: Context,
|
||||
...args: any[]
|
||||
) => {
|
||||
if (await $.auth.isLoggedIn(ctx)) {
|
||||
return fn(ctx, ...args)
|
||||
}
|
||||
|
||||
export const root = ($: AppContext, fn: Function) => (ctx: Context, ...args: any[]) => {
|
||||
if ($.config.dev) {
|
||||
log.debug('authed failed')
|
||||
throw new RPCError('User is not logged in', 403)
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
log.info('RPC call authed check fail', logFacts(ctx))
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
}
|
||||
|
||||
export const root = (
|
||||
$: AppContext,
|
||||
fn: (ctx: Context, ...args: any[]) => any,
|
||||
silent: boolean = false
|
||||
) => authed($, (
|
||||
ctx: Context,
|
||||
...args: any[]
|
||||
) => {
|
||||
if ($.discord.isRoot(ctx.session.userId)) {
|
||||
return fn(ctx, ...args)
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
}
|
||||
if ($.config.dev) {
|
||||
log.debug('root failed')
|
||||
throw new RPCError('User is not root', 403)
|
||||
}
|
||||
|
||||
export const manager = ($: AppContext, fn: Function) => (ctx: Context, server: string, ...args: any[]) => {
|
||||
if (!silent) {
|
||||
log.info('RPC call root check fail', logFacts(ctx))
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
})
|
||||
|
||||
export const manager = (
|
||||
$: AppContext,
|
||||
fn: (ctx: Context, server: string, ...args: any[]) => any,
|
||||
silent: boolean = false
|
||||
) => member($, (
|
||||
ctx: Context,
|
||||
server: string,
|
||||
...args: any[]
|
||||
) => {
|
||||
if ($.discord.canManageRoles(server, ctx.session.userId)) {
|
||||
return fn(ctx, server, ...args)
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
}
|
||||
if ($.config.dev) {
|
||||
log.debug('manager failed')
|
||||
throw new RPCError('User is not a role manager', 403)
|
||||
}
|
||||
|
||||
export const member = ($: AppContext, fn: Function) => (ctx: Context, server: string, ...args: any[]) => {
|
||||
if (!silent) {
|
||||
log.info('RPC call manager check fail', logFacts(ctx, { server }))
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
})
|
||||
|
||||
export const member = (
|
||||
$: AppContext,
|
||||
fn: (ctx: Context, server: string, ...args: any[]) => any,
|
||||
silent: boolean = false
|
||||
) => authed($, (
|
||||
ctx: Context,
|
||||
server: string,
|
||||
...args: any[]
|
||||
) => {
|
||||
if ($.discord.isMember(server, ctx.session.userId)) {
|
||||
return fn(ctx, server, ...args)
|
||||
}
|
||||
|
||||
if ($.config.dev) {
|
||||
log.debug('member failed')
|
||||
throw new RPCError('User is not a member of this server', 403)
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
log.info('RPC call member check fail', logFacts(ctx, { server }))
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
})
|
||||
|
||||
export const any = (
|
||||
$: AppContext,
|
||||
fn: (ctx: Context, ...args: any[]) => any,
|
||||
silent: boolean = false
|
||||
) => (...args: any) => fn(...args)
|
||||
|
||||
type Handler = (ctx: Context, ...args: any[]) => any
|
||||
type Strategy = (
|
||||
$: AppContext,
|
||||
fn: Handler,
|
||||
silent?: boolean
|
||||
) => any
|
||||
type StrategyPair = [ Strategy, Handler ]
|
||||
|
||||
/**
|
||||
* Weird func but ok -- test that a strategy doesn't fail, and run the first that doesn't.
|
||||
*/
|
||||
export const decide = (
|
||||
$: AppContext,
|
||||
...strategies: StrategyPair[]
|
||||
) => async (...args: any) => {
|
||||
for (let [ strat, handler ] of strategies) {
|
||||
if (strat === null) {
|
||||
strat = any
|
||||
}
|
||||
|
||||
try {
|
||||
return await strat($, handler, true)(...args)
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach the end, just throw
|
||||
if ($.config.dev) {
|
||||
log.info('decide failed for', strategies.map(v => v[0].name))
|
||||
}
|
||||
|
||||
throw PermissionError
|
||||
}
|
||||
|
|
32
rpc/index.js
32
rpc/index.js
|
@ -3,11 +3,20 @@ import logger from '../logger'
|
|||
import fnv from 'fnv-plus'
|
||||
import autoloader from './_autoloader'
|
||||
import RPCError from './_error'
|
||||
import type Roleypoly from '../Roleypoly'
|
||||
import type betterRouter from 'koa-better-router'
|
||||
import { type Context } from 'koa'
|
||||
import type Roleypoly, { Router } from '../Roleypoly'
|
||||
import type { Context } from 'koa'
|
||||
const log = logger(__filename)
|
||||
|
||||
export type RPCIncoming = {
|
||||
fn: string,
|
||||
args: any[]
|
||||
}
|
||||
|
||||
export type RPCOutgoing = {
|
||||
hash: string,
|
||||
response: any
|
||||
}
|
||||
|
||||
export default class RPCServer {
|
||||
ctx: Roleypoly
|
||||
|
||||
|
@ -21,6 +30,7 @@ export default class RPCServer {
|
|||
constructor (ctx: Roleypoly) {
|
||||
this.ctx = ctx
|
||||
this.reload()
|
||||
ctx.addRouteHook(this.hookRoutes)
|
||||
}
|
||||
|
||||
reload () {
|
||||
|
@ -32,33 +42,33 @@ export default class RPCServer {
|
|||
this.mapHash = fnv.hash(Object.keys(this.rpcMap)).str()
|
||||
|
||||
// call map for the client.
|
||||
this.rpcCalls = Object.keys(this.rpcMap).map(fn => ({ name: this.rpcMap[fn].name, args: 0 }))
|
||||
this.rpcCalls = Object.keys(this.rpcMap).map(fn => ({ name: fn, args: 0 }))
|
||||
}
|
||||
|
||||
hookRoutes (router: betterRouter) {
|
||||
hookRoutes = (router: Router) => {
|
||||
// RPC call reporter.
|
||||
// this is NEVER called in prod.
|
||||
// it is used to generate errors if RPC calls don't exist or are malformed in dev.
|
||||
router.get('/api/_rpc', async (ctx) => {
|
||||
ctx.body = {
|
||||
router.get('/api/_rpc', async (ctx: Context) => {
|
||||
ctx.body = ({
|
||||
hash: this.mapHash,
|
||||
available: this.rpcCalls
|
||||
}
|
||||
}: any)
|
||||
ctx.status = 200
|
||||
return true
|
||||
})
|
||||
|
||||
router.post('/api/_rpc', this.handleRPC.bind(this))
|
||||
router.post('/api/_rpc', this.handleRPC)
|
||||
}
|
||||
|
||||
async handleRPC (ctx: Context) {
|
||||
handleRPC = async (ctx: Context) => {
|
||||
// handle an impossible situation
|
||||
if (!(ctx.request.body instanceof Object)) {
|
||||
return this.rpcError(ctx, null, new RPCError('RPC format was very incorrect.', 400))
|
||||
}
|
||||
|
||||
// check if RPC exists
|
||||
const { fn, args } = ctx.request.body
|
||||
const { fn, args } = (ctx.request.body: RPCIncoming)
|
||||
|
||||
if (!(fn in this.rpcMap)) {
|
||||
return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}(...) not found.`, 404))
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
// @flow
|
||||
import { type AppContext } from '../Roleypoly'
|
||||
import { type Context } from 'koa'
|
||||
import * as secureAs from './_security'
|
||||
export default ($: AppContext) => ({
|
||||
// i want RPC to be *dead* simple.
|
||||
// probably too simple.
|
||||
hello (_: Context, hello: string) {
|
||||
return `hello, ${hello}!`
|
||||
return `hello, ${hello} and all who inhabit it`
|
||||
},
|
||||
|
||||
testJSON (_: Context, inobj: {}) {
|
||||
return inobj
|
||||
}
|
||||
},
|
||||
|
||||
testDecide: secureAs.decide($,
|
||||
[ secureAs.root, () => { return 'root' } ],
|
||||
[ secureAs.manager, () => { return 'manager' } ],
|
||||
[ secureAs.member, () => { return 'member' } ],
|
||||
[ secureAs.authed, () => { return 'authed' } ],
|
||||
[ secureAs.any, () => { return 'guest' } ]
|
||||
)
|
||||
})
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
import { type AppContext } from '../Roleypoly'
|
||||
import { type Context } from 'koa'
|
||||
import { type Guild } from 'eris'
|
||||
import { root } from './_security'
|
||||
import * as secureAs from './_security'
|
||||
|
||||
export default ($: AppContext) => ({
|
||||
|
||||
rootGetAllServers: root($, (ctx: Context) => {
|
||||
rootGetAllServers: secureAs.root($, (ctx: Context) => {
|
||||
return $.discord.client.guilds.map<{
|
||||
url: string,
|
||||
name: string,
|
||||
|
@ -24,7 +24,7 @@ export default ($: AppContext) => ({
|
|||
return $.P.serverSlug(srv)
|
||||
},
|
||||
|
||||
getServer (ctx: Context, id: string) {
|
||||
getServer: secureAs.member($, (ctx: Context, id: string) => {
|
||||
const { userId } = (ctx.session: { userId: string })
|
||||
|
||||
const srv = $.discord.client.guilds.get(id)
|
||||
|
@ -32,13 +32,6 @@ export default ($: AppContext) => ({
|
|||
return { err: 'not_found' }
|
||||
}
|
||||
|
||||
if (userId == null) {
|
||||
return {
|
||||
id: id,
|
||||
server: $.P.serverSlug(srv)
|
||||
}
|
||||
}
|
||||
|
||||
let gm
|
||||
if (srv.members.has(userId)) {
|
||||
gm = $.discord.gm(id, userId)
|
||||
|
@ -51,5 +44,5 @@ export default ($: AppContext) => ({
|
|||
}
|
||||
|
||||
return $.P.presentableServer(srv, gm)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
15
rpc/user.js
15
rpc/user.js
|
@ -1,11 +1,18 @@
|
|||
// @flow
|
||||
import { type AppContext } from '../Roleypoly'
|
||||
import { type Context } from 'koa'
|
||||
// import { type Guild } from 'discord.js'
|
||||
import * as secureAs from './_security'
|
||||
|
||||
export default ($: AppContext) => ({
|
||||
getCurrentUser (ctx: Context) {
|
||||
const { userId } = ctx.session
|
||||
|
||||
getCurrentUser: secureAs.authed($, (ctx: Context) => {
|
||||
return $.discord.getUserPartial(ctx.session.userId)
|
||||
}
|
||||
}),
|
||||
|
||||
isRoot: secureAs.root($, () => {
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue