diff --git a/frontend/src/app/components/setup/setup.component.spec.ts b/frontend/src/app/components/setup/setup.component.spec.ts index 04c761019..031286bc4 100644 --- a/frontend/src/app/components/setup/setup.component.spec.ts +++ b/frontend/src/app/components/setup/setup.component.spec.ts @@ -6,7 +6,6 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideRouter, Router } from '@angular/router'; import { IPasswordStrengthMeterService } from 'angular-password-strength-meter'; import { Angulartics2Module } from 'angulartics2'; -import { of } from 'rxjs'; import { SelfhostedService } from 'src/app/services/selfhosted.service'; import { SetupComponent } from './setup.component'; @@ -53,39 +52,37 @@ describe('SetupComponent', () => { expect(testable.password()).toBe('SecurePass123'); }); - it('should not submit if email is empty', () => { + it('should not submit if email is empty', async () => { const testable = component as SetupComponentTestable; const fakeCreateInitialUser = vi.spyOn(selfhostedService, 'createInitialUser'); testable.email.set(''); testable.password.set('SecurePass123'); - component.createAdminAccount(); + await component.createAdminAccount(); expect(fakeCreateInitialUser).not.toHaveBeenCalled(); }); - it('should not submit if password is empty', () => { + it('should not submit if password is empty', async () => { const testable = component as SetupComponentTestable; const fakeCreateInitialUser = vi.spyOn(selfhostedService, 'createInitialUser'); testable.email.set('test@example.com'); testable.password.set(''); - component.createAdminAccount(); + await component.createAdminAccount(); expect(fakeCreateInitialUser).not.toHaveBeenCalled(); }); - it('should create admin account and navigate to login on success', () => { + it('should create admin account and navigate to login on success', async () => { const testable = component as SetupComponentTestable; - const fakeCreateInitialUser = vi - .spyOn(selfhostedService, 'createInitialUser') - .mockReturnValue(of({ success: true })); + const fakeCreateInitialUser = vi.spyOn(selfhostedService, 'createInitialUser').mockResolvedValue({ success: true }); const fakeNavigate = vi.spyOn(router, 'navigate').mockResolvedValue(true); testable.email.set('admin@example.com'); testable.password.set('SecurePass123'); - component.createAdminAccount(); + await component.createAdminAccount(); expect(fakeCreateInitialUser).toHaveBeenCalledWith({ email: 'admin@example.com', diff --git a/frontend/src/app/components/setup/setup.component.ts b/frontend/src/app/components/setup/setup.component.ts index 33d374b67..5a57a6690 100644 --- a/frontend/src/app/components/setup/setup.component.ts +++ b/frontend/src/app/components/setup/setup.component.ts @@ -41,29 +41,23 @@ export class SetupComponent { this.password.set(value); } - createAdminAccount(): void { + async createAdminAccount(): Promise { if (!this.email() || !this.password()) { return; } this.submitting.set(true); - this._selfhostedService - .createInitialUser({ + try { + await this._selfhostedService.createInitialUser({ email: this.email(), password: this.password(), - }) - .subscribe({ - next: () => { - this.submitting.set(false); - this._router.navigate(['/login']); - }, - error: () => { - this.submitting.set(false); - }, - complete: () => { - this.submitting.set(false); - }, }); + this._router.navigate(['/login']); + } catch { + // Error handling is done in the service + } finally { + this.submitting.set(false); + } } } diff --git a/frontend/src/app/guards/configuration.guard.ts b/frontend/src/app/guards/configuration.guard.ts index c3aea3e6b..5d41ed184 100644 --- a/frontend/src/app/guards/configuration.guard.ts +++ b/frontend/src/app/guards/configuration.guard.ts @@ -1,6 +1,5 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; -import { map, of } from 'rxjs'; import { SelfhostedService } from '../services/selfhosted.service'; /** @@ -8,33 +7,30 @@ import { SelfhostedService } from '../services/selfhosted.service'; * In self-hosted mode, redirects to /setup if the app is not configured. * In SaaS mode, allows access immediately. */ -export const configurationGuard: CanActivateFn = () => { +export const configurationGuard: CanActivateFn = async () => { const selfhostedService = inject(SelfhostedService); const router = inject(Router); // In SaaS mode, always allow access to login if (!selfhostedService.isSelfHosted()) { - return of(true); + return true; } // If we already know the configuration state, use it const currentState = selfhostedService.isConfigured(); if (currentState !== null) { if (currentState) { - return of(true); + return true; } else { - return of(router.createUrlTree(['/setup'])); + return router.createUrlTree(['/setup']); } } // Check configuration from the server - return selfhostedService.checkConfiguration().pipe( - map((response) => { - if (response.isConfigured) { - return true; - } else { - return router.createUrlTree(['/setup']); - } - }), - ); + const response = await selfhostedService.checkConfiguration(); + if (response.isConfigured) { + return true; + } else { + return router.createUrlTree(['/setup']); + } }; diff --git a/frontend/src/app/guards/setup.guard.ts b/frontend/src/app/guards/setup.guard.ts index 8615e1e5e..717a024c3 100644 --- a/frontend/src/app/guards/setup.guard.ts +++ b/frontend/src/app/guards/setup.guard.ts @@ -1,6 +1,5 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; -import { map, of } from 'rxjs'; import { SelfhostedService } from '../services/selfhosted.service'; /** @@ -9,13 +8,13 @@ import { SelfhostedService } from '../services/selfhosted.service'; * - In self-hosted mode, redirects to /login if already configured * - Allows access to /setup only if self-hosted and not configured */ -export const setupGuard: CanActivateFn = () => { +export const setupGuard: CanActivateFn = async () => { const selfhostedService = inject(SelfhostedService); const router = inject(Router); // In SaaS mode, redirect to login (setup is only for self-hosted) if (!selfhostedService.isSelfHosted()) { - return of(router.createUrlTree(['/login'])); + return router.createUrlTree(['/login']); } // If we already know the configuration state, use it @@ -23,23 +22,20 @@ export const setupGuard: CanActivateFn = () => { if (currentState !== null) { if (currentState) { // Already configured, redirect to login - return of(router.createUrlTree(['/login'])); + return router.createUrlTree(['/login']); } else { // Not configured, allow access to setup - return of(true); + return true; } } // Check configuration from the server - return selfhostedService.checkConfiguration().pipe( - map((response) => { - if (response.isConfigured) { - // Already configured, redirect to login - return router.createUrlTree(['/login']); - } else { - // Not configured, allow access to setup - return true; - } - }), - ); + const response = await selfhostedService.checkConfiguration(); + if (response.isConfigured) { + // Already configured, redirect to login + return router.createUrlTree(['/login']); + } else { + // Not configured, allow access to setup + return true; + } }; diff --git a/frontend/src/app/services/selfhosted.service.ts b/frontend/src/app/services/selfhosted.service.ts index e38d211f7..af4f52a2f 100644 --- a/frontend/src/app/services/selfhosted.service.ts +++ b/frontend/src/app/services/selfhosted.service.ts @@ -1,6 +1,4 @@ -import { HttpClient } from '@angular/common/http'; import { computed, Injectable, inject, signal } from '@angular/core'; -import { catchError, EMPTY, map, Observable, tap } from 'rxjs'; import { environment } from 'src/environments/environment'; import { AlertActionType, AlertType } from '../models/alert'; import { NotificationsService } from './notifications.service'; @@ -22,7 +20,6 @@ export interface CreateInitialUserResponse { providedIn: 'root', }) export class SelfhostedService { - private _http = inject(HttpClient); private _notifications = inject(NotificationsService); private _isConfigured = signal(null); @@ -32,46 +29,60 @@ export class SelfhostedService { public readonly isCheckingConfiguration = this._isCheckingConfiguration.asReadonly(); public readonly isSelfHosted = computed(() => !(environment as any).saas); - checkConfiguration(): Observable { + async checkConfiguration(): Promise { this._isCheckingConfiguration.set(true); - return this._http.get('/selfhosted/is-configured').pipe( - tap((response) => { - this._isConfigured.set(response.isConfigured); - this._isCheckingConfiguration.set(false); - }), - catchError((err) => { - console.error('Failed to check configuration:', err); - this._isCheckingConfiguration.set(false); - // If the endpoint fails, assume configured to avoid blocking login - this._isConfigured.set(true); - return EMPTY; - }), - ); + try { + const response = await fetch('/api/selfhosted/is-configured'); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + const data: IsConfiguredResponse = await response.json(); + this._isConfigured.set(data.isConfigured); + this._isCheckingConfiguration.set(false); + return data; + } catch (err) { + console.error('Failed to check configuration:', err); + this._isCheckingConfiguration.set(false); + // If the endpoint fails, assume configured to avoid blocking login + this._isConfigured.set(true); + return { isConfigured: true }; + } } - createInitialUser(userData: CreateInitialUserRequest): Observable { - return this._http.post('/selfhosted/initial-user', userData).pipe( - map((res) => { - this._notifications.showSuccessSnackbar('Admin account created successfully.'); - this._isConfigured.set(true); - return res; - }), - catchError((err) => { - console.error('Failed to create initial user:', err); - this._notifications.showAlert( - AlertType.Error, - { abstract: err.error?.message || err.message, details: err.error?.originalMessage }, - [ - { - type: AlertActionType.Button, - caption: 'Dismiss', - action: () => this._notifications.dismissAlert(), - }, - ], - ); - return EMPTY; - }), - ); + async createInitialUser(userData: CreateInitialUserRequest): Promise { + try { + const response = await fetch('/api/selfhosted/initial-user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw { error: errorData, message: `HTTP error: ${response.status}` }; + } + + const data: CreateInitialUserResponse = await response.json(); + this._notifications.showSuccessSnackbar('Admin account created successfully.'); + this._isConfigured.set(true); + return data; + } catch (err: any) { + console.error('Failed to create initial user:', err); + this._notifications.showAlert( + AlertType.Error, + { abstract: err.error?.message || err.message, details: err.error?.originalMessage }, + [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: () => this._notifications.dismissAlert(), + }, + ], + ); + throw err; + } } resetConfigurationState(): void {