-
-
Notifications
You must be signed in to change notification settings - Fork 18
Feat/selfhosted setup page #1556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,40 +1,36 @@ | ||
| import { inject } from '@angular/core'; | ||
| import { CanActivateFn, Router } from '@angular/router'; | ||
| import { map, of } from 'rxjs'; | ||
| import { SelfhostedService } from '../services/selfhosted.service'; | ||
|
|
||
| /** | ||
| * Guard that protects the /login route. | ||
| * 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(); | ||
|
Comment on lines
19
to
+30
|
||
| if (response.isConfigured) { | ||
| return true; | ||
| } else { | ||
| return router.createUrlTree(['/setup']); | ||
| } | ||
| }; | ||
|
Comment on lines
1
to
36
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,37 +8,34 @@ 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 | ||
| const currentState = selfhostedService.isConfigured(); | ||
| 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(); | ||
|
Comment on lines
20
to
+33
|
||
| if (response.isConfigured) { | ||
| // Already configured, redirect to login | ||
| return router.createUrlTree(['/login']); | ||
| } else { | ||
| // Not configured, allow access to setup | ||
| return true; | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<boolean | null>(null); | ||
|
|
@@ -32,46 +29,60 @@ export class SelfhostedService { | |
| public readonly isCheckingConfiguration = this._isCheckingConfiguration.asReadonly(); | ||
| public readonly isSelfHosted = computed(() => !(environment as any).saas); | ||
|
|
||
| checkConfiguration(): Observable<IsConfiguredResponse> { | ||
| async checkConfiguration(): Promise<IsConfiguredResponse> { | ||
| this._isCheckingConfiguration.set(true); | ||
| return this._http.get<IsConfiguredResponse>('/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 }; | ||
| } | ||
|
Comment on lines
+43
to
+49
|
||
| } | ||
|
|
||
| createInitialUser(userData: CreateInitialUserRequest): Observable<CreateInitialUserResponse> { | ||
| return this._http.post<CreateInitialUserResponse>('/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<CreateInitialUserResponse> { | ||
| try { | ||
| const response = await fetch('/api/selfhosted/initial-user', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(userData), | ||
| }); | ||
|
Comment on lines
+35
to
+60
|
||
|
|
||
| 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The password field validation is delegated entirely to the UserPasswordComponent, but there's no explicit validation in the setup component itself. While the submit button checks for empty values (!this.email() || !this.password()), it doesn't verify password strength requirements. Consider whether password strength validation should be enforced at the component level in addition to any validation performed by UserPasswordComponent, or add a comment clarifying that password validation is handled by the child component.