Skip to content

feat: add self-hosted setup page for initial admin configuration#1555

Merged
gugu merged 2 commits intomainfrom
feat/selfhosted-setup-page
Feb 3, 2026
Merged

feat: add self-hosted setup page for initial admin configuration#1555
gugu merged 2 commits intomainfrom
feat/selfhosted-setup-page

Conversation

@gugu
Copy link
Contributor

@gugu gugu commented Feb 3, 2026

  • Add SelfhostedService with signals for configuration state management
  • Add ConfigurationGuard to redirect /login to /setup when not configured
  • Add SetupGuard to protect /setup route (only accessible when not configured)
  • Add SetupComponent with email/password form matching login page style
  • Update app-routing.module.ts with /setup route and guards

- Add SelfhostedService with signals for configuration state management
- Add ConfigurationGuard to redirect /login to /setup when not configured
- Add SetupGuard to protect /setup route (only accessible when not configured)
- Add SetupComponent with email/password form matching login page style
- Update app-routing.module.ts with /setup route and guards

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 12:22
@gugu gugu enabled auto-merge (squash) February 3, 2026 12:23
@gugu gugu merged commit 5422f64 into main Feb 3, 2026
15 checks passed
@gugu gugu deleted the feat/selfhosted-setup-page branch February 3, 2026 12:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a self-hosted setup flow that allows users to create an initial admin account on first launch. The implementation includes backend integration via a new SelfhostedService, route guards to control access to setup and login pages based on configuration state, and a setup UI component matching the login page's design.

Changes:

  • Added SelfhostedService with configuration checking and initial user creation endpoints
  • Added guards (setupGuard and configurationGuard) to manage routing between /setup and /login based on configuration state
  • Added SetupComponent with form UI for admin account creation

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
frontend/src/app/services/selfhosted.service.ts New service managing self-hosted configuration state with signals and HTTP endpoints for checking configuration and creating initial admin user
frontend/src/app/guards/setup.guard.ts New guard protecting /setup route, allowing access only for unconfigured self-hosted instances
frontend/src/app/guards/configuration.guard.ts New guard protecting /login route, redirecting to /setup if self-hosted instance is not configured
frontend/src/app/components/setup/setup.component.ts New component handling admin account creation form logic with email/password inputs
frontend/src/app/components/setup/setup.component.spec.ts Unit tests for SetupComponent covering basic functionality and validation
frontend/src/app/components/setup/setup.component.html Template for setup page with email and password fields matching login page design
frontend/src/app/components/setup/setup.component.css Styles for setup page with responsive design
frontend/src/app/app-routing.module.ts Updated routing configuration to include /setup route with guards

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +57 to +59
next: () => {
this.submitting.set(false);
this._router.navigate(['/login']);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router.navigate() call lacks error handling. If navigation fails (e.g., if the configurationGuard prevents access), the user will remain on the setup page without feedback. Consider handling the navigation promise and providing appropriate feedback if navigation fails.

Suggested change
next: () => {
this.submitting.set(false);
this._router.navigate(['/login']);
next: async () => {
this.submitting.set(false);
try {
const navigated = await this._router.navigate(['/login']);
if (!navigated) {
console.error('Navigation to /login was unsuccessful.');
}
} catch (error) {
console.error('Navigation to /login failed.', error);
}

Copilot uses AI. Check for mistakes.
}

createInitialUser(userData: CreateInitialUserRequest): Observable<CreateInitialUserResponse> {
return this._http.post<CreateInitialUserResponse>('/selfhosted/initial-user', userData).pipe(
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP endpoint '/selfhosted/initial-user' lacks the API prefix. Based on the codebase pattern, HTTP requests should be prefixed with '/api' or use the environment's apiRoot. For consistency with other endpoints like '/user/login' in auth.service.ts, consider using a full path or ensure the interceptor handles this path correctly.

Copilot uses AI. Check for mistakes.
}

// Check configuration from the server
return selfhostedService.checkConfiguration().pipe(
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition if both setupGuard and configurationGuard check configuration simultaneously. If a user navigates directly to /setup while another process checks /login, both guards might call checkConfiguration() concurrently. While RxJS handles this, the guards should potentially share the same observable to avoid duplicate HTTP requests. Consider adding a shareReplay operator to the checkConfiguration() observable or implementing a caching mechanism.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +97
it('should create admin account and navigate to login on success', () => {
const testable = component as SetupComponentTestable;
const fakeCreateInitialUser = vi
.spyOn(selfhostedService, 'createInitialUser')
.mockReturnValue(of({ success: true }));
const fakeNavigate = vi.spyOn(router, 'navigate').mockResolvedValue(true);

testable.email.set('admin@example.com');
testable.password.set('SecurePass123');

component.createAdminAccount();

expect(fakeCreateInitialUser).toHaveBeenCalledWith({
email: 'admin@example.com',
password: 'SecurePass123',
});
expect(testable.submitting()).toBe(false);
expect(fakeNavigate).toHaveBeenCalledWith(['/login']);
});
});
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite lacks coverage for error scenarios. Missing test cases include: (1) testing behavior when createInitialUser fails and (2) testing the error callback that sets submitting to false. Given that other component tests in the codebase (like login.component.spec.ts) include error scenario testing, this component should follow the same pattern for consistency and completeness.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +45
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 /setup route.
* - In SaaS mode, redirects to /login (setup is only for self-hosted)
* - 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 = () => {
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']));
}

// 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']));
} else {
// Not configured, allow access to setup
return of(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;
}
}),
);
};
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setupGuard lacks test coverage. Given that the codebase has test files for other guards (e.g., auth.guard.spec.ts), and this guard implements critical routing logic that determines access to the setup page, it should have corresponding tests. Test cases should cover: (1) SaaS mode redirects to login, (2) self-hosted + configured redirects to login, (3) self-hosted + not configured allows access, and (4) behavior when configuration state is unknown.

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When checkConfiguration() fails and sets isConfigured to true (line 46), subsequent navigation attempts will bypass the setup page even if the system is actually not configured. If a user later tries to access /setup after the configuration check failed, they'll be incorrectly redirected to /login. Consider implementing a retry mechanism or explicitly handling the error state separately from the configured state, using a tri-state value like null (unknown), true (configured), or false (not configured), with 'error' as a fourth state.

Copilot uses AI. Check for mistakes.

checkConfiguration(): Observable<IsConfiguredResponse> {
this._isCheckingConfiguration.set(true);
return this._http.get<IsConfiguredResponse>('/selfhosted/is-configured').pipe(
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP endpoint '/selfhosted/is-configured' lacks the API prefix. Based on the codebase pattern, HTTP requests should be prefixed with '/api' or use the environment's apiRoot. For example, auth.service.ts uses '/user/login' which gets prefixed via the token interceptor. However, for consistency and clarity, consider using a full path like environment.apiRoot + '/selfhosted/is-configured' or ensure the interceptor handles this path correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +46
// If the endpoint fails, assume configured to avoid blocking login
this._isConfigured.set(true);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the configuration check fails, the service assumes the system is configured and returns EMPTY. This silent fallback could hide critical backend issues. If the backend is down or misconfigured, users might be unable to access either the setup page or login page. Consider throwing an error or showing a notification to alert users to the problem, rather than assuming a default state.

Suggested change
// If the endpoint fails, assume configured to avoid blocking login
this._isConfigured.set(true);
// If the endpoint fails, assume configured to avoid blocking login,
// but also show a visible error so backend issues are not silent.
this._isConfigured.set(true);
this._notifications.showAlert(
AlertType.Error,
{
abstract: 'Unable to verify server configuration.',
details: err?.error?.message || err?.message,
},
[
{
type: AlertActionType.Button,
caption: 'Dismiss',
action: () => this._notifications.dismissAlert(),
},
],
);

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +80
export class SelfhostedService {
private _http = inject(HttpClient);
private _notifications = inject(NotificationsService);

private _isConfigured = signal<boolean | null>(null);
private _isCheckingConfiguration = signal<boolean>(false);

public readonly isConfigured = this._isConfigured.asReadonly();
public readonly isCheckingConfiguration = this._isCheckingConfiguration.asReadonly();
public readonly isSelfHosted = computed(() => !(environment as any).saas);

checkConfiguration(): Observable<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;
}),
);
}

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;
}),
);
}

resetConfigurationState(): void {
this._isConfigured.set(null);
}
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SelfhostedService lacks test coverage. Other services in the codebase (e.g., auth.service.spec.ts, user.service.spec.ts) have comprehensive test suites. Given that this service manages critical configuration state and API calls, it should have tests covering: (1) checkConfiguration success and failure scenarios, (2) createInitialUser success and failure scenarios, (3) signal state management, and (4) resetConfigurationState functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +47
createAdminAccount(): void {
if (!this.email() || !this.password()) {
return;
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form validation in the template allows submission when pristine is false, but the createAdminAccount method only checks if email and password are truthy. This creates a gap where a user could potentially submit an invalid email (if the email validator hasn't caught it yet) or a weak password. While the template validation prevents submission via the disabled button attribute, the method itself doesn't validate the data before sending it to the API. Consider adding explicit validation in the method or ensuring the method can only be called when the form is valid.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant