statistics
This commit is contained in:
@@ -61,4 +61,9 @@ export class UserController {
|
|||||||
): Promise<RedirectUri[]> {
|
): Promise<RedirectUri[]> {
|
||||||
return this.clientService.saveRedirectUris(req.user, id, uris);
|
return this.clientService.saveRedirectUris(req.user, id, uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('logins')
|
||||||
|
getUserLogins() {
|
||||||
|
return this.userService.getUserLogins();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Client, ClientRepository } from 'src/model/client.entity';
|
import { Client, ClientRepository } from 'src/model/client.entity';
|
||||||
|
import { LogRepository } from 'src/model/log.entity';
|
||||||
import { User } from 'src/model/user.entity';
|
import { User } from 'src/model/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(private clientRepository: ClientRepository) {}
|
constructor(
|
||||||
|
private clientRepository: ClientRepository,
|
||||||
|
private logRepository: LogRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
getUserClients(user: User): Promise<Client[]> {
|
getUserClients(user: User): Promise<Client[]> {
|
||||||
return this.clientRepository.find({
|
return this.clientRepository.find({
|
||||||
@@ -13,4 +17,29 @@ export class UserService {
|
|||||||
order: { createdAt: 'ASC' },
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoggerService, Injectable, Logger } from '@nestjs/common';
|
import { LoggerService, Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Log } from 'src/model/log.entity';
|
import { Log, LOGMESSAGETYPE } from 'src/model/log.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -10,11 +10,16 @@ export class CustomLogger implements LoggerService {
|
|||||||
// constructor(private readonly connection: Connection) {}
|
// constructor(private readonly connection: Connection) {}
|
||||||
constructor(@InjectRepository(Log) private logRepository: Repository<Log>) {}
|
constructor(@InjectRepository(Log) private logRepository: Repository<Log>) {}
|
||||||
|
|
||||||
private async logToDatabase(level: string, message: string) {
|
private async logToDatabase(
|
||||||
|
level: string,
|
||||||
|
message: string,
|
||||||
|
type: LOGMESSAGETYPE = 'system',
|
||||||
|
) {
|
||||||
const logEntry = new Log();
|
const logEntry = new Log();
|
||||||
logEntry.level = level;
|
logEntry.level = level;
|
||||||
logEntry.message = message;
|
logEntry.message = message;
|
||||||
logEntry.timestamp = new Date();
|
logEntry.timestamp = new Date();
|
||||||
|
logEntry.type = type;
|
||||||
await this.logRepository.save(logEntry);
|
await this.logRepository.save(logEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,18 +27,18 @@ export class CustomLogger implements LoggerService {
|
|||||||
this.logger[level](message);
|
this.logger[level](message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async error(message: any) {
|
async error(message: any, type?: LOGMESSAGETYPE) {
|
||||||
await this.logToDatabase('error', message.toString());
|
await this.logToDatabase('error', message.toString(), type);
|
||||||
this.logToConsole('error', message);
|
this.logToConsole('error', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async warn(message: any) {
|
async warn(message: any, type?: LOGMESSAGETYPE) {
|
||||||
await this.logToDatabase('warn', message.toString());
|
await this.logToDatabase('warn', message.toString(), type);
|
||||||
this.logToConsole('warn', message);
|
this.logToConsole('warn', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async log(message: any) {
|
async log(message: any, type?: LOGMESSAGETYPE) {
|
||||||
this.logToDatabase('log', message);
|
this.logToDatabase('log', message, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
async debug(message: any) {
|
async debug(message: any) {
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import {
|
|||||||
AuthorizationCodeRepository,
|
AuthorizationCodeRepository,
|
||||||
} from 'src/model/auth-code.entity';
|
} from 'src/model/auth-code.entity';
|
||||||
import { Client, ClientRepository } from 'src/model/client.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 { RedirectRepository, RedirectUri } from 'src/model/redirect-uri.entity';
|
||||||
import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity';
|
import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity';
|
||||||
import { User, UserRepository } from 'src/model/user.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 = [
|
const REPOSITORIES = [
|
||||||
UserRepository,
|
UserRepository,
|
||||||
ClientRepository,
|
ClientRepository,
|
||||||
AuthorizationCodeRepository,
|
AuthorizationCodeRepository,
|
||||||
SessionKeyRepository,
|
SessionKeyRepository,
|
||||||
RedirectRepository,
|
RedirectRepository,
|
||||||
|
LogRepository,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Column,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Repository,
|
||||||
|
DataSource,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Log {
|
export class Log {
|
||||||
@@ -13,4 +20,16 @@ export class Log {
|
|||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
|
||||||
|
@Column({ default: 'system' })
|
||||||
|
type: LOGMESSAGETYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogRepository extends Repository<Log> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(Log, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LOGMESSAGETYPE = 'login' | 'system' | 'register' | 'systemlogin';
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class UsersService {
|
|||||||
const uu = await this.userRepo.save(u);
|
const uu = await this.userRepo.save(u);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`User ${userDto.username} created for client ${userDto.clientId}`,
|
`User ${userDto.username} created for client ${userDto.clientId}`,
|
||||||
|
'register',
|
||||||
);
|
);
|
||||||
return uu;
|
return uu;
|
||||||
}
|
}
|
||||||
@@ -118,11 +119,15 @@ export class UsersService {
|
|||||||
if (getUserAccessToken) {
|
if (getUserAccessToken) {
|
||||||
user.accessToken = this.createAccessToken(user);
|
user.accessToken = this.createAccessToken(user);
|
||||||
user.refreshToken = this.createRefreshToken(user);
|
user.refreshToken = this.createRefreshToken(user);
|
||||||
|
this.logger.log(
|
||||||
|
`User logged in with code on client ${clientId}`,
|
||||||
|
'systemlogin',
|
||||||
|
);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await this.createAuthToken(user, client);
|
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;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
idp_client/package-lock.json
generated
17
idp_client/package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@angular/router": "^18.0.0",
|
"@angular/router": "^18.0.0",
|
||||||
"@ngneat/overview": "^6.0.0",
|
"@ngneat/overview": "^6.0.0",
|
||||||
"@ngxpert/hot-toast": "^3.0.0",
|
"@ngxpert/hot-toast": "^3.0.0",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.14.3"
|
"zone.js": "~0.14.3"
|
||||||
@@ -3146,6 +3147,11 @@
|
|||||||
"tslib": "2"
|
"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": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||||
@@ -5150,6 +5156,17 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@angular/router": "^18.0.0",
|
"@angular/router": "^18.0.0",
|
||||||
"@ngneat/overview": "^6.0.0",
|
"@ngneat/overview": "^6.0.0",
|
||||||
"@ngxpert/hot-toast": "^3.0.0",
|
"@ngxpert/hot-toast": "^3.0.0",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.14.3"
|
"zone.js": "~0.14.3"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="header flex-row">
|
<div class="header flex-row">
|
||||||
<span>{{ client.clientName }}x</span>
|
<span>{{ client.clientName }}</span>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<div class="flex-row" style=" gap: 0;"><mat-icon>shield_person</mat-icon><div style="line-height: 8px;">{{ client.admins.length }}</div></div>
|
<div class="flex-row" style=" gap: 0;"><mat-icon>shield_person</mat-icon><div style="line-height: 8px;">{{ client.admins.length }}</div></div>
|
||||||
<div class="flex-row" style=" gap: 0;"><mat-icon>link</mat-icon><div style="line-height: 8px;">{{ client.redirectUris.length }}</div></div>
|
<div class="flex-row" style=" gap: 0;"><mat-icon>link</mat-icon><div style="line-height: 8px;">{{ client.redirectUris.length }}</div></div>
|
||||||
|
|||||||
@@ -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<Login[]>('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 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<canvas id="canvas">{{chart}}</canvas>
|
||||||
|
</div>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,3 +30,5 @@
|
|||||||
} -->
|
} -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-chart-login></app-chart-login>
|
||||||
@@ -10,12 +10,12 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { CreateClientComponent } from './components/create-client/create-client.component';
|
import { CreateClientComponent } from './components/create-client/create-client.component';
|
||||||
import { CreateHotToastRef, HotToastService } from '@ngxpert/hot-toast';
|
import { CreateHotToastRef, HotToastService } from '@ngxpert/hot-toast';
|
||||||
import {MatBottomSheet, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet';
|
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({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ClientCardComponent, MatButtonModule, MatIconModule, MatDialogModule, MatBottomSheetModule],
|
imports: [ClientCardComponent, MatButtonModule, MatIconModule, MatDialogModule, MatBottomSheetModule, LoginChartComponent],
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrl: './dashboard.component.scss'
|
styleUrl: './dashboard.component.scss'
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user