diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore
index df7fb2a..0be734a 100644
--- a/packages/frontend/.gitignore
+++ b/packages/frontend/.gitignore
@@ -43,4 +43,4 @@ __screenshots__/
Thumbs.db
###
-src/generated-api/
+src/app/generated-api/
diff --git a/packages/frontend/angular.json b/packages/frontend/angular.json
index 92bcada..227f981 100644
--- a/packages/frontend/angular.json
+++ b/packages/frontend/angular.json
@@ -21,9 +21,7 @@
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
- "polyfills": [
- "zone.js"
- ],
+ "polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
@@ -32,9 +30,7 @@
"input": "public"
}
],
- "styles": [
- "src/styles.scss"
- ]
+ "styles": ["src/styles.scss"]
},
"configurations": {
"production": {
@@ -78,10 +74,7 @@
"test": {
"builder": "@angular/build:karma",
"options": {
- "polyfills": [
- "zone.js",
- "zone.js/testing"
- ],
+ "polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
@@ -90,9 +83,7 @@
"input": "public"
}
],
- "styles": [
- "src/styles.scss"
- ]
+ "styles": ["src/styles.scss"]
}
}
}
diff --git a/packages/frontend/public/favicon.ico b/packages/frontend/public/favicon.ico
index 57614f9..22b8e32 100644
Binary files a/packages/frontend/public/favicon.ico and b/packages/frontend/public/favicon.ico differ
diff --git a/packages/frontend/public/silent-check-sso.html b/packages/frontend/public/silent-check-sso.html
new file mode 100644
index 0000000..b3bd540
--- /dev/null
+++ b/packages/frontend/public/silent-check-sso.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/packages/frontend/src/app/app.config.ts b/packages/frontend/src/app/app.config.ts
index d953f4c..2cd232f 100644
--- a/packages/frontend/src/app/app.config.ts
+++ b/packages/frontend/src/app/app.config.ts
@@ -1,12 +1,52 @@
-import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
-import { provideRouter } from '@angular/router';
+import {
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+ provideZoneChangeDetection,
+} from '@angular/core';
+import {
+ PreloadAllModules,
+ provideRouter,
+ withComponentInputBinding,
+ withPreloading,
+} from '@angular/router';
+
+import {
+ AutoRefreshTokenService,
+ createInterceptorCondition,
+ INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
+ IncludeBearerTokenCondition,
+ provideKeycloak,
+ UserActivityService,
+ withAutoRefreshToken,
+} from 'keycloak-angular';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
+ provideRouter(routes, withPreloading(PreloadAllModules), withComponentInputBinding()),
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
- provideRouter(routes)
- ]
+ provideKeycloak({
+ config: {
+ url: 'https://keycloak.toni714.de:8443/',
+ realm: 'taekwondo',
+ clientId: 'angular-frontend',
+ },
+ initOptions: {
+ onLoad: 'check-sso',
+ silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
+ },
+ features: [withAutoRefreshToken({ sessionTimeout: 600000 })],
+ providers: [AutoRefreshTokenService, UserActivityService],
+ }),
+ {
+ provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
+ useValue: [
+ createInterceptorCondition({
+ urlPattern: /^https:\/\/tkd-api\.toni714\.de.*$/,
+ }),
+ ],
+ },
+ ],
};
diff --git a/packages/frontend/src/app/app.html b/packages/frontend/src/app/app.html
index 7528372..942524b 100644
--- a/packages/frontend/src/app/app.html
+++ b/packages/frontend/src/app/app.html
@@ -1,342 +1,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Hello, {{ title() }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+@if(this.loggedIn()){
+
+}
+
diff --git a/packages/frontend/src/app/app.routes.ts b/packages/frontend/src/app/app.routes.ts
index dc39edb..2b8765d 100644
--- a/packages/frontend/src/app/app.routes.ts
+++ b/packages/frontend/src/app/app.routes.ts
@@ -1,3 +1,50 @@
-import { Routes } from '@angular/router';
+import { inject } from '@angular/core';
+import {
+ ActivatedRouteSnapshot,
+ CanActivateFn,
+ Router,
+ RouterStateSnapshot,
+ Routes,
+ UrlTree,
+} from '@angular/router';
+import { AuthGuardData, createAuthGuard } from 'keycloak-angular';
+import { Login } from './components/login/login';
+import { MissingPage } from './components/missing-page/missing-page';
-export const routes: Routes = [];
+export const roles = {
+ UserAdmin: 'useradmin',
+ MemberAdmin: 'memberadmin',
+};
+
+function requireRoles(roles: string[], any: boolean = false): CanActivateFn {
+ const isAllowed = async (
+ _: ActivatedRouteSnapshot,
+ __: RouterStateSnapshot,
+ authData: AuthGuardData
+ ): Promise => {
+ const { authenticated, grantedRoles } = authData;
+ const allRoles = Object.values(grantedRoles).flat();
+
+ const hasRequiredRoles = any
+ ? roles.some((role) => allRoles.includes(role))
+ : roles.every((role) => allRoles.includes(role));
+
+ if (authenticated && hasRequiredRoles) {
+ return true;
+ }
+ const router = inject(Router);
+ return router.parseUrl('/login');
+ };
+
+ return createAuthGuard(isAllowed);
+}
+
+export const routes: Routes = [
+ { path: 'login', component: Login },
+ {
+ path: '',
+ loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
+ canActivate: [requireRoles([roles.MemberAdmin, roles.UserAdmin], true)],
+ },
+ { path: '**', component: MissingPage },
+];
diff --git a/packages/frontend/src/app/app.scss b/packages/frontend/src/app/app.scss
index e69de29..2e49617 100644
--- a/packages/frontend/src/app/app.scss
+++ b/packages/frontend/src/app/app.scss
@@ -0,0 +1,10 @@
+@media print {
+ .no-print {
+ display: none;
+ visibility: hidden;
+ }
+
+ :not(.no-print) {
+ visibility: visible;
+ }
+}
diff --git a/packages/frontend/src/app/app.ts b/packages/frontend/src/app/app.ts
index d560a9f..4640aab 100644
--- a/packages/frontend/src/app/app.ts
+++ b/packages/frontend/src/app/app.ts
@@ -1,12 +1,18 @@
import { Component, signal } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterModule, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
+ imports: [RouterOutlet, MatButtonModule, RouterModule],
templateUrl: './app.html',
- styleUrl: './app.scss'
+ styleUrl: './app.scss',
})
export class App {
+ protected loggedIn = signal(false);
protected readonly title = signal('frontend');
+
+ protected onLogOut() {
+ this.loggedIn.set(false);
+ }
}
diff --git a/packages/frontend/src/app/components/home/home.html b/packages/frontend/src/app/components/home/home.html
new file mode 100644
index 0000000..5f2c53f
--- /dev/null
+++ b/packages/frontend/src/app/components/home/home.html
@@ -0,0 +1 @@
+home works!
diff --git a/packages/frontend/src/app/components/home/home.scss b/packages/frontend/src/app/components/home/home.scss
new file mode 100644
index 0000000..e69de29
diff --git a/packages/frontend/src/app/components/home/home.spec.ts b/packages/frontend/src/app/components/home/home.spec.ts
new file mode 100644
index 0000000..e2d9468
--- /dev/null
+++ b/packages/frontend/src/app/components/home/home.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Home } from './home';
+
+describe('Home', () => {
+ let component: Home;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Home]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Home);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/packages/frontend/src/app/components/home/home.ts b/packages/frontend/src/app/components/home/home.ts
new file mode 100644
index 0000000..fe83818
--- /dev/null
+++ b/packages/frontend/src/app/components/home/home.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-home',
+ imports: [],
+ templateUrl: './home.html',
+ styleUrl: './home.scss',
+})
+export class Home {
+
+}
diff --git a/packages/frontend/src/app/components/login/login.html b/packages/frontend/src/app/components/login/login.html
new file mode 100644
index 0000000..06d2bbd
--- /dev/null
+++ b/packages/frontend/src/app/components/login/login.html
@@ -0,0 +1 @@
+Weiterleitung zur Login-Seite erfolgt in Kürze...
diff --git a/packages/frontend/src/app/components/login/login.scss b/packages/frontend/src/app/components/login/login.scss
new file mode 100644
index 0000000..e69de29
diff --git a/packages/frontend/src/app/components/login/login.spec.ts b/packages/frontend/src/app/components/login/login.spec.ts
new file mode 100644
index 0000000..dd8bbb3
--- /dev/null
+++ b/packages/frontend/src/app/components/login/login.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Login } from './login';
+
+describe('Login', () => {
+ let component: Login;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Login]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Login);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/packages/frontend/src/app/components/login/login.ts b/packages/frontend/src/app/components/login/login.ts
new file mode 100644
index 0000000..7d1526b
--- /dev/null
+++ b/packages/frontend/src/app/components/login/login.ts
@@ -0,0 +1,15 @@
+import { Component, inject } from '@angular/core';
+import { Authentication } from '../../services/authentication';
+
+@Component({
+ selector: 'app-login',
+ imports: [],
+ templateUrl: './login.html',
+ styleUrl: './login.scss',
+})
+export class Login {
+ private readonly authentication = inject(Authentication);
+ public constructor() {
+ this.authentication.login();
+ }
+}
diff --git a/packages/frontend/src/app/components/missing-page/missing-page.html b/packages/frontend/src/app/components/missing-page/missing-page.html
new file mode 100644
index 0000000..5f1f15c
--- /dev/null
+++ b/packages/frontend/src/app/components/missing-page/missing-page.html
@@ -0,0 +1 @@
+missing-page works!
diff --git a/packages/frontend/src/app/components/missing-page/missing-page.scss b/packages/frontend/src/app/components/missing-page/missing-page.scss
new file mode 100644
index 0000000..e69de29
diff --git a/packages/frontend/src/app/components/missing-page/missing-page.spec.ts b/packages/frontend/src/app/components/missing-page/missing-page.spec.ts
new file mode 100644
index 0000000..34f9c4f
--- /dev/null
+++ b/packages/frontend/src/app/components/missing-page/missing-page.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MissingPage } from './missing-page';
+
+describe('MissingPage', () => {
+ let component: MissingPage;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [MissingPage]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(MissingPage);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/packages/frontend/src/app/components/missing-page/missing-page.ts b/packages/frontend/src/app/components/missing-page/missing-page.ts
new file mode 100644
index 0000000..945845f
--- /dev/null
+++ b/packages/frontend/src/app/components/missing-page/missing-page.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-missing-page',
+ imports: [],
+ templateUrl: './missing-page.html',
+ styleUrl: './missing-page.scss',
+})
+export class MissingPage {
+
+}
diff --git a/packages/frontend/src/app/components/test/test.html b/packages/frontend/src/app/components/test/test.html
new file mode 100644
index 0000000..941f267
--- /dev/null
+++ b/packages/frontend/src/app/components/test/test.html
@@ -0,0 +1 @@
+test works!
diff --git a/packages/frontend/src/app/components/test/test.scss b/packages/frontend/src/app/components/test/test.scss
new file mode 100644
index 0000000..e69de29
diff --git a/packages/frontend/src/app/components/test/test.spec.ts b/packages/frontend/src/app/components/test/test.spec.ts
new file mode 100644
index 0000000..a469f82
--- /dev/null
+++ b/packages/frontend/src/app/components/test/test.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Test } from './test';
+
+describe('Test', () => {
+ let component: Test;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Test]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Test);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/packages/frontend/src/app/components/test/test.ts b/packages/frontend/src/app/components/test/test.ts
new file mode 100644
index 0000000..2dd40d0
--- /dev/null
+++ b/packages/frontend/src/app/components/test/test.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-test',
+ imports: [],
+ templateUrl: './test.html',
+ styleUrl: './test.scss',
+})
+export class Test {
+
+}
diff --git a/packages/frontend/src/app/services/authentication.spec.ts b/packages/frontend/src/app/services/authentication.spec.ts
new file mode 100644
index 0000000..d3617bd
--- /dev/null
+++ b/packages/frontend/src/app/services/authentication.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { Authentication } from './authentication';
+
+describe('Authentication', () => {
+ let service: Authentication;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(Authentication);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/packages/frontend/src/app/services/authentication.ts b/packages/frontend/src/app/services/authentication.ts
new file mode 100644
index 0000000..eba2cca
--- /dev/null
+++ b/packages/frontend/src/app/services/authentication.ts
@@ -0,0 +1,84 @@
+import { effect, inject, Injectable, signal } from '@angular/core';
+import {
+ KEYCLOAK_EVENT_SIGNAL,
+ KeycloakEventType,
+ ReadyArgs,
+ typeEventArgs,
+} from 'keycloak-angular';
+import Keycloak, { KeycloakProfile } from 'keycloak-js';
+
+export enum AuthenticationState {
+ Authenticated,
+ Unauthenticated,
+ Unknown,
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class Authentication {
+ private readonly keycloak = inject(Keycloak);
+ private readonly keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL);
+ private readonly _authenticationState = signal(AuthenticationState.Unknown);
+ private readonly _userInfo = signal(null);
+ public readonly authenticationState = this._authenticationState.asReadonly();
+ public readonly userInfo = this._userInfo.asReadonly();
+
+ public constructor() {
+ effect(() => {
+ const event = this.keycloakSignal();
+ switch (event?.type) {
+ case KeycloakEventType.Ready:
+ this._authenticationState.set(
+ typeEventArgs(event.args)
+ ? AuthenticationState.Authenticated
+ : AuthenticationState.Unauthenticated
+ );
+ break;
+ case KeycloakEventType.AuthSuccess:
+ this._authenticationState.set(AuthenticationState.Authenticated);
+ break;
+ case KeycloakEventType.AuthLogout:
+ this._authenticationState.set(AuthenticationState.Unauthenticated);
+ break;
+ case KeycloakEventType.AuthError:
+ this._authenticationState.set(AuthenticationState.Unauthenticated);
+ break;
+ case KeycloakEventType.AuthRefreshError:
+ this._authenticationState.set(AuthenticationState.Unauthenticated);
+ break;
+ case KeycloakEventType.AuthRefreshSuccess:
+ this._authenticationState.set(AuthenticationState.Authenticated);
+ break;
+ case KeycloakEventType.TokenExpired:
+ this._authenticationState.set(AuthenticationState.Unauthenticated);
+ break;
+ default:
+ break;
+ }
+ });
+
+ effect(async () => {
+ const authenticationState = this.authenticationState();
+ if (authenticationState !== AuthenticationState.Authenticated) {
+ this._userInfo.set(null);
+ }
+ const profile = await this.keycloak.loadUserProfile();
+ this._userInfo.set(profile);
+ });
+ }
+
+ public async login(location?: string): Promise {
+ var redirectUri = location
+ ? `${window.location.origin}/${location}`
+ : `${window.location.origin}/login`;
+ return await this.keycloak.login({
+ redirectUri: redirectUri,
+ locale: 'de-DE',
+ });
+ }
+
+ public async logout(): Promise {
+ return await this.keycloak.logout({ redirectUri: window.location.origin + '/login' });
+ }
+}
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index 3af61ec..8492554 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -1,13 +1,19 @@
-
-
-
-
- Frontend
-
-
-
-
-
-
-
+
+
+
+
+
+ 1.TC Chemnitz Mitgliederdatenbank
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/robots.txt b/packages/frontend/src/robots.txt
new file mode 100644
index 0000000..1f53798
--- /dev/null
+++ b/packages/frontend/src/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/packages/frontend/src/styles.scss b/packages/frontend/src/styles.scss
index 90d4ee0..c3f88db 100644
--- a/packages/frontend/src/styles.scss
+++ b/packages/frontend/src/styles.scss
@@ -1 +1,39 @@
+
+// Include theming for Angular Material with `mat.theme()`.
+// This Sass mixin will define CSS variables that are used for styling Angular Material
+// components according to the Material 3 design spec.
+// Learn more about theming and how to use it for your application's
+// custom components at https://material.angular.dev/guide/theming
+@use '@angular/material' as mat;
+
+html {
+ @include mat.theme((
+ color: (
+ primary: mat.$cyan-palette,
+ tertiary: mat.$orange-palette,
+ ),
+ typography: Roboto,
+ density: 0,
+ ));
+}
+
+body {
+ // Default the application to a light color theme. This can be changed to
+ // `dark` to enable the dark color theme, or to `light dark` to defer to the
+ // user's system settings.
+ color-scheme: light;
+
+ // Set a default background, font and text colors for the application using
+ // Angular Material's system-level CSS variables. Learn more about these
+ // variables at https://material.angular.dev/guide/system-variables
+ background-color: var(--mat-sys-surface);
+ color: var(--mat-sys-on-surface);
+ font: var(--mat-sys-body-medium);
+
+ // Reset the user agent margin.
+ margin: 0;
+}
/* You can add global styles to this file, and also import other style files */
+
+html, body { height: 100%; }
+body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }