intermediate commit

This commit is contained in:
Flo 2024-08-25 20:42:39 +00:00
parent 5dd8aee455
commit 0adf0a9bfc
30 changed files with 407 additions and 114 deletions

View File

@ -47,10 +47,11 @@
"src/favicon.ico" "src/favicon.ico"
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": [
"node_modules/flowbite/dist/flowbite.min.js"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -110,7 +111,6 @@
"src/favicon.ico" "src/favicon.ico"
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.scss",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []

View File

@ -15,10 +15,10 @@
"@angular/compiler": "^15.2.0", "@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0", "@angular/core": "^15.2.0",
"@angular/forms": "^15.2.0", "@angular/forms": "^15.2.0",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.2.0", "@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0", "@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0", "@angular/router": "^15.2.0",
"flowbite": "^2.5.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "~0.12.0"
@ -34,7 +34,7 @@
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0", "karma-jasmine-html-reporter": "~2.0.0",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.10",
"typescript": "~4.9.4" "typescript": "~4.9.4"
} }
} }

View File

@ -1,9 +1,18 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AuthService } from './core/services/auth.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent implements OnInit {
constructor(
private authService: AuthService
) {
}
ngOnInit(): void {
this.authService.readUserState();
}
} }

View File

@ -8,9 +8,11 @@ import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
import { HomeComponent } from './core/components/home/home.component'; import { HomeComponent } from './core/components/home/home.component';
import { CoreModule } from './core/core.module'; import { CoreModule } from './core/core.module';
import { AuthGuard } from './core/guards/auth.guard';
const routes: Routes = [ const routes: Routes = [
{ path: 'home', component: HomeComponent }, { path: 'auth', loadChildren: () => import('./core/auth/auth.module').then(m => m.AuthModule) },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'home', pathMatch: 'full' },
]; ];

View File

@ -0,0 +1,33 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoginComponent } from './components/login/login.component';
import { RegistrationComponent } from './components/registration/registration.component';
import { ConfirmRegistrationComponent } from './components/confirm-registration/confirm-registration.component';
import { RouterModule, Routes } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'registration', component: RegistrationComponent },
{ path: 'registration/:id', component: ConfirmRegistrationComponent },
{ path: '', redirectTo: 'login', pathMatch: 'full' },
];
@NgModule({
declarations: [
LoginComponent,
RegistrationComponent,
ConfirmRegistrationComponent
],
exports: [
LoginComponent,
RegistrationComponent,
ConfirmRegistrationComponent
],
imports: [
RouterModule.forChild(routes),
CommonModule,
ReactiveFormsModule
]
})
export class AuthModule { }

View File

@ -0,0 +1 @@
<p>confirm-registration works!</p>

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-confirm-registration',
templateUrl: './confirm-registration.component.html',
styleUrls: ['./confirm-registration.component.scss']
})
export class ConfirmRegistrationComponent {
}

View File

@ -0,0 +1,24 @@
<div class="mb-10">
</div>
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-accent text-5xl mb-5">
Beekeeper
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
Anmeldung
</h1>
</div>
<form class="max-w-sm mx-auto" [formGroup]="loginForm">
<div class="mb-5">
<label for="identifier" class="block mb-2 text-sm font-medium text-skin-accent-muted">Benutzername oder E-Mail</label>
<input formControlName="identifier" type="text" id="identifier" class="bg-skin-primary border border-gray-300 text-skin-accent-muted text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="username72 / your@email.com" required />
</div>
<div class="mb-5">
<label for="password" class="block mb-2 text-sm font-medium text-skin-accent-muted">Passwort</label>
<input formControlName="password" type="password" id="password" class="bg-skin-primary border border-gray-300 text-skin-accent-muted text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required />
<p id="helper-text-explanation" class="mt-2 text-sm text-skin-accent-muted">Neu hier? <a routerLink="/auth/registration" class="font-medium text-skin-secondary hover:underline">Jetzt registrieren!</a></p>
</div>
<button (click)="login()" [disabled]="!loginForm.valid" type="submit" class="w-full 9xl:w-auto font-bold text-skin-primary bg-skin-secondary hover:bg-skin-accent rounded-lg text-sm px-5 py-2.5 text-center">Anmelden</button>
</form>

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { filter } from 'rxjs';
import { AuthService } from 'src/app/core/services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
loginForm = new FormGroup({
identifier: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]),
});
constructor(
private authService: AuthService,
private router: Router
) {
this.authService.currentState$.pipe(
filter(state => state !== undefined)
).subscribe(state => {
this.router.navigateByUrl('/home');
});
}
login(): void {
this.authService.login({
identifier: this.loginForm.value.identifier!,
password: this.loginForm.value.password!
});
}
}

View File

@ -0,0 +1 @@
<p>registration works!</p>

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.scss']
})
export class RegistrationComponent {
}

View File

@ -1,5 +1,5 @@
<app-navigation></app-navigation> <app-navigation></app-navigation>
<div> <div>
Home <iframe src="/api/health"></iframe>
</div> </div>

View File

@ -1,5 +1,47 @@
<a routerLink="/home">
<h1 class="w-full bg-zinc-700 p-5 text-white text-6xl font-bold text-center">
Navigation <nav class="bg-skin-secondary">
</h1> <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="#" class="flex items-center space-x-3">
<img src="assets/icon.png" class="h-8" alt="Beekeeper Logo" />
<span class="text-skin-primary self-center text-2xl font-semibold whitespace-nowrap">Beekeeper</span>
</a> </a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0">
<button type="button" class="block w-8 h-8 text-sm bg-skin-primary rounded-full md:me-0 focus:ring-4 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="text-skin-secondary rounded-full">AA</span>
<!-- <img class="w-8 h-8 rounded-full" src="/docs/images/people/profile-picture-3.jpg" alt="user photo"> -->
</button>
<!-- Dropdown menu -->
<div class="z-50 hidden my-4 text-base list-none divide-y divide-gray-100 rounded-lg shadow bg-skin-primary" id="user-dropdown">
<div class="px-4 py-3">
<span class="block text-sm text-skin-accent font-bold">{{state?.username}}</span>
<span class="block text-sm text-skin-accent-muted truncate">{{state?.roleIdentifier}}</span>
</div>
<ul class="p-4" aria-labelledby="user-menu-button">
<li>
<a href="#" class="block py-2 px-3 rounded text-skin-primary-muted hover:text-skin-primary md:text-skin-secondary-muted md:hover:text-skin-secondary hover:bg-skin-secondary md:p-0">Einstellungen</a>
</li>
<li>
<button class="w-full block py-2 px-3 rounded text-skin-primary-muted hover:text-skin-primary md:text-skin-secondary-muted md:hover:text-skin-secondary hover:bg-skin-secondary md:p-0" (click)="logout()">Ausloggen</button>
</li>
</ul>
</div>
<button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-skin-secondary bg-skin-primary rounded-lg md:hidden hover:bg-skin-primary-muted focus:outline-none focus:ring-2 focus:ring-gray-200" aria-controls="navbar-user" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
</div>
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 rounded-lg bg-skin-primary md:bg-skin-secondary md:space-x-8 md:flex-row md:mt-0 md:border-0 md:bg-skin-fill">
<li>
<a routerLink="/home" class="block py-2 px-3 rounded text-skin-primary md:text-skin-secondary md:p-0" aria-current="page">Home</a>
</li>
<li>
<a href="#" class="block py-2 px-3 rounded text-skin-primary-muted hover:text-skin-primary md:text-skin-secondary-muted md:hover:text-skin-secondary hover:bg-skin-secondary md:p-0">About</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@ -1,9 +1,36 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { initFlowbite } from 'flowbite';
import { AuthService } from '../../services/auth.service';
import { UserStateResponse } from '../../models/user-state-request.model';
import { Router } from '@angular/router';
import { filter, map } from 'rxjs';
@Component({ @Component({
selector: 'app-navigation', selector: 'app-navigation',
templateUrl: './navigation.component.html', templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'], styleUrls: ['./navigation.component.scss'],
}) })
export class NavigationComponent { export class NavigationComponent implements OnInit {
state: UserStateResponse | undefined | null;
constructor(
private authService: AuthService,
private router: Router,
) {
this.state = this.authService.currentState$.value;
this.authService.currentState$.pipe(
filter(state => state === undefined)
).subscribe( state =>
this.router.navigateByUrl('/auth/login')
);
}
ngOnInit(): void {
initFlowbite();
}
logout(): void {
this.authService.logout();
}
} }

View File

@ -2,6 +2,9 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HomeComponent } from './components/home/home.component'; import { HomeComponent } from './components/home/home.component';
import { NavigationComponent } from './components/navigation/navigation.component'; import { NavigationComponent } from './components/navigation/navigation.component';
import { AuthGuard } from './guards/auth.guard';
import { AuthService } from './services/auth.service';
import { RequestService } from './services/request.service';
@ -10,6 +13,7 @@ import { NavigationComponent } from './components/navigation/navigation.componen
exports: [HomeComponent, NavigationComponent], exports: [HomeComponent, NavigationComponent],
imports: [ imports: [
CommonModule CommonModule
] ],
providers: [AuthGuard, AuthService, RequestService]
}) })
export class CoreModule { } export class CoreModule { }

View File

@ -0,0 +1,26 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from "@angular/router";
import { filter, map, Observable } from "rxjs";
import { AuthService } from "../services/auth.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {
}
canActivate(): Observable<boolean> | boolean {
return this.authService.currentState$.pipe(
map((currentState) => {
if (!currentState) {
this.router.navigateByUrl('/auth');
return false;
}
return true;
})
);
}
}

View File

@ -0,0 +1,8 @@
export interface LoginRequest {
identifier: string,
password: string,
}
export interface LoginResponse {
sessionId: string
}

View File

@ -0,0 +1,12 @@
export interface UserStateRequest {
}
export interface UserStateResponse {
id: string,
sessionId: string,
username: string,
roleIdentifier: string,
createdAt: string,
updatedAt: string,
permissions: string[],
}

View File

@ -0,0 +1,56 @@
import { Injectable } from "@angular/core";
import { RequestService } from "./request.service";
import { UserStateResponse } from "../models/user-state-request.model";
import { BehaviorSubject, Observable } from "rxjs";
import { LoginRequest, LoginResponse } from "../models/login-request.model copy";
import { Router } from "@angular/router";
@Injectable()
export class AuthService {
currentState$ = new BehaviorSubject<UserStateResponse | null | undefined>(undefined);
constructor(
private requestService: RequestService,
private router: Router
) {
}
readUserState(): void {
this.requestService.get(
'user/state',
{},
(response: UserStateResponse) => {
this.currentState$.next(response);
},
() => {
this.currentState$.next(undefined);
}
)
}
login(body: LoginRequest): LoginResponse|null {
let result = null;
this.requestService.post(
'auth/login-user',
body,
(response: LoginResponse) => {
result = response;
this.readUserState();
}
);
return result;
}
logout(): void {
this.requestService.post(
'auth/logout-user',
{},
(response: any) => {
this.readUserState();
}
);
}
}

View File

@ -1,7 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -11,24 +10,42 @@ export class RequestService {
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private router: Router, private router: Router,
private snackBar: MatSnackBar
) { ) {
} }
post(apiPath: string, body: any, fct: Function) public post(apiPath: string, body: any, successFunction: Function, errorFunction: Function|null = null)
{ {
let url = this.obtainUrl(apiPath); let url = this.obtainUrl(apiPath);
let observable = this.http.post(url, body).subscribe( let observable = this.http.post(url, body).subscribe(
(answer:any) => { (answer:any) => {
fct(answer); successFunction(answer);
}, },
(error:any) => { (error:any) => {
if (errorFunction === null)
this.handleError(error); this.handleError(error);
else
errorFunction(error);
} }
); );
} }
postFiles(apiPath: string, files: File[], fct: Function) { public get(apiPath: string, body: any, successFunction: Function, errorFunction: Function|null = null)
{
let url = this.obtainUrl(apiPath);
let observable = this.http.get(url, body).subscribe(
(answer:any) => {
successFunction(answer);
},
(error:any) => {
if (errorFunction === null)
this.handleError(error);
else
errorFunction(error);
}
);
}
public postFiles(apiPath: string, files: File[], successFunction: Function, errorFunction: Function|null = null) {
if (!files || files.length === 0) { if (!files || files.length === 0) {
throw 'Need to select at least one file'; throw 'Need to select at least one file';
} }
@ -42,23 +59,13 @@ export class RequestService {
let url = this.obtainUrl(apiPath); let url = this.obtainUrl(apiPath);
let observable = this.http.post<any>(url, formData).subscribe( let observable = this.http.post<any>(url, formData).subscribe(
(answer: any) => { (answer: any) => {
fct(answer); successFunction(answer);
},
(error: any) => {
this.handleError(error);
}
);
}
get(apiPath: string, body: any, fct: Function)
{
let url = this.obtainUrl(apiPath);
let observable = this.http.get(url, body).subscribe(
(answer:any) => {
fct(answer);
}, },
(error: any) => { (error: any) => {
if (errorFunction === null)
this.handleError(error); this.handleError(error);
else
errorFunction(error);
} }
); );
} }
@ -98,8 +105,9 @@ export class RequestService {
} }
private showSnackBar(message: string, action?: string) { private showSnackBar(message: string, action?: string) {
this.snackBar.open(message.toString(), action, { /*this.snackBar.open(message.toString(), action, {
duration: 3000, duration: 3000,
}); });*/
console.log(message);
} }
} }

View File

@ -7,27 +7,6 @@ import { PaginatorComponent } from './components/paginator/paginator.component';
import { TabControlComponent } from './components/tab-control/tab-control.component'; import { TabControlComponent } from './components/tab-control/tab-control.component';
import { TableComponent } from './components/table/table.component'; import { TableComponent } from './components/table/table.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -44,54 +23,12 @@ import { MatChipsModule } from '@angular/material/chips';
FormComponent, FormComponent,
TableComponent, TableComponent,
MatSlideToggleModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule,
MatFormFieldModule,
FormsModule, FormsModule,
MatExpansionModule,
MatMenuModule,
MatListModule,
MatToolbarModule,
MatSidenavModule,
MatInputModule,
MatSelectModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatCheckboxModule,
MatTabsModule,
MatTableModule,
MatChipsModule,
], ],
imports: [ imports: [
RouterModule, RouterModule,
CommonModule, CommonModule,
MatSlideToggleModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule,
MatFormFieldModule,
FormsModule, FormsModule,
MatExpansionModule,
MatMenuModule,
MatListModule,
MatToolbarModule,
MatSidenavModule,
MatInputModule,
MatSelectModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatCheckboxModule,
MatTabsModule,
MatTableModule,
MatChipsModule,
], ],
}) })
export class SharedModule {} export class SharedModule {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -5,11 +5,10 @@
<title>Bienen</title> <title>Bienen</title>
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="assets/icon.ico" /> <link rel="icon" type="image/x-icon" href="assets/icon.png" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
</head> </head>
<body class="bg-zinc-900"> <body>
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@ -1,3 +1,20 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
:root {
--color-text-primary: 0,0,0;
--color-text-primary-muted: 100,100,100;
--color-text-secondary: 255,255,255;
--color-text-secondary-muted: 170,170,170;
--color-text-accent: 255, 199, 44;
--color-text-accent-muted: 255, 199, 44;
--color-primary: 255, 255, 255;
--color-secondary: 255, 199, 44;
--color-accent: 255, 199, 44;
}
}

View File

@ -1,9 +1,41 @@
/** @type {import('tailwindcss').Config} */ function withOpacity(variableName) {
module.exports = { return ({ opacityValue }) => {
content: ["./src/**/*.{html,ts}"], if (opacityValue !== undefined) {
theme: { return `rgba(var(${variableName}), ${opacityValue})`
extend: {}, }
}, return `rgb(var(${variableName}))`
plugins: [], }
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
"./node_modules/flowbite/**/*.js"
],
theme: {
extend: {
textColor: {
skin: {
primary: withOpacity('--color-text-primary'),
'primary-muted': withOpacity('--color-text-primary-muted'),
secondary: withOpacity('--color-text-secondary'),
'secondary-muted': withOpacity('--color-text-secondary-muted'),
accent: withOpacity('--color-text-accent'),
'accent-muted': withOpacity('--color-text-accent-muted'),
},
},
backgroundColor: {
skin: {
primary: withOpacity('--color-primary'),
secondary: withOpacity('--color-secondary'),
accent: withOpacity('--color-accent'),
},
},
},
},
plugins: [
require('flowbite/plugin')
],
} }