better code: expect ApiError from ApiHandlers
Massimo Melina committed
Jan 10, 2022 at 14:30 UTC
2a89d184574bc5d2526423069974fd7367317a05
4 files changed
+30
-23
src/api.auth.ts
+13
-14
@@ -1,7 +1,7 @@
1
import { getAccount, saveSrpInfo, updateAccount } from './perm'
2
import { verifyPassword } from './crypt'
3
import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
4
-import { ApiHandler } from './apis'
4
+import { ApiError, ApiHandler } from './apis'
5
import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
6
import { SESSION_DURATION } from './index'
7
@@ -15,17 +15,17 @@ function makeExp() {
15
16
export const login: ApiHandler = async ({ username, password }, ctx) => {
17
if (!username)
18
- return ctx.status = 400
18
+ return new ApiError(400)
19
if (!password)
20
- return ctx.status = 400
20
+ return new ApiError(400)
21
username = username.toLocaleLowerCase()
22
const acc = getAccount(username)
23
if (!acc)
24
- return ctx.status = 401
24
+ return new ApiError(401)
25
if (!acc.hashed_password)
26
- return ctx.status = 406
26
+ return new ApiError(406)
27
if (!await verifyPassword(acc.hashed_password, password))
28
- return ctx.status = 401
28
+ return new ApiError(401)
29
if (ctx.session)
30
ctx.session.username = username
31
return makeExp()
@@ -33,15 +33,15 @@ export const login: ApiHandler = async ({ username, password }, ctx) => {
33
34
export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
35
if (!username)
36
- return ctx.status = 400
36
+ return new ApiError(400)
37
username = username.toLocaleLowerCase()
38
const account = getAccount(username)
39
if (!ctx.session)
40
return ctx.throw(500)
41
if (!account) // TODO simulate fake account to prevent knowing valid usernames
42
- return ctx.status = 401
42
+ return new ApiError(401)
43
if (!account.srp)
44
- return ctx.status = 406 // unacceptable
44
+ return new ApiError(406) // unacceptable
45
46
const [salt, verifier] = account.srp.split('|')
47
const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
@@ -64,8 +64,7 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
64
return { proof: String(M2), ...makeExp() }
65
}
66
catch(e) {
67
- ctx.body = String(e)
68
- ctx.status = 401
67
+ return new ApiError(401, String(e))
68
}
69
}
70
@@ -84,7 +83,7 @@ export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
83
if (!newPassword) // clear text version
84
return Error('missing parameters')
85
if (!ctx.account)
87
- return ctx.status = 401
86
+ return new ApiError(401)
87
await updateAccount(ctx.account, account => {
88
account.password = newPassword
89
})
@@ -93,11 +92,11 @@ export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
92
93
export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
94
if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
96
- return ctx.status = 406
95
+ return new ApiError(406)
96
if (!salt || !verifier)
97
return Error('missing parameters')
98
if (!ctx.account)
100
- return ctx.status = 401
99
+ return new ApiError(401)
100
await updateAccount(ctx.account, account => {
101
saveSrpInfo(account, salt, verifier)
102
delete account.hashed_password // remove leftovers
src/api.file_list.ts
+2
-2
@@ -2,7 +2,7 @@ import { vfs, VfsNode, walkNode } from './vfs'
2
import _ from 'lodash'
3
import createSSE from './sse'
4
import { basename } from 'path'
5
-import { ApiHandler } from './apis'
5
+import { ApiError, ApiHandler } from './apis'
6
import { stat } from 'fs/promises'
7
8
export const file_list:ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
@@ -10,7 +10,7 @@ export const file_list:ApiHandler = async ({ path, offset, limit, search, omit,
10
if (!node)
11
return
12
if (search?.includes('..'))
13
- return ctx.throw(400)
13
+ return new ApiError(400)
14
if (node.default)
15
return { redirect: path }
16
offset = Number(offset)
src/apis.ts
+13
-6
@@ -1,29 +1,36 @@
1
import Koa from 'koa'
2
3
-export type ApiHandler = (params:any, ctx:Koa.Context) => any
3
+export class ApiError extends Error {
4
+ constructor(public status:number, message?:string | Error) {
5
+ super(typeof message === 'string' ? message : message?.message)
6
+ }
7
+}
8
+type ApiHandlerResult = Record<string,any> | ApiError
9
+export type ApiHandler = (params:any, ctx:Koa.Context) => ApiHandlerResult | Promise<ApiHandlerResult>
10
export type ApiHandlers = Record<string, ApiHandler>
11
12
export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
13
return async (ctx, next) => {
14
const params = ctx.method === 'POST' ? ctx.request.body : ctx.request.query
15
console.debug('API', ctx.method, ctx.path, { ...params })
10
- if (!(ctx.path in apis)) {
16
+ if (!apis.hasOwnProperty(ctx.path)) {
17
ctx.body = 'invalid api'
18
return ctx.status = 404
19
}
14
- const cb = (apis as any)[ctx.path]
20
let res
21
try {
17
- res = await cb(params||{}, ctx)
22
+ res = await apis[ctx.path](params || {}, ctx)
23
}
24
catch(e) {
25
ctx.throw(500, String(e))
26
}
27
if (res)
23
- if (res instanceof Error)
28
+ if (res instanceof ApiError)
29
+ ctx.throw(res.status, res.message)
30
+ else if (res instanceof Error)
31
ctx.throw(400, res)
32
else
26
- ctx.body = res === true ? {} : res
33
+ ctx.body = res
34
await next()
35
}
36
}
todo.md
+2
-1
@@ -4,6 +4,7 @@
4
- update tests to SRP login
5
- anti-csrf
6
- upload
7
+- https
8
- updater (stop,unzip,start)
9
- search and login dialogs should push to history so that mobile can use back button to close them
10
- node.comment
@@ -11,10 +12,10 @@
12
- account.redirect
13
- config: bans
14
- config: min disk space
15
+- thumbnails support
16
- link to parent folder in the list (as an option of the frontend? plugin?)
17
- archive for search results
18
- archive only selected files
17
-- https
19
- webdav?
20
- vfs: ability to remove/hide/rename files deep in a source
21
- administration interface