Files
Toju/toju-app/src/app/domains/authentication/application/services/signal-server-authorize.service.ts
T
myxelium f9e8538c80 feat(auth): recover cross-signal authorization with provision secrets
A client that could not authorize against a foreign signal server was
redirected into a dead end with no way to retry, so servers joined from
another signal route became unreachable.

The home server now stores a per-user provision secret, clients keep it in
their own store, and a recovery service records why authorization failed per
server URL. Invite, server browser, and chat room surface that reason and
offer a retry instead of silently redirecting.
2026-08-14 03:19:29 +02:00

80 lines
2.8 KiB
TypeScript

import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { ServerDirectoryFacade } from '../../../server-directory';
import { AUTH_MODE_AUTHORIZE, buildLoginReturnQueryParams } from '../../domain/logic/auth-navigation.rules';
import { isSameSignalServerUrl } from '../../domain/logic/signal-server-auth-failure.rules';
import { SignalServerAuthService } from './signal-server-auth.service';
@Injectable({ providedIn: 'root' })
export class SignalServerAuthorizeService {
private readonly router = inject(Router);
private readonly store = inject(Store);
private readonly serverDirectory = inject(ServerDirectoryFacade);
private readonly signalServerAuth = inject(SignalServerAuthService);
async ensureCredentialForServerUrl(serverUrl: string): Promise<boolean> {
if (this.signalServerAuth.hasValidCredential(serverUrl)) {
return true;
}
const currentUser = await firstValueFrom(this.store.select(selectCurrentUser));
if (!currentUser) {
return false;
}
if (isSameSignalServerUrl(serverUrl, currentUser.homeSignalServerUrl)) {
// The home account is never auto-provisioned or re-authorized as a
// foreign account. Resurrect the credential from the legacy token store
// (session-restore case); a genuinely missing home session is handled by
// the session-expiry flow, not the authorize login page.
this.signalServerAuth.migrateHomeCredential(currentUser);
return this.signalServerAuth.hasValidCredential(serverUrl);
}
let result;
try {
result = await this.signalServerAuth.ensureProvisioned(serverUrl, currentUser);
} catch {
return false;
}
if (result.kind === 'existing' || result.kind === 'provisioned') {
return true;
}
// Automatic recovery must never turn a healthy home session into a generic
// foreign-server login redirect. The contextual caller renders the
// per-server recovery action; explicit authorization remains available in
// Network settings.
return false;
}
async navigateToAuthorize(serverUrl: string, returnUrl: string): Promise<void> {
const endpoint = this.serverDirectory.ensureServerEndpoint({
name: this.buildEndpointName(serverUrl),
url: serverUrl
});
await this.router.navigate(['/login'], {
queryParams: buildLoginReturnQueryParams(returnUrl, undefined, {
mode: AUTH_MODE_AUTHORIZE,
serverId: endpoint.id
})
});
}
private buildEndpointName(serverUrl: string): string {
try {
return new URL(serverUrl).hostname;
} catch {
return serverUrl;
}
}
}