AI Implementation feature(823): Landing Page and Login/Register Modal (#11)
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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<ModalMode>('closed');
|
||||
readonly submitting = signal(false);
|
||||
readonly errorText = signal<string | null>(null);
|
||||
readonly errorCode = signal<string | null>(null);
|
||||
readonly retryAfterSeconds = signal<number | null>(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();
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user