Skip to content

Commit

Permalink
add admin page with guard
Browse files Browse the repository at this point in the history
  • Loading branch information
FelixTJDietrich committed Oct 5, 2024
1 parent 2c593e8 commit f47a69e
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 18 deletions.
17 changes: 17 additions & 0 deletions webapp/src/app/admin/admin.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<h1 class="text-3xl font-bold">Admin</h1>
<div>
<div>User:</div>
{{ user() | json }}
</div>

@if (query.isPending()) {
<div>Loading...</div>
} @else if (query.error()) {
<div>Something went wrong...</div>
}
@if (query.data(); as data) {
<div>
<div>Greeting:</div>
{{ data | json }}
</div>
}
26 changes: 26 additions & 0 deletions webapp/src/app/admin/admin.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { AdminService } from '@app/core/modules/openapi/api/admin.service';
import { SecurityStore } from '@app/core/security/security-store.service';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';

@Component({
selector: 'app-admin',
standalone: true,
imports: [CommonModule],
templateUrl: './admin.component.html'
})
export class AdminComponent {
adminService = inject(AdminService);
securityStore = inject(SecurityStore);

signedIn = this.securityStore.signedIn;
user = this.securityStore.loadedUser;

query = injectQuery(() => ({
enabled: this.signedIn(),
queryKey: ['admin', 'greeting'],
queryFn: async () => lastValueFrom(this.adminService.getGretting())
}));
}
18 changes: 18 additions & 0 deletions webapp/src/app/admin/admin.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type Meta, type StoryObj } from '@storybook/angular';
import { AdminComponent } from './admin.component';

const meta: Meta<AdminComponent> = {
title: 'pages/admin',
component: AdminComponent,
tags: ['autodocs']
};

export default meta;
type Story = StoryObj<AdminComponent>;

export const Default: Story = {
render: (args) => ({
props: args,
template: `<app-admin />`
})
};
13 changes: 10 additions & 3 deletions webapp/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Routes } from '@angular/router';
import { AboutComponent } from 'app/about/about.component';
import { HomeComponent } from 'app/home/home.component';
import { AboutComponent } from '@app/about/about.component';
import { HomeComponent } from '@app/home/home.component';
import { AdminComponent } from '@app/admin/admin.component';
import { AdminGuard } from '@app/core/security/admin.guard';

export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
{ path: 'about', component: AboutComponent },
{
path: 'admin',
component: AdminComponent,
canActivate: [AdminGuard]
}
];
26 changes: 26 additions & 0 deletions webapp/src/app/core/security/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { inject, Injectable, Injector } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { SecurityStore } from '@app/core/security/security-store.service';
import { filter, map, Observable } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
injector = inject(Injector);
securityStore = inject(SecurityStore);
router = inject(Router);

canActivate(): Observable<boolean | UrlTree> {
return toObservable(this.securityStore.loadedUser, { injector: this.injector }).pipe(
filter(Boolean),
map((user) => {
if (user && user.roles.includes('ADMIN')) {
return true;
}
return this.router.createUrlTree(['/']);
})
);
}
}
3 changes: 3 additions & 0 deletions webapp/src/app/core/security/keycloak.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export interface UserProfile {
email: string;
given_name: string;
family_name: string;
realmAccess: { roles: string[] };
token: string;
roles: string[];
}

@Injectable({ providedIn: 'root' })
Expand Down Expand Up @@ -36,6 +38,7 @@ export class KeycloakService {
}
this.profile = (await this.keycloak.loadUserInfo()) as unknown as UserProfile;
this.profile.token = this.keycloak.token || '';
this.profile.roles = this.keycloak.realmAccess?.roles || [];
return true;
}

Expand Down
4 changes: 3 additions & 1 deletion webapp/src/app/core/security/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ export interface User {
name: string;
anonymous: boolean;
bearer: string;
roles: string[];
}

export const ANONYMOUS_USER: User = {
id: '',
email: 'nomail',
name: 'no user',
anonymous: true,
bearer: ''
bearer: '',
roles: []
};

export interface SecurityState {
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/app/core/security/security-store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ export class SecurityStore {

const isLoggedIn = await keycloakService.init();
if (isLoggedIn && keycloakService.profile) {
const { sub, email, given_name, family_name, token } = keycloakService.profile;
const { sub, email, given_name, family_name, token, roles } = keycloakService.profile;
const user = {
id: sub,
email,
name: `${given_name} ${family_name}`,
anonymous: false,
bearer: token
bearer: token,
roles
};
this.user.set(user);
this.loaded.set(true);
Expand Down
5 changes: 1 addition & 4 deletions webapp/src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4 xl:gap-8">
<div class="space-y-2 col-span-1">
<div class="flex flex-col gap-2 mb-4">
<h1 class="text-3xl font-bold">
Artemis<br class="hidden md:visible" />
Leaderboard
</h1>
<h1 class="text-3xl font-bold">Artemis Leaderboard</h1>
@if (signedIn() && user(); as userValue) {
<h2 class="text-xl text-muted-foreground">Hi {{ userValue.name }} 👋</h2>
}
Expand Down
14 changes: 6 additions & 8 deletions webapp/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import dayjs from 'dayjs';
import { combineLatest, timer, lastValueFrom, map } from 'rxjs';
import { Component, computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { LeaderboardService } from 'app/core/modules/openapi/api/leaderboard.service';
import { LeaderboardComponent } from 'app/home/leaderboard/leaderboard.component';
import { combineLatest, timer, lastValueFrom, map } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { LucideAngularModule, CircleX } from 'lucide-angular';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { LeaderboardService } from '@app/core/modules/openapi/api/leaderboard.service';
import { LeaderboardComponent } from '@app/home/leaderboard/leaderboard.component';
import { LeaderboardFilterComponent } from './leaderboard/filter/filter.component';
import dayjs from 'dayjs';
import { AdminService } from '@app/core/modules/openapi/api/admin.service';
import { SecurityStore } from '@app/core/security/security-store.service';
import { HlmAlertModule } from '@spartan-ng/ui-alert-helm';
import { LucideAngularModule, CircleX } from 'lucide-angular';

@Component({
selector: 'app-home',
Expand All @@ -22,7 +21,6 @@ export class HomeComponent {
protected CircleX = CircleX;

securityStore = inject(SecurityStore);
adminService = inject(AdminService);
leaderboardService = inject(LeaderboardService);

signedIn = this.securityStore.signedIn;
Expand Down

0 comments on commit f47a69e

Please sign in to comment.