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>
177 lines
5.4 KiB
TypeScript
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
|
|
})
|
|
});
|
|
}
|
|
}
|