Files
Toju/toju-app/src/app/domains/authentication/feature/login/login.component.ts
T
myxeliumandCursor d3d22846e7 fix: Bug - User login status showing as both logged in and logged out
Stop treating transient auth_required as home-session expiry, keep auth scope
consistent across login redirect and server-rail joins, and leave /login when
the in-memory user is still authenticated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 01:54:01 +02:00

177 lines
5.4 KiB
TypeScript

import {
Component,
computed,
effect,
inject,
OnInit,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Actions } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideLogIn } from '@ng-icons/lucide';
import { firstValueFrom } from 'rxjs';
import { AuthenticationService } from '../../application/services/authentication.service';
import { ServerDirectoryFacade } from '../../../server-directory';
import {
AUTH_MODE_AUTHORIZE,
buildLoginReturnQueryParams,
isAuthorizeAuthMode,
resolveSafeReturnUrl,
waitForAuthenticationOutcome
} from '../../domain/logic/auth-navigation.rules';
import { UsersActions } from '../../../../store/users/users.actions';
import { User } from '../../../../shared-kernel';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { AutoFocusDirective, SelectOnFocusDirective } from '../../../../shared/directives';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
@Component({
selector: 'app-login',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon,
AutoFocusDirective,
SelectOnFocusDirective,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [provideIcons({ lucideLogIn })],
templateUrl: './login.component.html'
})
/**
* Login form allowing existing users to authenticate against a selected server.
*/
export class LoginComponent implements OnInit {
serversSvc = inject(ServerDirectoryFacade);
servers = this.serversSvc.servers;
username = '';
password = '';
serverId: string | undefined = this.serversSvc.activeServer()?.id;
error = signal<string | null>(null);
readonly isAuthorizeMode = signal(false);
readonly authorizeServerName = computed(() => {
const sid = this.serverId || this.serversSvc.activeServer()?.id;
const endpoint = this.servers().find((server) => server.id === sid);
return endpoint?.name ?? this.appI18n.instant('auth.authorize.defaultServerName');
});
private readonly appI18n = inject(AppI18nService);
private auth = inject(AuthenticationService);
private actions$ = inject(Actions);
private store = inject(Store);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly currentUser = this.store.selectSignal(selectCurrentUser);
constructor() {
effect(() => {
if (this.isAuthorizeMode()) {
return;
}
const user = this.currentUser();
if (!user) {
return;
}
const returnUrl = resolveSafeReturnUrl(this.route.snapshot.queryParamMap.get('returnUrl'));
void this.router.navigateByUrl(returnUrl);
});
}
/** TrackBy function for server list rendering. */
trackById(_index: number, item: { id: string }) { return item.id; }
ngOnInit(): void {
const mode = this.route.snapshot.queryParamMap.get('mode');
const requestedServerId = this.route.snapshot.queryParamMap.get('serverId')?.trim();
this.isAuthorizeMode.set(isAuthorizeAuthMode(mode));
if (requestedServerId) {
this.serverId = requestedServerId;
}
}
/** Validate and submit the login form, then navigate to search on success. */
submit() {
this.error.set(null);
const sid = this.serverId || this.serversSvc.activeServer()?.id;
this.auth.login({ username: this.username.trim(),
password: this.password,
serverId: sid }).subscribe({
next: async (resp) => {
const serverUrl = this.auth.resolveServerUrlFor(sid);
if (this.isAuthorizeMode()) {
this.store.dispatch(UsersActions.authorizeSignalServer({
serverUrl,
response: resp,
provisioned: false
}));
const returnUrl = resolveSafeReturnUrl(this.route.snapshot.queryParamMap.get('returnUrl'));
await this.router.navigateByUrl(returnUrl);
return;
}
if (sid) {
this.serversSvc.setActiveServer(sid);
}
const homeSignalServerUrl = this.serversSvc.servers().find((server) => server.id === sid)?.url
?? this.serversSvc.activeServer()?.url;
const user: User = {
id: resp.id,
oderId: resp.id,
username: resp.username,
displayName: resp.displayName,
status: 'online',
role: 'member',
joinedAt: Date.now(),
homeSignalServerUrl
};
this.store.dispatch(UsersActions.authenticateUser({ user, loginResponse: resp }));
const outcome = await firstValueFrom(waitForAuthenticationOutcome(this.actions$));
if (outcome.kind === 'failure') {
this.error.set(outcome.error);
return;
}
const returnUrl = resolveSafeReturnUrl(this.route.snapshot.queryParamMap.get('returnUrl'));
await this.router.navigateByUrl(returnUrl);
},
error: (err) => {
this.error.set(err?.error?.error || this.appI18n.instant('auth.login.failed'));
}
});
}
/** Navigate to the registration page. */
goRegister() {
this.router.navigate(['/register'], {
queryParams: buildLoginReturnQueryParams(this.router.url, undefined, {
mode: this.isAuthorizeMode() ? AUTH_MODE_AUTHORIZE : undefined,
serverId: this.serverId
})
});
}
}