fix: avoid firing 'login' event to plugins and then 'logout' immediately because the account is disabled or because of allow_net

Massimo Melina committed Feb 12, 2026 at 17:48 UTC d34e1c436ff6b9c34c5444cb13c9b8f0748b9992
3 files changed +22 -14
src/api.auth.ts
+1
@@ -27,6 +27,7 @@ export const login: ApiHandler = async ({ username, password }, ctx) => {
27 const account = await clearTextLogin(ctx, username, password, 'api')
28 if (!account)
29 return new ApiError(HTTP_UNAUTHORIZED)
30 + await setLoggedIn(ctx, account.username)
31 }
32 catch (e) {
33 return new ApiError(HTTP_UNAUTHORIZED, String(e))
src/auth.ts
+3 -6
@@ -43,11 +43,7 @@ export async function clearTextLogin(ctx: Context, u: string, p: string, via: st
43 if ((await events.emitAsync('attemptingLogin', { ctx, username: u, via }))?.isDefaultPrevented()) return
44 const plugins = await events.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }) // provide clear password to plugins
45 const a = plugins?.some(x => x === true) ? getAccount(u) : await srpCheck(u, p)
46 - if (a) {
47 - await setLoggedIn(ctx, a.username)
48 - ctx.headers['x-username'] = a.username // give an easier way to determine if the login was successful
49 - }
50 - else if (u)
46 + if (!a && u)
47 events.emit('failedLogin', { ctx, username: u, via })
48 return a
49 }
@@ -59,7 +55,8 @@ export async function setLoggedIn(ctx: Context, username: string | false) {
55 return ctx.throw(HTTP_SERVER_ERROR,'session')
56 delete ctx.state.usernames
57 if (username === false) {
62 - events.emit('logout', ctx)
58 + if (s.username)
59 + events.emit('logout', ctx)
60 delete ctx.state.account
61 delete s.username
62 delete s.allowNet
src/middlewares.ts
+18 -8
@@ -88,16 +88,26 @@ export function getProxyDetected() {
88 }
89
90 export const prepareState: Koa.Middleware = async (ctx, next) => {
91 - if (ctx.session?.username) {
92 - if (ctx.session.ts < invalidateSessionBefore.get(ctx.session.username)!)
93 - delete ctx.session.username
94 - ctx.session.maxAge = sessionDuration.compiled()
91 + const s = ctx.session
92 + if (s?.username) {
93 + if (s.ts < invalidateSessionBefore.get(s?.username)!)
94 + delete s.username
95 + s.maxAge = sessionDuration.compiled()
96 }
97 // calculate these once and for all
98 ctx.state.connection = socket2connection(ctx.socket)!
98 - const a = ctx.state.account = await urlLogin() || await getHttpAccount() || getAccount(ctx.session?.username, false)
99 - if (a && (!accountCanLogin(a) || failAllowNet(ctx, a))) // enforce allow_net also after login
100 - await setLoggedIn(ctx, false)
99 + let a = await urlLogin() || await getHttpAccount()
100 + const loggedInNotBySession = a
101 + ctx.state.account = a ||= getAccount(s?.username, false) // with least precedence, we consider session
102 + if (a)
103 + if (!accountCanLogin(a) || failAllowNet(ctx, a)) // enforce allow_net also after login
104 + await setLoggedIn(ctx, false)
105 + else if (loggedInNotBySession) {
106 + if (a.username)
107 + await setLoggedIn(ctx, a.username)
108 + ctx.headers['x-username'] = a.username // give an easier way to determine if the login was successful
109 + }
110 +
111 ctx.state.revProxyPath = ctx.get('x-forwarded-prefix')
112 updateConnectionForCtx(ctx)
113 await next()
@@ -115,7 +125,7 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
125 if (!b64) return
126 try {
127 const [u, p] = atob(b64).split(':')
118 - if (!u || u === ctx.session?.username) return // providing credentials, but not needed
128 + if (!u || u === s?.username) return // providing credentials, but not needed
129 return clearTextLogin(ctx, u, p||'', 'header')
130 }
131 catch {}