AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { BootstrapService } from './core/services/bootstrap.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
private bootstrap = inject(BootstrapService);
|
||||
async ngOnInit() {
|
||||
await this.bootstrap.load();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { authGuard } from './core/guards/auth.guard';
|
||||
|
||||
export const APP_ROUTES: Routes = [
|
||||
{
|
||||
path: 'bootstrap',
|
||||
loadComponent: () => import('./features/bootstrap/create-admin.component').then((m) => m.CreateAdminComponent),
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
loadComponent: () => import('./features/auth/login.component').then((m) => m.LoginComponent),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./features/home/home.component').then((m) => m.HomeComponent),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { BootstrapService } from '../services/bootstrap.service';
|
||||
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const auth = inject(AuthService);
|
||||
const bootstrap = inject(BootstrapService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (!bootstrap.initialized()) {
|
||||
return router.createUrlTree(['/bootstrap']);
|
||||
}
|
||||
if (!auth.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login']);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const auth = inject(AuthService);
|
||||
const token = auth.getAccessToken();
|
||||
if (token) {
|
||||
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
|
||||
}
|
||||
return next(req);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
|
||||
function readCookie(name: string): string | null {
|
||||
const header = document.cookie;
|
||||
if (!header) return null;
|
||||
for (const part of header.split(';')) {
|
||||
const [k, ...v] = part.trim().split('=');
|
||||
if (k === name) return v.join('=');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const UNSAFE = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
export const csrfInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
if (UNSAFE.has(req.method)) {
|
||||
const token = readCookie('csrf');
|
||||
if (token) {
|
||||
req = req.clone({ setHeaders: { 'X-CSRF-Token': token } });
|
||||
}
|
||||
}
|
||||
return next(req);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
username: string;
|
||||
role: 'admin' | 'player';
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private accessToken = signal<string | null>(null);
|
||||
private user = signal<CurrentUser | null>(null);
|
||||
|
||||
readonly currentUser = computed(() => this.user());
|
||||
readonly isAuthenticated = computed(() => !!this.user());
|
||||
|
||||
setSession(token: string, user: CurrentUser): void {
|
||||
this.accessToken.set(token);
|
||||
this.user.set(user);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.accessToken.set(null);
|
||||
this.user.set(null);
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return this.accessToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface BootstrapPayload {
|
||||
initialized: boolean;
|
||||
pageTitle: string;
|
||||
logo: string;
|
||||
welcomeMarkdown: string;
|
||||
theme: any;
|
||||
defaultChallengeIp: string;
|
||||
registrationsEnabled: boolean;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BootstrapService {
|
||||
readonly payload = signal<BootstrapPayload | null>(null);
|
||||
readonly initialized = signal<boolean>(false);
|
||||
|
||||
async load(): Promise<BootstrapPayload | null> {
|
||||
try {
|
||||
const res = await fetch('/api/v1/bootstrap', { credentials: 'include' });
|
||||
const data = (await res.json()) as BootstrapPayload;
|
||||
this.payload.set(data);
|
||||
this.initialized.set(data.initialized);
|
||||
this.applyTheme(data.theme);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Bootstrap load failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private applyTheme(theme: any): void {
|
||||
if (!theme?.tokens) return;
|
||||
const root = document.documentElement.style;
|
||||
const t = theme.tokens;
|
||||
root.setProperty('--color-primary', t.primary);
|
||||
root.setProperty('--color-secondary', t.secondary);
|
||||
root.setProperty('--color-accent', t.accent);
|
||||
root.setProperty('--color-surface', t.surface);
|
||||
root.setProperty('--color-text', t.text);
|
||||
root.setProperty('--color-success', t.success);
|
||||
root.setProperty('--color-warning', t.warning);
|
||||
root.setProperty('--color-danger', t.danger);
|
||||
root.setProperty('--font-family', t.fontFamily);
|
||||
root.setProperty('--radius-sm', t.radii.sm);
|
||||
root.setProperty('--radius-md', t.radii.md);
|
||||
root.setProperty('--radius-lg', t.radii.lg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [FormsModule],
|
||||
template: `
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Sign in</h1>
|
||||
<label>Username<input [(ngModel)]="username" name="u" /></label>
|
||||
<label>Password<input [(ngModel)]="password" name="p" type="password" /></label>
|
||||
<p style="color:var(--color-danger)">{{ error }}</p>
|
||||
<button (click)="submit()" [disabled]="loading">{{ loading ? 'Signing in...' : 'Sign in' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class LoginComponent {
|
||||
private http = inject(HttpClient);
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
|
||||
username = '';
|
||||
password = '';
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
async submit(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await fetch('/api/v1/auth/csrf', { credentials: 'include' });
|
||||
const res: any = await this.http
|
||||
.post('/api/v1/auth/login', { username: this.username, password: this.password }, { withCredentials: true })
|
||||
.toPromise();
|
||||
this.auth.setSession(res.accessToken, res.user);
|
||||
await this.router.navigateByUrl('/');
|
||||
} catch (e: any) {
|
||||
this.error = e?.error?.message ?? 'Failed';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-admin',
|
||||
standalone: true,
|
||||
imports: [FormsModule],
|
||||
template: `
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Create the first admin</h1>
|
||||
<p>This HIPCTF instance has not been initialized.</p>
|
||||
<label>Username<input [(ngModel)]="username" name="username" /></label>
|
||||
<label>Password<input [(ngModel)]="password" name="password" type="password" /></label>
|
||||
<p style="color:var(--color-danger)">{{ error }}</p>
|
||||
<button (click)="submit()" [disabled]="loading">{{ loading ? 'Creating...' : 'Create admin' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class CreateAdminComponent {
|
||||
private http = inject(HttpClient);
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
|
||||
username = '';
|
||||
password = '';
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
async submit(): Promise<void> {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await fetch('/api/v1/auth/csrf', { credentials: 'include' });
|
||||
const res: any = await this.http
|
||||
.post('/api/v1/auth/register-first-admin', { username: this.username, password: this.password }, { withCredentials: true })
|
||||
.toPromise();
|
||||
this.auth.setSession(res.accessToken, res.user);
|
||||
await this.router.navigateByUrl('/');
|
||||
} catch (e: any) {
|
||||
this.error = e?.error?.message ?? 'Failed';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BootstrapService } from '../../core/services/bootstrap.service';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div class="container">
|
||||
<h1>{{ bootstrap.payload()?.pageTitle ?? 'HIPCTF' }}</h1>
|
||||
<div *ngIf="!auth.isAuthenticated()">
|
||||
<p>{{ bootstrap.payload()?.welcomeMarkdown }}</p>
|
||||
<a href="/login">Sign in</a>
|
||||
</div>
|
||||
<div *ngIf="auth.isAuthenticated()">
|
||||
<p>Signed in as <b>{{ auth.currentUser()?.username }}</b> ({{ auth.currentUser()?.role }})</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class HomeComponent {
|
||||
bootstrap = inject(BootstrapService);
|
||||
auth = inject(AuthService);
|
||||
}
|
||||
Reference in New Issue
Block a user