authentication

This commit is contained in:
Bastian Wagner
2024-09-13 21:14:09 +02:00
parent c00aad559d
commit b4a5f04505
65 changed files with 1140 additions and 77 deletions

View File

@@ -32,8 +32,10 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss",
"src/styles/ag.css"
"src/styles/ag.css",
"node_modules/@ngxpert/hot-toast/src/styles/styles.css"
],
"scripts": []
},
@@ -56,7 +58,13 @@
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
@@ -95,6 +103,7 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []

View File

@@ -9,13 +9,17 @@
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/cdk": "^18.2.4",
"@angular/common": "^18.0.0",
"@angular/compiler": "^18.0.0",
"@angular/core": "^18.0.0",
"@angular/forms": "^18.0.0",
"@angular/material": "^18.2.4",
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"@ngneat/overview": "^6.0.0",
"@ngxpert/hot-toast": "^3.0.1",
"ag-grid-angular": "^32.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -343,6 +347,22 @@
}
}
},
"node_modules/@angular/cdk": {
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.4.tgz",
"integrity": "sha512-o+TuxZDqStfkviEkCR05pVyP6R2RIruEs/45Cms76hlsIheMoxRaxir/yrHdh4tZESJJhcO/EVE+aymNIRWAfg==",
"dependencies": {
"tslib": "^2.3.0"
},
"optionalDependencies": {
"parse5": "^7.1.2"
},
"peerDependencies": {
"@angular/common": "^18.0.0 || ^19.0.0",
"@angular/core": "^18.0.0 || ^19.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/cli": {
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.4.tgz",
@@ -470,6 +490,23 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/material": {
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.4.tgz",
"integrity": "sha512-F09145mI/EAHY9ngdnQTo3pFRmUoU/50i6cmddtL4cse0WidatoodQr0gZCksxhmpJgRy5mTcjh/LU2hShOgcA==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": "^18.0.0 || ^19.0.0",
"@angular/cdk": "18.2.4",
"@angular/common": "^18.0.0 || ^19.0.0",
"@angular/core": "^18.0.0 || ^19.0.0",
"@angular/forms": "^18.0.0 || ^19.0.0",
"@angular/platform-browser": "^18.0.0 || ^19.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/platform-browser": {
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz",
@@ -3293,6 +3330,17 @@
"win32"
]
},
"node_modules/@ngneat/overview": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@ngneat/overview/-/overview-6.0.0.tgz",
"integrity": "sha512-pm4bAEYtnUl8q82dwjh5NN9HF0WTFEI58VtR12izp9Oaa2dtseX82VUArfb4fadmlbHpPMUwXHrsm0ORyWii2A==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/core": ">=17"
}
},
"node_modules/@ngtools/webpack": {
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.4.tgz",
@@ -3309,6 +3357,18 @@
"webpack": "^5.54.0"
}
},
"node_modules/@ngxpert/hot-toast": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@ngxpert/hot-toast/-/hot-toast-3.0.1.tgz",
"integrity": "sha512-pMXUtvXSsF5QIOJ4hAg8TdhAagkOpPVJg1nJadLqnVXOHMpV1r57XZq45ISNtuy91hHXRUfCjc78bMAFSx3f4Q==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": ">= 18.0.0",
"@ngneat/overview": "6.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -6155,7 +6215,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.12"
},
@@ -10000,7 +10060,7 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"dev": true,
"devOptional": true,
"dependencies": {
"entities": "^4.4.0"
},

View File

@@ -11,13 +11,17 @@
"private": true,
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/cdk": "^18.2.4",
"@angular/common": "^18.0.0",
"@angular/compiler": "^18.0.0",
"@angular/core": "^18.0.0",
"@angular/forms": "^18.0.0",
"@angular/material": "^18.2.4",
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"@ngneat/overview": "^6.0.0",
"@ngxpert/hot-toast": "^3.0.1",
"ag-grid-angular": "^32.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -36,4 +40,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

1
client/public/key.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="Layer_1" enable-background="new 0 0 100 100" height="100" viewBox="0 0 100 100" width="100" xmlns="http://www.w3.org/2000/svg"><g><path d="m88.287 60.723h-4.46v-26.525c0-4.909-3.62-8.902-8.07-8.902h-51.515c-4.449 0-8.068 3.994-8.068 8.902v26.524h-4.461c-1.104 0-2 .896-2 2v2.68c0 5.129 4.173 9.302 9.302 9.302h61.971c5.13 0 9.302-4.173 9.302-9.302v-2.68c-.001-1.104-.895-1.999-2.001-1.999zm-64.045-31.427h51.515c2.206 0 4.07 2.245 4.07 4.902v26.524h-59.653v-26.524c0-2.657 1.863-4.902 4.068-4.902zm56.744 41.408h-61.971c-2.923 0-5.302-2.378-5.302-5.302v-.68h4.461 63.653 4.46v.68c0 2.924-2.378 5.302-5.301 5.302z" fill="#2b273d"/><path d="m28.525 53.705h10.805c.552 0 1-.447 1-1v-10.807c0-.552-.448-1-1-1h-1.148c.016-.071.043-.139.043-.215 0-2.819-1.928-5.113-4.297-5.113-2.371 0-4.299 2.294-4.299 5.113 0 .076.027.144.043.215h-1.148c-.552 0-1 .448-1 1v10.807c.001.553.449 1 1.001 1zm3.105-13.021c0-1.688 1.053-3.113 2.299-3.113 1.245 0 2.297 1.426 2.297 3.113 0 .076.027.144.043.215h-4.683c.017-.072.044-.139.044-.215zm-2.105 2.214h8.805v8.807h-8.805z" fill="#8e2db2"/><g fill="#2b273d"><path d="m44.695 52.729c0 .554.448 1 1 1h19.019c4.28 0 7.763-2.873 7.763-6.405 0-3.529-3.481-6.399-7.763-6.399h-19.019c-.552 0-1 .448-1 1s.448 1 1 1h19.019c3.123 0 5.763 2.015 5.763 4.399 0 2.43-2.586 4.405-5.763 4.405h-19.019c-.552 0-1 .447-1 1z"/><path d="m45.695 47.172 1.336.369-.934 1.113.709.463.754-1.163.75 1.204.708-.482-.897-1.086 1.352-.369-.266-.782-1.271.525.107-1.47h-.877l.104 1.441-1.308-.537z"/><path d="m51.686 47.172 1.335.369-.934 1.113.709.463.754-1.163.748 1.204.709-.482-.898-1.086 1.353-.369-.267-.782-1.27.525.107-1.47h-.878l.103 1.441-1.307-.537z"/><path d="m57.675 47.172 1.334.369-.934 1.113.709.463.754-1.163.75 1.204.709-.482-.897-1.086 1.35-.369-.262-.782-1.273.525.106-1.47h-.875l.101 1.441-1.308-.537z"/><path d="m63.663 47.172 1.338.369-.937 1.113.709.463.754-1.163.75 1.204.707-.482-.893-1.086 1.35-.369-.266-.782-1.271.525.108-1.47h-.878l.102 1.441-1.304-.537z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,10 +1,16 @@
<router-outlet></router-outlet>
<!-- <div class="example-sidenav-content">
<button type="button" mat-button (click)="drawer.toggle()">
Toggle sidenav
</button>
</div> -->
<!-- <div class="content" style="width: 100%; height: 100%;"> -->
<!-- The AG Grid component, with Dimensions, CSS Theme, Row Data, and Column Definition -->
<ag-grid-angular
style="width: 100%; height: 100%;"
[rowData]="rowData"
[columnDefs]="colDefs"
[defaultColDef]="defaultColDef"
/>
<!-- </div> -->

View File

@@ -1,5 +1,8 @@
:host {
display: flex;
height: 100vh;
width: 100vw;
}
overflow: hidden;
display: flex;
flex-direction: column;
}

View File

@@ -1,62 +1,36 @@
import { HttpClient } from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { Component, inject, LOCALE_ID } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AgGridAngular } from 'ag-grid-angular'; // Angular Data Grid Component
import { ColDef } from 'ag-grid-community'; // Column Definition Type Interface
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, AgGridAngular],
providers: [[{ provide: LOCALE_ID, useValue: 'de-DE' }]],
imports: [RouterOutlet,],
providers: [
{ provide: LOCALE_ID, useValue: 'de-DE' },
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'client';
defaultColDef: ColDef = {
flex: 1,
editable: true
};
private http: HttpClient = inject(HttpClient);
constructor() {
this.http.get('api/').subscribe({
next: n => {
console.log(n)
},
error: e => {
console.log(e)
}
})
}
rowData = [
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
];
// Column Definitions: Defines the columns to be displayed.
colDefs: ColDef[] = [
{ field: "make" },
{
field: "model",
cellEditor: 'agSelectCellEditor',
singleClickEdit: true,
cellEditorParams: {
values: ['English', 'Spanish', 'French', 'Portuguese', '(other)'],
}
},
{ field: "price", type: 'number'
// cellEditor: 'agDateCellEditor',
// cellEditorParams: {
// min: '2000-01-01',
// max: '2019-12-31',
// }
},
{ field: "electric", editable: true }
];
ngOnInit(): void {
}
}

View File

@@ -1,9 +1,21 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHotToastConfig } from '@ngxpert/hot-toast';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { tokenInterceptor } from './core/interceptor/token.interceptor';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withInterceptors([tokenInterceptor]))
, provideHotToastConfig({
stacking: "depth",
visibleToasts: 5,
position: 'top-center',
theme: 'toast',
autoClose: true,
dismissible: false,
duration: 5000
}), provideAnimationsAsync()]
};

View File

@@ -1,3 +1,16 @@
import { Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { AuthenticatedGuard } from './core/auth/auth.guard';
import { StartComponent } from './modules/start/start.component';
import { LoginComponent } from './modules/auth/login/login.component';
import { LayoutComponent } from './core/layout/layout.component';
import { DashboardComponent } from './modules/dashboard/dashboard.component';
import { AllUsersComponent } from './modules/admin/all-users/all-users.component';
export const routes: Routes = [];
export const routes: Routes = [
{ path: '', component: LayoutComponent, canActivate: [AuthenticatedGuard], children: [
{ path: '', component: DashboardComponent },
{ path: 'users', component: AllUsersComponent }
]},
{ path: 'login', component: LoginComponent},
];

View File

@@ -0,0 +1,36 @@
import { inject, Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router } from "@angular/router";
import { HotToastService } from "@ngxpert/hot-toast";
import { AuthService } from "./auth.service";
@Injectable({
providedIn: 'root'
})
export class AuthenticatedGuard {
public isLoading = false;
private router = inject(Router);
private toast = inject(HotToastService);
private authService = inject(AuthService);
async canActivate(route: ActivatedRouteSnapshot):
Promise<boolean> {
this.isLoading = true;
if (this.authService.authenticated) { return true; }
const s = await this.authService.getMe();
if (s) {
return true;
}
const authCode = route.queryParams["code"];
if (authCode) {
const success = await this.authService.authenticateWithCode(authCode);
if (success) { return true; }
}
this.router.navigateByUrl('/login');
return false;
}
}

View File

@@ -0,0 +1,108 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable, tap, of, catchError } from 'rxjs';
import { IUser } from '../../model/interface/user.interface';
import { environment } from '../../../environments/environment.development';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private accessTokenSubject = new BehaviorSubject<string | null>(null);
private refreshToken: string | null = null;
private http: HttpClient = inject(HttpClient);
private route: ActivatedRoute = inject(ActivatedRoute);
private user: IUser | null = null;
constructor() {
const token = localStorage.getItem('accessToken_vault');
const refresh = localStorage.getItem('refreshToken_vault');
this.accessTokenSubject.next(token);
this.refreshToken = refresh;
}
getMe() {
if (!this.getAccessToken()) {
return false;
}
return new Promise(resolve => {
this.http.get<IUser>('/api/auth/me').subscribe({
next: user => {
this.user = user;
resolve(true)
},
error: () => {
resolve(false)
}
})
})
}
authenticateWithCode(authcode: string) {
return new Promise(resolve => {
this.http.post<IUser>('/api/auth/auth-code', { code: authcode }).subscribe(user => {
this.setTokens({ accessToken: user.accessToken, refreshToken: user.refreshToken});
this.user = user;
return resolve(true)
})
})
}
get authenticated(): boolean {
return this.user != null;
}
login(credentials: { username: string; password: string }): Observable<any> {
return this.http.post<any>('/api/auth/login', credentials).pipe(
tap(tokens => {
this.setTokens(tokens);
})
);
}
private setTokens(tokens: { accessToken: string; refreshToken: string }) {
this.accessTokenSubject.next(tokens.accessToken);
this.refreshToken = tokens.refreshToken;
localStorage.setItem('accessToken_vault', tokens.accessToken);
localStorage.setItem('refreshToken_vault', tokens.refreshToken);
}
getAccessToken(): string | null {
return this.accessTokenSubject.value;
}
refreshAccessToken(): Observable<any> {
if (!this.refreshToken) {
return of(null);
}
return this.http.post<any>('/api/auth/refresh', { refreshToken: this.refreshToken }).pipe(
tap(tokens => {
this.setTokens(tokens);
}),
catchError(() => {
this.logout();
return of(null);
})
);
}
logout() {
this.accessTokenSubject.next(null);
this.refreshToken = null;
localStorage.removeItem('accessToken_vault');
localStorage.removeItem('refreshToken_vault');
}
public routeToLogin() {
const url = `https://sso.beantastic.de?client_id=ffc46841-26f8-4946-a57a-5a9f8f21bc13&redirect_uri=${environment.location}`;
location.href = url;
}
}

View File

@@ -0,0 +1,52 @@
import { inject } from '@angular/core';
import {
HttpEvent,
HttpRequest,
HttpErrorResponse,
HttpInterceptorFn,
HttpHandlerFn
} from '@angular/common/http';
import { Observable, catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { HotToastService } from '@ngxpert/hot-toast';
export const tokenInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn
): Observable<HttpEvent<any>> => {
const authService: AuthService = inject(AuthService);
const toast: HotToastService = inject(HotToastService);
const accessToken = authService.getAccessToken();
let authReq = req;
if (accessToken) {
authReq = req.clone({
setHeaders: { Authorization: `Bearer ${accessToken}` }
});
}
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return authService.refreshAccessToken().pipe(
switchMap(() => {
const newAccessToken = authService.getAccessToken();
const newAuthReq = req.clone({
setHeaders: { Authorization: `Bearer ${newAccessToken}` }
});
return next(newAuthReq);
}),
catchError(err => {
authService.logout();
toast.error(err.error.message)
return throwError(() => err);
})
);
} else {
return throwError(() => error);
}
})
);
}

View File

@@ -0,0 +1,29 @@
<mat-toolbar>
<button mat-icon-button (click)="drawer.toggle()">
<mat-icon>menu</mat-icon>
</button>
<span>Keyvault</span>
<span class="example-spacer"></span>
<button mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon">
<mat-icon>share</mat-icon>
</button>
</mat-toolbar>
<mat-drawer-container class="example-container" autosize>
<mat-drawer #drawer class="main_sidenav" mode="side" opened="true">
sdf
</mat-drawer>
<router-outlet></router-outlet>
<!-- <div class="example-sidenav-content">
<button type="button" mat-button (click)="drawer.toggle()">
Toggle sidenav
</button>
</div> -->
</mat-drawer-container>

View File

@@ -0,0 +1,24 @@
:host {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
mat-drawer-container {
flex: 1 1 auto;
}
.example-spacer {
flex: 1 1 auto;
}
mat-drawer, mat-toolbar {
background-color: #fff;
border-radius: 0;
}
.main_sidenav{
width: 200px;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LayoutComponent } from './layout.component';
describe('LayoutComponent', () => {
let component: LayoutComponent;
let fixture: ComponentFixture<LayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-layout',
standalone: true,
imports: [MatButtonModule, MatIconModule, MatSidenavModule, RouterModule, MatToolbarModule],
templateUrl: './layout.component.html',
styleUrl: './layout.component.scss'
})
export class LayoutComponent {
}

View File

@@ -0,0 +1,8 @@
export interface IUser {
username: string;
id: string;
lastName: string;
firstName: String;
refreshToken: string;
accessToken: string;
}

View File

@@ -0,0 +1,7 @@
@if (gridOptions || true) {
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
/>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AllUsersComponent } from './all-users.component';
describe('AllUsersComponent', () => {
let component: AllUsersComponent;
let fixture: ComponentFixture<AllUsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AllUsersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AllUsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,80 @@
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { ApiService } from '../../../shared/api.service';
import { AgGridAngular } from 'ag-grid-angular';
import { GridOptions,GridApi, GridReadyEvent, CellEditingStoppedEvent } from 'ag-grid-community';
import { HotToastService } from '@ngxpert/hot-toast';
@Component({
selector: 'app-all-users',
standalone: true,
imports: [AgGridAngular],
templateUrl: './all-users.component.html',
styleUrl: './all-users.component.scss'
})
export class AllUsersComponent {
private toast: HotToastService = inject(HotToastService);
private api: ApiService = inject(ApiService);
gridApi!: GridApi;
private roles: string [] = [];
gridOptions: GridOptions = {
rowData: [],
columnDefs: [
{ field: 'username' , headerName: 'User', flex: 1, editable: true, sort: 'asc' },
{ field: 'firstName', headerName: 'Vorname', flex: 1, editable: true},
{ field: 'lastName', headerName: 'Nachname', flex: 1, editable: true},
{ field: 'isActive', headerName: 'Aktiv', flex: 1, editable: true, },
{ field: 'role', headerName: 'Rolle', flex: 1, editable: true, cellEditor: 'agSelectCellEditor',
cellEditorParams: {
values: ['user', 'develop', 'admin'],
},
singleClickEdit: true,
},
],
loading: true,
overlayLoadingTemplate: 'Lade Daten...'
}
ngOnInit(): void {
}
loadUsers() {
this.api.getAllUsers().subscribe({
next: n => {
this.gridApi.setGridOption("rowData", n)
this.gridApi.setGridOption("loading", false);
}
})
}
cellEditEnd(params: CellEditingStoppedEvent, self: AllUsersComponent) {
if (!params.valueChanged) { return; }
self.api.saveUser(params.data)
.pipe(
self.toast.observe({
loading: 'speichern...',
success: 'Änderungen gespeichert',
error: 'Änderungen konnten nicht gespeichert werden!'
})
).subscribe({
error: () => {
const data = self.gridApi.getRowNode(params.node.id as string);
data?.setDataValue(params.colDef.field as string, params.oldValue)
}
});
}
onGridReady(params: GridReadyEvent) {
this.gridApi = params.api;
const self = this;
this.gridApi.addEventListener("cellEditingStopped", evt => this.cellEditEnd(evt, self))
this.loadUsers();
console.log(params)
}
}

View File

@@ -0,0 +1 @@
<button mat-flat-button color="primary" (click)="authService.routeToLogin()">Login</button>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { AuthService } from '../../../core/auth/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [MatButtonModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
public authService: AuthService = inject(AuthService);
}

View File

@@ -0,0 +1,7 @@
<ag-grid-angular
style="width: 100%; height: 100%;"
[rowData]="rowData"
[columnDefs]="colDefs"
[defaultColDef]="defaultColDef"
/>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,51 @@
import { Component } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import { ColDef } from 'ag-grid-community'; // Column Definition Type Interface
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [AgGridAngular],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss'
})
export class DashboardComponent {
defaultColDef: ColDef = {
flex: 1,
editable: true
};
rowData = [
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
];
// Column Definitions: Defines the columns to be displayed.
colDefs: ColDef[] = [
{ field: "make" },
{
field: "model",
cellEditor: 'agSelectCellEditor',
singleClickEdit: true,
cellEditorParams: {
values: ['English', 'Spanish', 'French', 'Portuguese', '(other)'],
}
},
{ field: "price", type: 'number'
// cellEditor: 'agDateCellEditor',
// cellEditorParams: {
// min: '2000-01-01',
// max: '2019-12-31',
// }
},
{ field: "electric", editable: true }
];
}

View File

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

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StartComponent } from './start.component';
describe('StartComponent', () => {
let component: StartComponent;
let fixture: ComponentFixture<StartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StartComponent]
})
.compileComponents();
fixture = TestBed.createComponent(StartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-start',
standalone: true,
imports: [],
templateUrl: './start.component.html',
styleUrl: './start.component.scss'
})
export class StartComponent {
private http: HttpClient = inject(HttpClient);
ngOnInit(): void {
this.http.get('/api/').subscribe(res => {
console.log(res)
})
}
}

View File

@@ -0,0 +1,26 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { IUser } from '../model/interface/user.interface';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private http: HttpClient = inject(HttpClient);
constructor() { }
getAllUsers(): Observable<IUser[]> {
return this.http.get<IUser[]>('/api/user');
}
saveUser(user: IUser) {
return this.http.post('/api/user', user);
}
getRoles(): Observable<{id: string, name: string}[]> {
return this.http.get<{id: string, name: string}[]>('/api/role');
}
}

View File

@@ -0,0 +1,3 @@
export const environment = {
location: 'http://localhost:4200'
};

View File

@@ -0,0 +1,3 @@
export const environment = {
location: 'https://keyvaultpro.de'
};

View File

@@ -5,9 +5,11 @@
<title>Client</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" href="key.svg">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@@ -5,9 +5,12 @@ html, body {
overflow: hidden;
margin: 0;
padding: 0;
background-color: #e2e2e2;
}
/* Core Data Grid CSS */
@import "ag-grid-community/styles/ag-grid.css";
/* Quartz Theme Specific CSS */
@import 'ag-grid-community/styles/ag-theme-quartz.css';
@import 'ag-grid-community/styles/ag-theme-quartz.css';
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }