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'