Add logout functionality to helix-front#184
Conversation
Users had no way to log out or re-authenticate after their identity token expired (~24h), causing 401 errors on write operations while the UI still showed them as logged in. This adds a POST /user/logout server route that destroys the session and clears cookies, a logout() method in UserService, and a dropdown menu on the user chip with Sign In / Sign Out options. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…or handling
- Show Sign In only when logged out, Sign Out only when logged in
- Clear helixui_identity.token cookie on logout (not just connect.sid)
- Pass explicit { path: '/' } to clearCookie for robustness
- Remove catchError that silently swallowed logout errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When not logged in, clicking the user chip opens the login dialog directly (original behavior). The dropdown menu with Sign Out only appears when logged in. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| @@ -28,10 +28,24 @@ | |||
| <span class="helix-title">Helix</span> | |||
| </a> | |||
| <span fxFlex="1 1 auto"></span> | |||
| <a mat-button (click)="login()"> | |||
| <a mat-button (click)="login()" *ngIf="(currentUser | async) === 'Sign In'"> | |||
There was a problem hiding this comment.
The new template references (currentUser | async) 4 times in the toolbar. Each | async opens its own subscription and getCurrentUser() returns this.http.get(...), so a single page load fires 4 GET
/user/current calls.
Expose currentUser$ = this.service.getCurrentUser().pipe(shareReplay(1)) once and bind to that, or resolve the value into a plain field in ngOnInit. The old template was already noisy, this doubles
it.
There was a problem hiding this comment.
Added shareReplay(1) to getCurrentUser() in user.service.ts. All | async pipes now share a single HTTP call per observable instance.
| <button | ||
| mat-button | ||
| [matMenuTriggerFor]="userMenu" | ||
| *ngIf="(currentUser | async) !== 'Sign In'" |
There was a problem hiding this comment.
=== 'Sign In' / !== 'Sign In' as a logged-in branch is brittle. If /user/current ever returns null, '', or someone happens to be named "Sign In", the UI breaks. Compute an isLoggedIn boolean (or
isLoggedIn$) and gate the two buttons on that.
There was a problem hiding this comment.
Replaced string comparison with an isLoggedIn boolean, derived via tap() on the currentUser observable. Template now uses *ngIf="isLoggedIn" / *ngIf="!isLoggedIn". The tap still checks user !== 'Sign In' internally since the server returns that as the default - changing the server return value risks breaking other consumers.
| protected logout(req: HelixRequest, res: Response) { | ||
| req.session.destroy((err) => { | ||
| if (err) { | ||
| res.status(500).json({ error: 'Logout failed' }); | ||
| return; | ||
| } | ||
| res.clearCookie('connect.sid', { path: '/' }); | ||
| res.clearCookie('helixui_identity.token'); | ||
| res.json(true); | ||
| }); | ||
| } |
There was a problem hiding this comment.
req.session is typed session?: HelixSession (optional) in d.ts. If a request lands without a session (store flake, tampered cookie, whatever), req.session.destroy(...) throws TypeError and the client
gets a generic 500.
if (!req.session) {
res.json(true);
return;
}There was a problem hiding this comment.
Added if (!req.session) guard before destroy() call. Returns res.json(true) early if session is missing.
| @@ -11,4 +12,20 @@ interface HelixSession { | |||
| identityToken: any; | |||
| username: string; | |||
| isAdmin: boolean; | |||
| destroy(callback: (err?: any) => void): void; | |||
There was a problem hiding this comment.
Changed to callback?: (optional) to match express-session's actual signature. Now consistent with #186.
… null check, optional destroy callback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9620b72 to
592baf5
Compare
| @@ -116,4 +123,18 @@ export class AppComponent implements OnInit { | |||
| } | |||
| ); | |||
| } | |||
|
|
|||
| logout() { | |||
There was a problem hiding this comment.
Once #186 lands, AppComponent has a 30s setInterval watching the identity cookie. Your manual Sign Out clears the cookie but doesn't stop that interval. ~30s after the user clicks Sign Out, #186's
watcher detects "no token", pops "Session Expired", calls /logout again, and reloads. User experiences: click Sign Out -> snackbar -> 30s of normal UI -> surprise dialog.
Stop the watcher on manual logout. Either expose a stopExpiryWatch() public method from the watcher logic, or move the handle out of AppComponent into a shared service that both #186 and #184's
logout can drive.
There was a problem hiding this comment.
Added clearInterval(this.expiryCheckHandle) at the top of logout() before the API call. Stops the 30s watcher immediately so it won't detect the cleared cookie and fire a surprise dialog.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…store session null check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
POST /user/logoutserver route that destroys the Express session and clears theconnect.sidcookiedestroy()to theHelixSessionTypeScript interfacelogout()method to AngularUserServicemat-menudropdown offering Sign In and Sign Outlogout()method toAppComponentwith snackbar confirmationTesting Done
localhost:4200:helix-ui_logout_flow.mov
No duplicate HTTP calls:

Context
Users had no way to log out or re-authenticate after their identity token expired (~24h), causing 401 errors on write operations while the UI still showed them as logged in. The only workaround was manually clearing cookies in DevTools.