diff --git a/docs/systems/auth.md b/docs/systems/auth.md index e5b50248..79dad638 100644 --- a/docs/systems/auth.md +++ b/docs/systems/auth.md @@ -149,7 +149,7 @@ If `requestedAvatarColor` is provided and is in `AVATAR_COLORS`, use it. Otherwi 4. Check username uniqueness (exact match on lowercased username) 5. Hash password (bcrypt, 12 rounds) 6. Generate Snowflake ID -7. Insert user row with `status: 'online'`, admin flag if first user +7. Insert user row (status defaults to `'offline'` at the schema level; it is set to `'online'` only when the client establishes a WebSocket via the WS auth path in `ws/handler.ts`). Admin flag set if first user. 8. Sign JWT with `{ userId, username }` 9. Return `{ token, user }` (user sanitized via `sanitizeUser(user, true)`) diff --git a/packages/server/src/routes/auth.ts b/packages/server/src/routes/auth.ts index 445ffa63..be717e38 100644 --- a/packages/server/src/routes/auth.ts +++ b/packages/server/src/routes/auth.ts @@ -166,12 +166,19 @@ export async function authRoutes(app: FastifyInstance): Promise { ? requestedAvatarColor : AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)]; + // Note: status is left at the schema default ('offline') and is set to + // 'online' exclusively by the WebSocket auth path (ws/handler.ts). A + // successful REST /register does not by itself imply a live connection — + // the client may never establish a WS (transient network failure, mobile + // background, error path between this 201 response and /ws connect), + // which would otherwise produce a permanently stuck-online row that no + // disconnect timer can clean up. The WS handshake will flip it to + // 'online' once a real socket attaches. db.insert(schema.users).values({ id: userId, username: trimmedUsername, displayName: displayName?.trim() || null, passwordHash, - status: 'online', isAdmin: isFirstUser ? 1 : 0, homeInstance: homeInstance || null, homeUserId: (homeInstance && homeUserId && typeof homeUserId === 'string') ? homeUserId : null,