better code
Massimo Melina committed
Feb 28, 2026 at 16:34 UTC
fc14f6b32301675b0f49d4bafd79983e07fa6ad6
3 files changed
+108
-103
src/api.auth.ts
+104
-99
@@ -3,7 +3,7 @@
3
import {
4
accountCanLogin, accountIsDisabled, accountCanChangePassword, changeSrpHelper, expandUsername, getAccount
5
} from './perm'
6
-import { ApiError, ApiHandler } from './apiMiddleware'
6
+import { ApiError, ApiHandler, ApiHandlers } from './apiMiddleware'
7
import { SRPServerSessionStep1 } from 'tssrp6a'
8
import {
9
ADMIN_URI,
@@ -19,97 +19,7 @@ import { apiAssertTypes } from './misc'
19
const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
20
const keepSessionAlive = defineConfig('keep_session_alive', true)
21
22
-export const login: ApiHandler = async ({ username, password }, ctx) => {
23
- if (!username)
24
- return new ApiError(HTTP_BAD_REQUEST)
25
- if (!ctx.session)
26
- return new ApiError(HTTP_SERVER_ERROR)
27
- try {
28
- const account = await clearTextLogin(ctx, username, password, 'api')
29
- if (!account)
30
- return new ApiError(HTTP_UNAUTHORIZED)
31
- await setLoggedIn(ctx, account.username)
32
- }
33
- catch (e) {
34
- return new ApiError(HTTP_UNAUTHORIZED, String(e))
35
- }
36
- return {
37
- redirect: ctx.state.account?.redirect,
38
- ...await refresh_session({},ctx)
39
- }
40
-}
41
-
42
-export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
43
- apiAssertTypes({ string: { username } })
44
- if (!username)
45
- return new ApiError(HTTP_BAD_REQUEST)
46
- const account = getAccount(username)
47
- if (!ctx.session)
48
- return new ApiError(HTTP_SERVER_ERROR)
49
- if (account?.plugin?.auth) // tell client to do clear-text login, before firing attemptingLogin, before triggering anti-brute
50
- return new ApiError(HTTP_METHOD_NOT_ALLOWED)
51
- if ((await events.emitAsync('attemptingLogin', { ctx, username }))?.isDefaultPrevented()) return
52
- if (!account || !accountCanLogin(account)) { // TODO simulate fake account to prevent knowing valid usernames
53
- ctx.logExtra({ u: username })
54
- ctx.state.dontLog = false // log even if log_api is false
55
- return new ApiError(HTTP_UNAUTHORIZED, account && accountIsDisabled(account) ? 'Account disabled' : undefined)
56
- }
57
- if (failAllowNet(ctx, account))
58
- return new ApiError(HTTP_UNAUTHORIZED)
59
- try {
60
- const { srpServer, ...rest } = await srpServerStep1(account)
61
- const sid = Math.random()
62
- ongoingLogins[sid] = srpServer
63
- setTimeout(()=> delete ongoingLogins[sid], 60_000)
64
- ctx.session.loggingIn = { username, sid } // temporarily store until process is complete
65
- return rest
66
- }
67
- catch (code: any) {
68
- return new ApiError(code)
69
- }
70
-}
71
-
72
-export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
73
- if (!ctx.session)
74
- return new ApiError(HTTP_SERVER_ERROR)
75
- if (!ctx.session.loggingIn)
76
- return new ApiError(HTTP_CONFLICT)
77
- const { username, sid } = ctx.session.loggingIn
78
- delete ctx.session.loggingIn
79
- const step1 = ongoingLogins[sid]
80
- if (!step1)
81
- return new ApiError(HTTP_NOT_FOUND)
82
- try {
83
- const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
84
- .catch(() => { throw '' })
85
- await setLoggedIn(ctx, username)
86
- return {
87
- proof: String(M2),
88
- redirect: ctx.state.account?.redirect,
89
- ...await refresh_session({},ctx)
90
- }
91
- }
92
- catch(e) {
93
- ctx.logExtra({ u: username })
94
- ctx.state.dontLog = false // log even if log_api is false
95
- events.emit('failedLogin', { ctx, username })
96
- return new ApiError(HTTP_UNAUTHORIZED, e ? String(e) : undefined)
97
- }
98
- finally {
99
- delete ongoingLogins[sid]
100
- }
101
-}
102
-
103
-// this api is here for consistency, but frontend is actually using
104
-export const logout: ApiHandler = async ({}, ctx) => {
105
- if (!ctx.session)
106
- return new ApiError(HTTP_SERVER_ERROR)
107
- await setLoggedIn(ctx, false)
108
- // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
109
- return new ApiError(HTTP_UNAUTHORIZED)
110
-}
111
-
112
-export const refresh_session: ApiHandler = async ({}, ctx) => {
22
+const refresh_session: ApiHandler = async ({}, ctx) => {
23
const username = getCurrentUsername(ctx)
24
return !ctx.session ? new ApiError(HTTP_SERVER_ERROR) : {
25
username,
@@ -122,10 +32,105 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
32
}
33
}
34
125
-export const change_my_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
126
- const a = ctx.state.account
127
- return !a || !accountCanChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
128
- : changeSrpHelper(a, salt, verifier).then(() => {
129
- delete a.require_password_change
130
- })
131
-}
35
+export const authApis = {
36
+
37
+ async login({ username, password }, ctx) {
38
+ if (!username)
39
+ return new ApiError(HTTP_BAD_REQUEST)
40
+ if (!ctx.session)
41
+ return new ApiError(HTTP_SERVER_ERROR)
42
+ try {
43
+ const account = await clearTextLogin(ctx, username, password, 'api')
44
+ if (!account)
45
+ return new ApiError(HTTP_UNAUTHORIZED)
46
+ await setLoggedIn(ctx, account.username)
47
+ }
48
+ catch (e) {
49
+ return new ApiError(HTTP_UNAUTHORIZED, String(e))
50
+ }
51
+ return {
52
+ redirect: ctx.state.account?.redirect,
53
+ ...await refresh_session({}, ctx)
54
+ }
55
+ },
56
+
57
+ async loginSrp1({ username }, ctx) {
58
+ apiAssertTypes({ string: { username } })
59
+ if (!username)
60
+ return new ApiError(HTTP_BAD_REQUEST)
61
+ const account = getAccount(username)
62
+ if (!ctx.session)
63
+ return new ApiError(HTTP_SERVER_ERROR)
64
+ if (account?.plugin?.auth) // tell client to do clear-text login, before firing attemptingLogin, before triggering anti-brute
65
+ return new ApiError(HTTP_METHOD_NOT_ALLOWED)
66
+ if ((await events.emitAsync('attemptingLogin', { ctx, username }))?.isDefaultPrevented()) return
67
+ if (!account || !accountCanLogin(account)) { // TODO simulate fake account to prevent knowing valid usernames
68
+ ctx.logExtra({ u: username })
69
+ ctx.state.dontLog = false // log even if log_api is false
70
+ return new ApiError(HTTP_UNAUTHORIZED, account && accountIsDisabled(account) ? 'Account disabled' : undefined)
71
+ }
72
+ if (failAllowNet(ctx, account))
73
+ return new ApiError(HTTP_UNAUTHORIZED)
74
+ try {
75
+ const { srpServer, ...rest } = await srpServerStep1(account)
76
+ const sid = Math.random()
77
+ ongoingLogins[sid] = srpServer
78
+ setTimeout(()=> delete ongoingLogins[sid], 60_000)
79
+ ctx.session.loggingIn = { username, sid } // temporarily store until process is complete
80
+ return rest
81
+ }
82
+ catch (code: any) {
83
+ return new ApiError(code)
84
+ }
85
+ },
86
+
87
+ async loginSrp2({ pubKey, proof }, ctx) {
88
+ if (!ctx.session)
89
+ return new ApiError(HTTP_SERVER_ERROR)
90
+ if (!ctx.session.loggingIn)
91
+ return new ApiError(HTTP_CONFLICT)
92
+ const { username, sid } = ctx.session.loggingIn
93
+ delete ctx.session.loggingIn
94
+ const step1 = ongoingLogins[sid]
95
+ if (!step1)
96
+ return new ApiError(HTTP_NOT_FOUND)
97
+ try {
98
+ const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
99
+ .catch(() => { throw '' })
100
+ await setLoggedIn(ctx, username)
101
+ return {
102
+ proof: String(M2),
103
+ redirect: ctx.state.account?.redirect,
104
+ ...await refresh_session({}, ctx)
105
+ }
106
+ }
107
+ catch(e) {
108
+ ctx.logExtra({ u: username })
109
+ ctx.state.dontLog = false // log even if log_api is false
110
+ events.emit('failedLogin', { ctx, username })
111
+ return new ApiError(HTTP_UNAUTHORIZED, e ? String(e) : undefined)
112
+ }
113
+ finally {
114
+ delete ongoingLogins[sid]
115
+ }
116
+ },
117
+
118
+ // this api is here for consistency, but frontend is actually using
119
+ async logout({}, ctx) {
120
+ if (!ctx.session)
121
+ return new ApiError(HTTP_SERVER_ERROR)
122
+ await setLoggedIn(ctx, false)
123
+ // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
124
+ return new ApiError(HTTP_UNAUTHORIZED)
125
+ },
126
+
127
+ refresh_session,
128
+
129
+ async change_my_srp({ salt, verifier }, ctx) {
130
+ const a = ctx.state.account
131
+ return !a || !accountCanChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
132
+ : changeSrpHelper(a, salt, verifier).then(() => {
133
+ delete a.require_password_change
134
+ })
135
+ }
136
+} as const satisfies ApiHandlers
src/frontEndApis.ts
+2
-2
@@ -2,7 +2,7 @@
2
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { get_file_list } from './api.get_file_list'
5
-import * as api_auth from './api.auth'
5
+import { authApis } from './api.auth'
6
import events from './events'
7
import Koa from 'koa'
8
import { isValidFileName } from './util-files'
@@ -27,7 +27,7 @@ const partialFolderSize: any = {}
27
28
export const frontEndApis: ApiHandlers = {
29
get_file_list,
30
- ...api_auth,
30
+ ...authApis,
31
32
get_notifications({ channel }, ctx) {
33
apiAssertTypes({ string: { channel } })
src/serveGuiFiles.ts
+2
-2
@@ -7,7 +7,7 @@ import {
7
} from './const'
8
import { serveFile } from './serveFile'
9
import { getPluginConfigFields, getPluginInfo, mapPlugins, pluginsConfig } from './plugins'
10
-import { refresh_session } from './api.auth'
10
+import { authApis } from './api.auth'
11
import { ApiError } from './apiMiddleware'
12
import { join, extname } from 'path'
13
import {
@@ -70,7 +70,7 @@ const getFaviconTimestamp = debounceAsync(async () => {
70
}, { retain: 5_000 })
71
72
async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
73
- const session = await refresh_session({}, ctx)
73
+ const session = await authApis.refresh_session({}, ctx)
74
ctx.set('etag', '')
75
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
76
ctx.type = 'html'