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()){ +
+ + Home +
+} + 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; }