import { ChangeDetectionStrategy, Component, OnInit, HostListener, computed, inject, signal, } from '@angular/core'; import { CommonModule } from '@angular/common'; 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 { MarkdownService } from '../../core/services/markdown.service'; import { LandingService } from './landing.service'; import { buildLoginFailureMessage } from './login-modal.service'; import { passwordMatchValidator } from '../setup/setup-create-admin.validators'; type ModalMode = 'closed' | 'login' | 'register'; @Component({ selector: 'app-landing', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, ReactiveFormsModule], templateUrl: './landing.component.html', styleUrls: ['./landing.component.css'], }) export class LandingComponent implements OnInit { private readonly fb = inject(FormBuilder); private readonly auth = inject(AuthService); private readonly router = inject(Router); private readonly bootstrap = inject(BootstrapService); private readonly markdown = inject(MarkdownService); readonly landing = inject(LandingService); readonly mode = signal('closed'); readonly submitting = signal(false); readonly errorText = signal(null); readonly errorCode = signal(null); readonly retryAfterSeconds = signal(null); readonly pageTitle = computed(() => this.bootstrap.payload()?.pageTitle ?? 'HIPCTF'); readonly logo = computed(() => this.bootstrap.payload()?.logo ?? ''); readonly welcomeHtml = computed(() => this.markdown.render(this.bootstrap.payload()?.welcomeMarkdown ?? '')); readonly registrationsEnabled = computed( () => this.bootstrap.payload()?.registrationsEnabled ?? false, ); readonly loginForm = this.fb.nonNullable.group({ username: this.fb.nonNullable.control('', [Validators.required]), password: this.fb.nonNullable.control('', [Validators.required]), }); readonly registerForm = 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 }, ); ngOnInit(): void { void this.landing.refresh(); void this.bootstrap.refresh(); } openLogin(): void { this.mode.set('login'); this.clearError(); } openRegister(): void { this.mode.set('register'); this.clearError(); } closeModal(): void { if (this.submitting()) return; this.mode.set('closed'); this.clearError(); } switchToLogin(): void { this.mode.set('login'); this.clearError(); } switchToRegister(): void { this.mode.set('register'); this.clearError(); } @HostListener('document:keydown.escape') onEscape(): void { this.closeModal(); } loginError(): string | null { const c = this.loginForm.controls; if (!c.username.touched && !c.password.touched) return null; if (c.username.errors?.['required']) return 'Username is required'; if (c.password.errors?.['required']) return 'Password is required'; return null; } registerError(): string | null { const c = this.registerForm.controls; if (!c.username.touched && !c.password.touched && !c.passwordConfirm.touched && !this.registerForm.errors) { return null; } if (c.username.errors?.['required']) return 'Username is required'; if (c.username.errors?.['minlength']) return 'Username must be at least 3 characters'; if (c.username.errors?.['maxlength']) return 'Username must be at most 32 characters'; if (c.username.errors?.['pattern']) { return 'Username may only contain letters, digits, dot, dash and underscore'; } if (c.password.errors?.['required']) return 'Password is required'; if (c.passwordConfirm.errors?.['required']) return 'Please confirm your password'; if (this.registerForm.errors?.['passwordMismatch']) return 'Passwords do not match'; return null; } async submitLogin(): Promise { if (this.submitting()) return; this.loginForm.markAllAsTouched(); if (this.loginForm.invalid) return; this.submitting.set(true); this.clearError(); const { username, password } = this.loginForm.getRawValue(); const result: { ok: true; accessToken: string; expiresIn: number; user: any } | { ok: false; status: number; code: string; message: string; } = await this.auth.login({ username, password }); this.submitting.set(false); if (result.ok === true) { this.auth.setSession(result.accessToken, result.user); this.mode.set('closed'); await this.router.navigateByUrl('/'); return; } if (result.ok === false) { this.showFailure(result.code, result.message); } } async submitRegister(): Promise { if (this.submitting()) return; this.registerForm.markAllAsTouched(); if (this.registerForm.invalid) return; this.submitting.set(true); this.clearError(); const { username, password, passwordConfirm } = this.registerForm.getRawValue(); const result: { ok: true; accessToken: string; expiresIn: number; user: any } | { ok: false; status: number; code: string; message: string; } = await this.auth.register({ username, password, passwordConfirm }); this.submitting.set(false); if (result.ok === true) { this.auth.setSession(result.accessToken, result.user); this.mode.set('closed'); await this.router.navigateByUrl('/'); return; } if (result.ok === false) { this.showFailure(result.code, result.message); } } private showFailure(code: string, message: string): void { this.errorCode.set(code); const built = buildLoginFailureMessage({ code, message }); this.errorText.set(built.text); this.retryAfterSeconds.set(built.retryAfterSeconds ?? null); } private clearError(): void { this.errorCode.set(null); this.errorText.set(null); this.retryAfterSeconds.set(null); } }