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 {}