AI Implementation feature(841): First-Start Create Admin Modal 1.00 (#3)
This commit was merged in pull request #3.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { authGuard } from './core/guards/auth.guard';
|
||||
import { adminGuard } from './core/guards/admin.guard';
|
||||
|
||||
export const APP_ROUTES: Routes = [
|
||||
{
|
||||
@@ -17,6 +18,14 @@ export const APP_ROUTES: Routes = [
|
||||
path: '',
|
||||
loadComponent: () => import('./features/home/home.component').then((m) => m.HomeComponent),
|
||||
canActivate: [authGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'admin',
|
||||
canActivate: [adminGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/admin/admin-users.component').then((m) => m.AdminUsersComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
];
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export type AdminGuardDecision =
|
||||
| { kind: 'allow' }
|
||||
| { kind: 'redirect'; path: '/bootstrap' | '/login' | '/' };
|
||||
|
||||
export function decideAdminGuard(input: {
|
||||
initialized: boolean;
|
||||
isAuthenticated: boolean;
|
||||
role: 'admin' | 'player' | undefined;
|
||||
}): AdminGuardDecision {
|
||||
if (!input.initialized) return { kind: 'redirect', path: '/bootstrap' };
|
||||
if (!input.isAuthenticated) return { kind: 'redirect', path: '/login' };
|
||||
if (input.role !== 'admin') return { kind: 'redirect', path: '/' };
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { BootstrapService } from '../services/bootstrap.service';
|
||||
import { decideAdminGuard } from './admin.guard.decision';
|
||||
|
||||
export { decideAdminGuard } from './admin.guard.decision';
|
||||
export type { AdminGuardDecision } from './admin.guard.decision';
|
||||
|
||||
export const adminGuard: CanActivateFn = () => {
|
||||
const auth = inject(AuthService);
|
||||
const bootstrap = inject(BootstrapService);
|
||||
const router = inject(Router);
|
||||
|
||||
const decision = decideAdminGuard({
|
||||
initialized: bootstrap.initialized(),
|
||||
isAuthenticated: auth.isAuthenticated(),
|
||||
role: auth.currentUser()?.role,
|
||||
});
|
||||
|
||||
if (decision.kind === 'allow') return true;
|
||||
return router.createUrlTree([decision.path]);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface AdminUser {
|
||||
id: string;
|
||||
username: string;
|
||||
role: 'admin' | 'player';
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
async listUsers(): Promise<AdminUser[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<AdminUser[]>('/api/v1/admin/users', { withCredentials: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnInit,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AdminService, AdminUser } from '../../core/services/admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-users',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<section class="admin-area">
|
||||
<h2>Admin area</h2>
|
||||
<p data-testid="admin-loading" *ngIf="loading()">Loading users...</p>
|
||||
<p data-testid="admin-error" *ngIf="error()" style="color:var(--color-danger)">{{ error() }}</p>
|
||||
<ul data-testid="admin-user-list" *ngIf="!loading() && !error()">
|
||||
<li *ngFor="let u of users()">
|
||||
<b>{{ u.username }}</b> ({{ u.role }})
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class AdminUsersComponent implements OnInit {
|
||||
private readonly admin = inject(AdminService);
|
||||
|
||||
readonly users = signal<AdminUser[]>([]);
|
||||
readonly loading = signal(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
try {
|
||||
const list = await this.admin.listUsers();
|
||||
this.users.set(list);
|
||||
} catch (e: any) {
|
||||
this.error.set(e?.error?.message ?? e?.message ?? 'Failed to load users');
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,47 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { BootstrapService } from '../../core/services/bootstrap.service';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { shouldShowAdminNav } from './home.shell';
|
||||
|
||||
export { shouldShowAdminNav } from './home.shell';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, RouterLink, RouterOutlet],
|
||||
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>
|
||||
<header class="shell-header">
|
||||
<h1 class="shell-title">{{ bootstrap.payload()?.pageTitle ?? 'HIPCTF' }}</h1>
|
||||
<div class="shell-user" *ngIf="auth.isAuthenticated()">
|
||||
<span>Signed in as <b>{{ auth.currentUser()?.username }}</b> ({{ auth.currentUser()?.role }})</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="shell-nav" *ngIf="showAdminNav()">
|
||||
<a routerLink="/admin" data-testid="nav-admin">Admin</a>
|
||||
</nav>
|
||||
|
||||
<section class="shell-body">
|
||||
<div *ngIf="!auth.isAuthenticated()">
|
||||
<p>{{ bootstrap.payload()?.welcomeMarkdown }}</p>
|
||||
<a href="/login">Sign in</a>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class HomeComponent {
|
||||
bootstrap = inject(BootstrapService);
|
||||
auth = inject(AuthService);
|
||||
}
|
||||
|
||||
readonly showAdminNav = computed(() =>
|
||||
shouldShowAdminNav({
|
||||
isAuthenticated: this.auth.isAuthenticated(),
|
||||
role: this.auth.currentUser()?.role,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export function shouldShowAdminNav(input: {
|
||||
isAuthenticated: boolean;
|
||||
role: 'admin' | 'player' | undefined;
|
||||
}): boolean {
|
||||
return input.isAuthenticated && input.role === 'admin';
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export class SetupCreateAdminComponent {
|
||||
if (result.ok) {
|
||||
this.auth.setSession(result.accessToken, result.user);
|
||||
this.bootstrap.markInitialized();
|
||||
await this.router.navigateByUrl('/challenges');
|
||||
await this.router.navigateByUrl('/admin');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user