145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
HostListener,
|
|
computed,
|
|
effect,
|
|
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';
|
|
import {
|
|
usernameError as computeUsernameError,
|
|
passwordError as computePasswordError,
|
|
confirmError as computeConfirmError,
|
|
} from './setup-create-admin.field-errors';
|
|
|
|
@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);
|
|
|
|
private readonly alreadyInitialized = computed(() => this.bootstrap.initialized());
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
if (this.alreadyInitialized()) {
|
|
void this.router.navigateByUrl(this.auth.isAuthenticated() ? '/' : '/login');
|
|
}
|
|
});
|
|
}
|
|
|
|
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<string | null>(null);
|
|
readonly serverErrorCode = signal<string | null>(null);
|
|
|
|
readonly canRetry = computed(
|
|
() => !this.submitting() && this.serverError() !== null && this.serverErrorCode() !== 'USERNAME_TAKEN',
|
|
);
|
|
|
|
usernameError(): string | null {
|
|
const c = this.form.controls.username;
|
|
return computeUsernameError({
|
|
value: c.value,
|
|
touched: c.touched,
|
|
errors: (c.errors as { required?: true; minlength?: true; maxlength?: true; pattern?: true } | null) ?? null,
|
|
});
|
|
}
|
|
|
|
passwordError(): string | null {
|
|
const c = this.form.controls.password;
|
|
return computePasswordError({
|
|
value: c.value,
|
|
touched: c.touched,
|
|
errors: (c.errors as { required?: true } | null) ?? null,
|
|
});
|
|
}
|
|
|
|
confirmError(): string | null {
|
|
const c = this.form.controls.passwordConfirm;
|
|
return computeConfirmError({
|
|
value: c.value,
|
|
touched: c.touched,
|
|
errors: (c.errors as { required?: true } | null) ?? null,
|
|
groupMismatch: !!this.form.errors?.['passwordMismatch'],
|
|
});
|
|
}
|
|
|
|
@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<void> {
|
|
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);
|
|
}
|
|
}
|