import { ChangeDetectionStrategy, Component, HostListener, computed, inject, signal, } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Router } from '@angular/router'; import { AuthService } from '../../core/services/auth.service'; import { BootstrapService } from '../../core/services/bootstrap.service'; import { SetupCreateAdminService, CreateAdminFailure } from './setup-create-admin.service'; import { passwordMatchValidator } from './setup-create-admin.validators'; @Component({ selector: 'app-setup-create-admin', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ReactiveFormsModule], templateUrl: './setup-create-admin.component.html', styleUrls: ['./setup-create-admin.component.css'], }) export class SetupCreateAdminComponent { private readonly fb = inject(FormBuilder); private readonly api = inject(SetupCreateAdminService); private readonly auth = inject(AuthService); private readonly router = inject(Router); private readonly bootstrap = inject(BootstrapService); readonly policyDescription = computed(() => this.bootstrap.passwordPolicy().description); readonly form = this.fb.nonNullable.group( { username: this.fb.nonNullable.control('', [ Validators.required, Validators.minLength(3), Validators.maxLength(32), Validators.pattern(/^[a-zA-Z0-9_.-]+$/), ]), password: this.fb.nonNullable.control('', [Validators.required]), passwordConfirm: this.fb.nonNullable.control('', [Validators.required]), }, { validators: passwordMatchValidator }, ); readonly submitting = signal(false); readonly serverError = signal(null); readonly serverErrorCode = signal(null); readonly canRetry = computed( () => !this.submitting() && this.serverError() !== null && this.serverErrorCode() !== 'USERNAME_TAKEN', ); readonly usernameErrors = computed(() => { const c = this.form.controls.username; if (!c.touched || c.valid) return null; if (c.errors?.['required']) return 'Username is required'; if (c.errors?.['minlength']) return 'Username must be at least 3 characters'; if (c.errors?.['maxlength']) return 'Username must be at most 32 characters'; if (c.errors?.['pattern']) return 'Username may only contain letters, digits, dot, dash and underscore'; return 'Invalid username'; }); readonly confirmErrors = computed(() => { const c = this.form.controls.passwordConfirm; const groupMismatch = this.form.errors?.['passwordMismatch']; if ((!c.touched || c.valid) && !groupMismatch) return null; if (c.errors?.['required']) return 'Please confirm your password'; if (groupMismatch) return 'Passwords do not match'; return 'Invalid value'; }); @HostListener('document:keydown.escape', ['$event']) swallowEscape(event: KeyboardEvent): void { event.preventDefault(); event.stopPropagation(); } swallowBackdrop(_event: MouseEvent): void { // intentionally no-op: modal cannot be dismissed by clicking the backdrop } async submit(): Promise { if (this.submitting()) return; this.form.markAllAsTouched(); if (this.form.invalid) return; this.submitting.set(true); this.serverError.set(null); this.serverErrorCode.set(null); const { username, password, passwordConfirm } = this.form.getRawValue(); const result = await this.api.create({ username, password, passwordConfirm }); this.submitting.set(false); if (result.ok) { this.auth.setSession(result.accessToken, result.user); this.bootstrap.markInitialized(); await this.router.navigateByUrl('/admin'); return; } const failure = result as CreateAdminFailure; if (failure.code === 'USERNAME_TAKEN') { this.serverError.set('Username already exists'); this.serverErrorCode.set('USERNAME_TAKEN'); return; } this.serverError.set('Setup failed, please retry'); this.serverErrorCode.set(failure.code); } }