diff --git a/idp/src/application/user/user.controller.ts b/idp/src/application/user/user.controller.ts index 51289a6..25b0304 100644 --- a/idp/src/application/user/user.controller.ts +++ b/idp/src/application/user/user.controller.ts @@ -61,4 +61,9 @@ export class UserController { ): Promise { return this.clientService.saveRedirectUris(req.user, id, uris); } + + @Get('logins') + getUserLogins() { + return this.userService.getUserLogins(); + } } diff --git a/idp/src/application/user/user.service.ts b/idp/src/application/user/user.service.ts index b71c112..2452913 100644 --- a/idp/src/application/user/user.service.ts +++ b/idp/src/application/user/user.service.ts @@ -1,10 +1,14 @@ import { Injectable } from '@nestjs/common'; import { Client, ClientRepository } from 'src/model/client.entity'; +import { LogRepository } from 'src/model/log.entity'; import { User } from 'src/model/user.entity'; @Injectable() export class UserService { - constructor(private clientRepository: ClientRepository) {} + constructor( + private clientRepository: ClientRepository, + private logRepository: LogRepository, + ) {} getUserClients(user: User): Promise { return this.clientRepository.find({ @@ -13,4 +17,29 @@ export class UserService { order: { createdAt: 'ASC' }, }); } + + async getUserLogins() { + const logs = await this.logRepository.find({ + where: [{ type: 'login' }, { type: 'systemlogin' }], + }); + + const res = logs.reduce((acc, succ) => { + let ob = acc[succ.timestamp.toISOString().substring(0, 10)]; + if (!ob) { + ob = { logins: 0, systemLogins: 0 }; + } + if (succ.type == 'login') { + ob.logins += 1; + } else if (succ.type == 'systemlogin') { + ob.systemLogins += 1; + } + acc[succ.timestamp.toISOString().substring(0, 10)] = ob; + return acc; + }, {}); + + return Object.entries(res).map(([date, count]) => ({ + date: new Date(date), + count, + })); + } } diff --git a/idp/src/core/custom.logger.ts b/idp/src/core/custom.logger.ts index 4899cbc..bbdd65b 100644 --- a/idp/src/core/custom.logger.ts +++ b/idp/src/core/custom.logger.ts @@ -1,6 +1,6 @@ import { LoggerService, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Log } from 'src/model/log.entity'; +import { Log, LOGMESSAGETYPE } from 'src/model/log.entity'; import { Repository } from 'typeorm'; @Injectable() @@ -10,11 +10,16 @@ export class CustomLogger implements LoggerService { // constructor(private readonly connection: Connection) {} constructor(@InjectRepository(Log) private logRepository: Repository) {} - private async logToDatabase(level: string, message: string) { + private async logToDatabase( + level: string, + message: string, + type: LOGMESSAGETYPE = 'system', + ) { const logEntry = new Log(); logEntry.level = level; logEntry.message = message; logEntry.timestamp = new Date(); + logEntry.type = type; await this.logRepository.save(logEntry); } @@ -22,18 +27,18 @@ export class CustomLogger implements LoggerService { this.logger[level](message); } - async error(message: any) { - await this.logToDatabase('error', message.toString()); + async error(message: any, type?: LOGMESSAGETYPE) { + await this.logToDatabase('error', message.toString(), type); this.logToConsole('error', message); } - async warn(message: any) { - await this.logToDatabase('warn', message.toString()); + async warn(message: any, type?: LOGMESSAGETYPE) { + await this.logToDatabase('warn', message.toString(), type); this.logToConsole('warn', message); } - async log(message: any) { - this.logToDatabase('log', message); + async log(message: any, type?: LOGMESSAGETYPE) { + this.logToDatabase('log', message, type); } async debug(message: any) { diff --git a/idp/src/core/database/database.module.ts b/idp/src/core/database/database.module.ts index 0868b36..c514ac1 100644 --- a/idp/src/core/database/database.module.ts +++ b/idp/src/core/database/database.module.ts @@ -5,17 +5,26 @@ import { AuthorizationCodeRepository, } from 'src/model/auth-code.entity'; import { Client, ClientRepository } from 'src/model/client.entity'; +import { Log, LogRepository } from 'src/model/log.entity'; import { RedirectRepository, RedirectUri } from 'src/model/redirect-uri.entity'; import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity'; import { User, UserRepository } from 'src/model/user.entity'; -const ENTITIES = [User, Client, RedirectUri, AuthorizationCode, SessionKey]; +const ENTITIES = [ + User, + Client, + RedirectUri, + AuthorizationCode, + SessionKey, + Log, +]; const REPOSITORIES = [ UserRepository, ClientRepository, AuthorizationCodeRepository, SessionKeyRepository, RedirectRepository, + LogRepository, ]; @Module({ diff --git a/idp/src/model/log.entity.ts b/idp/src/model/log.entity.ts index 804a69a..17b297d 100644 --- a/idp/src/model/log.entity.ts +++ b/idp/src/model/log.entity.ts @@ -1,4 +1,11 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + Repository, + DataSource, +} from 'typeorm'; @Entity() export class Log { @@ -13,4 +20,16 @@ export class Log { @Column() timestamp: Date; + + @Column({ default: 'system' }) + type: LOGMESSAGETYPE; } + +@Injectable() +export class LogRepository extends Repository { + constructor(dataSource: DataSource) { + super(Log, dataSource.createEntityManager()); + } +} + +export type LOGMESSAGETYPE = 'login' | 'system' | 'register' | 'systemlogin'; diff --git a/idp/src/users/users.service.ts b/idp/src/users/users.service.ts index 33d27ac..ddca7cb 100644 --- a/idp/src/users/users.service.ts +++ b/idp/src/users/users.service.ts @@ -40,6 +40,7 @@ export class UsersService { const uu = await this.userRepo.save(u); this.logger.log( `User ${userDto.username} created for client ${userDto.clientId}`, + 'register', ); return uu; } @@ -118,11 +119,15 @@ export class UsersService { if (getUserAccessToken) { user.accessToken = this.createAccessToken(user); user.refreshToken = this.createRefreshToken(user); + this.logger.log( + `User logged in with code on client ${clientId}`, + 'systemlogin', + ); return user; } const token = await this.createAuthToken(user, client); - this.logger.log(`User logged in with code on client ${clientId}`); + this.logger.log(`User logged in with code on client ${clientId}`, 'login'); return token; } diff --git a/idp_client/package-lock.json b/idp_client/package-lock.json index fb408bc..5903e24 100644 --- a/idp_client/package-lock.json +++ b/idp_client/package-lock.json @@ -19,6 +19,7 @@ "@angular/router": "^18.0.0", "@ngneat/overview": "^6.0.0", "@ngxpert/hot-toast": "^3.0.0", + "chart.js": "^4.4.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" @@ -3146,6 +3147,11 @@ "tslib": "2" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5150,6 +5156,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/idp_client/package.json b/idp_client/package.json index 8ea5dbd..d7f78fd 100644 --- a/idp_client/package.json +++ b/idp_client/package.json @@ -21,6 +21,7 @@ "@angular/router": "^18.0.0", "@ngneat/overview": "^6.0.0", "@ngxpert/hot-toast": "^3.0.0", + "chart.js": "^4.4.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" diff --git a/idp_client/src/app/dashboard/components/card/card.component.html b/idp_client/src/app/dashboard/components/card/card.component.html index 6e0dd3e..a1fe357 100644 --- a/idp_client/src/app/dashboard/components/card/card.component.html +++ b/idp_client/src/app/dashboard/components/card/card.component.html @@ -1,5 +1,5 @@
- {{ client.clientName }}x + {{ client.clientName }}
shield_person
{{ client.admins.length }}
link
{{ client.redirectUris.length }}
diff --git a/idp_client/src/app/dashboard/components/charts/login/login.chart.component.ts b/idp_client/src/app/dashboard/components/charts/login/login.chart.component.ts new file mode 100644 index 0000000..077c2ab --- /dev/null +++ b/idp_client/src/app/dashboard/components/charts/login/login.chart.component.ts @@ -0,0 +1,69 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import Chart from 'chart.js/auto'; + +@Component({ + selector: 'app-chart-login', + standalone: true, + imports: [], + templateUrl: './login.component.html', + styleUrl: './login.component.scss' +}) +export class LoginChartComponent { + chart: any = []; + private http: HttpClient = inject(HttpClient); + + constructor() {} + + ngOnInit() { + + + + this.getData(); + + } + + private getData() { + this.http.get('api/app/user/logins').subscribe(res => { + console.log(res) + this.createChart(res); + }) + } + + private createChart(data: Login[] ) { + + + this.chart = new Chart('canvas', { + type: 'bar', + data: { + labels: data.map(d => new Date(d.date).toLocaleDateString('de-DE', {dateStyle: 'short'})), + datasets: [ + { + label: 'Logins', + data: data.map(d => d.count.logins), + borderWidth: 1, + },{ + label: 'Applogins', + data: data.map(d => d.count.systemLogins), + borderWidth: 1, + }, + ], + }, + options: { + scales: { + y: { + beginAtZero: true, + }, + x: { + stacked: true, + } + }, + }, + }); + } +} + +interface Login { + date: string; + count: { logins: number, systemLogins: number } +} \ No newline at end of file diff --git a/idp_client/src/app/dashboard/components/charts/login/login.component.html b/idp_client/src/app/dashboard/components/charts/login/login.component.html new file mode 100644 index 0000000..81e8d1c --- /dev/null +++ b/idp_client/src/app/dashboard/components/charts/login/login.component.html @@ -0,0 +1,3 @@ +
+ {{chart}} +
\ No newline at end of file diff --git a/idp_client/src/app/dashboard/components/charts/login/login.component.scss b/idp_client/src/app/dashboard/components/charts/login/login.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/idp_client/src/app/dashboard/components/charts/login/login.component.spec.ts b/idp_client/src/app/dashboard/components/charts/login/login.component.spec.ts new file mode 100644 index 0000000..18f3685 --- /dev/null +++ b/idp_client/src/app/dashboard/components/charts/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/idp_client/src/app/dashboard/dashboard.component.html b/idp_client/src/app/dashboard/dashboard.component.html index a2a3aff..52572ba 100644 --- a/idp_client/src/app/dashboard/dashboard.component.html +++ b/idp_client/src/app/dashboard/dashboard.component.html @@ -29,4 +29,6 @@ } -->
-
\ No newline at end of file + + + \ No newline at end of file diff --git a/idp_client/src/app/dashboard/dashboard.component.ts b/idp_client/src/app/dashboard/dashboard.component.ts index c27dee3..78acaf2 100644 --- a/idp_client/src/app/dashboard/dashboard.component.ts +++ b/idp_client/src/app/dashboard/dashboard.component.ts @@ -10,12 +10,12 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { CreateClientComponent } from './components/create-client/create-client.component'; import { CreateHotToastRef, HotToastService } from '@ngxpert/hot-toast'; import {MatBottomSheet, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet'; -import { ClientAdminsComponent } from './components/client-admins/client-admins.component'; +import { LoginChartComponent } from './components/charts/login/login.chart.component'; @Component({ selector: 'app-dashboard', standalone: true, - imports: [ClientCardComponent, MatButtonModule, MatIconModule, MatDialogModule, MatBottomSheetModule], + imports: [ClientCardComponent, MatButtonModule, MatIconModule, MatDialogModule, MatBottomSheetModule, LoginChartComponent], templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.scss' })