dashboard
This commit is contained in:
@@ -1,11 +1,18 @@
|
|||||||
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Req, UseGuards } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { AuthGuard, Roles, RolesGuard } from 'src/core/secure/guards';
|
import { AuthGuard, Roles, RolesGuard } from 'src/core/secure/guards';
|
||||||
|
import { Client } from 'src/model/client.entity';
|
||||||
|
import { AuthenticatedRequest } from 'src/model';
|
||||||
|
import { CreateClientDto } from 'src/model/dto/create-client.dto';
|
||||||
|
import { ClientService } from 'src/client/client.service';
|
||||||
|
|
||||||
@UseGuards(AuthGuard, RolesGuard)
|
@UseGuards(AuthGuard, RolesGuard)
|
||||||
@Controller('app/user')
|
@Controller('app/user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private userService: UserService) {}
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private clientService: ClientService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getIt() {
|
getIt() {
|
||||||
@@ -14,7 +21,25 @@ export class UserController {
|
|||||||
|
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@Get('clients')
|
@Get('clients')
|
||||||
getClients(@Req() req: any) {
|
getClients(@Req() req: AuthenticatedRequest): Promise<Client[]> {
|
||||||
return this.userService.getUserClients(req['user']);
|
return this.userService.getUserClients(req.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles('admin')
|
||||||
|
@Post('client')
|
||||||
|
createClient(@Req() req: AuthenticatedRequest, @Body() b: CreateClientDto) {
|
||||||
|
return this.clientService.createClient(
|
||||||
|
req.user,
|
||||||
|
b.clientName,
|
||||||
|
b.clientSecret,
|
||||||
|
[],
|
||||||
|
b.description,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles('admin')
|
||||||
|
@Delete('client/:id')
|
||||||
|
deleteClient(@Req() req: AuthenticatedRequest, @Param('id') id) {
|
||||||
|
return this.clientService.deleteClient(req.user, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { UserController } from './user.controller';
|
|||||||
import { SecureModule } from 'src/core/secure/secure.module';
|
import { SecureModule } from 'src/core/secure/secure.module';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { ClientRepository } from 'src/model/client.entity';
|
import { ClientRepository } from 'src/model/client.entity';
|
||||||
|
import { ClientService } from 'src/client/client.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
imports: [SecureModule],
|
imports: [SecureModule],
|
||||||
providers: [UserService, ClientRepository],
|
providers: [UserService, ClientRepository, ClientService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ClientRepository } from 'src/model/client.entity';
|
import { Client, ClientRepository } from 'src/model/client.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) {}
|
||||||
|
|
||||||
getUserClients(user: User) {
|
getUserClients(user: User): Promise<Client[]> {
|
||||||
console.log(user.id);
|
return this.clientRepository.find({
|
||||||
return this.clientRepository.find();
|
where: { admins: { id: user.id } },
|
||||||
|
relations: ['admins'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Client, ClientRepository } from 'src/model/client.entity';
|
import { Client, ClientRepository } from 'src/model/client.entity';
|
||||||
import { RedirectUri } from 'src/model/redirect-uri.entity';
|
import { RedirectUri } from 'src/model/redirect-uri.entity';
|
||||||
|
import { User } from 'src/model/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClientService {
|
export class ClientService {
|
||||||
constructor(private clientRepo: ClientRepository) {}
|
constructor(private clientRepo: ClientRepository) {}
|
||||||
|
|
||||||
async createClient(
|
async createClient(
|
||||||
|
user: User,
|
||||||
clientName: string,
|
clientName: string,
|
||||||
clientSecret: string,
|
clientSecret: string,
|
||||||
redirectUris: string[],
|
redirectUris: string[],
|
||||||
|
description = '',
|
||||||
): Promise<Client> {
|
): Promise<Client> {
|
||||||
const clientDto: Client = {
|
const clientDto: Client = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
@@ -18,8 +21,10 @@ export class ClientService {
|
|||||||
clientSecret,
|
clientSecret,
|
||||||
redirectUris: [],
|
redirectUris: [],
|
||||||
authorizationCodes: [],
|
authorizationCodes: [],
|
||||||
|
description,
|
||||||
};
|
};
|
||||||
const c = this.clientRepo.create(clientDto);
|
const c = this.clientRepo.create(clientDto);
|
||||||
|
c.admins = [user];
|
||||||
const client = await this.clientRepo.save(c);
|
const client = await this.clientRepo.save(c);
|
||||||
|
|
||||||
redirectUris.forEach(async (uri) => {
|
redirectUris.forEach(async (uri) => {
|
||||||
@@ -59,4 +64,16 @@ export class ClientService {
|
|||||||
getClientById(clientId: string): Promise<Client> {
|
getClientById(clientId: string): Promise<Client> {
|
||||||
return this.clientRepo.findById(clientId);
|
return this.clientRepo.findById(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteClient(user: User, id: string) {
|
||||||
|
const client = await this.clientRepo.findOne({
|
||||||
|
where: { admins: { id: user.id }, id: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
return this.clientRepo.remove(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpException('client not found', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import {
|
|||||||
DataSource,
|
DataSource,
|
||||||
Repository,
|
Repository,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
|
ManyToMany,
|
||||||
|
JoinTable,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { RedirectUri } from './redirect-uri.entity';
|
import { RedirectUri } from './redirect-uri.entity';
|
||||||
import { AuthorizationCode } from './auth-code.entity';
|
import { AuthorizationCode } from './auth-code.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Client {
|
export class Client {
|
||||||
@@ -23,6 +26,9 @@ export class Client {
|
|||||||
@Column()
|
@Column()
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
@OneToMany(() => RedirectUri, (uri) => uri.client, {
|
@OneToMany(() => RedirectUri, (uri) => uri.client, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -31,6 +37,10 @@ export class Client {
|
|||||||
|
|
||||||
@OneToMany(() => AuthorizationCode, (code) => code.client)
|
@OneToMany(() => AuthorizationCode, (code) => code.client)
|
||||||
authorizationCodes: AuthorizationCode[];
|
authorizationCodes: AuthorizationCode[];
|
||||||
|
|
||||||
|
@ManyToMany(() => User, (user) => user.clients, { onDelete: 'CASCADE' })
|
||||||
|
@JoinTable()
|
||||||
|
admins?: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
5
idp/src/model/dto/create-client.dto.ts
Normal file
5
idp/src/model/dto/create-client.dto.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CreateClientDto {
|
||||||
|
clientName: string;
|
||||||
|
clientSecret: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
1
idp/src/model/index.ts
Normal file
1
idp/src/model/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './interface';
|
||||||
5
idp/src/model/interface/authenticated.request.ts
Normal file
5
idp/src/model/interface/authenticated.request.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { User } from '../user.entity';
|
||||||
|
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
1
idp/src/model/interface/index.ts
Normal file
1
idp/src/model/interface/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './authenticated.request';
|
||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
OneToMany,
|
OneToMany,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
ManyToMany,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { AuthorizationCode } from './auth-code.entity';
|
import { AuthorizationCode } from './auth-code.entity';
|
||||||
import { SessionKey } from './session-key.entity';
|
import { SessionKey } from './session-key.entity';
|
||||||
import { Role } from './role.entity';
|
import { Role } from './role.entity';
|
||||||
|
import { Client } from './client.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
@@ -51,6 +53,10 @@ export class User {
|
|||||||
@ManyToOne(() => Role, (role) => role.users)
|
@ManyToOne(() => Role, (role) => role.users)
|
||||||
role?: Role;
|
role?: Role;
|
||||||
|
|
||||||
|
@Exclude()
|
||||||
|
@ManyToMany(() => Client, (client) => client.admins)
|
||||||
|
clients?: Client[];
|
||||||
|
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
session_key?: string;
|
session_key?: string;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export class UsersService {
|
|||||||
private sessionRepo: SessionKeyRepository,
|
private sessionRepo: SessionKeyRepository,
|
||||||
private logger: CustomLogger,
|
private logger: CustomLogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUser(userDto: CreateUserDto): Promise<User> {
|
async createUser(userDto: CreateUserDto): Promise<User> {
|
||||||
const hashedPassword = await bcrypt.hash(userDto.password, 10);
|
const hashedPassword = await bcrypt.hash(userDto.password, 10);
|
||||||
const user: User = {
|
const user: User = {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
"src/styles.scss",
|
"src/styles.scss",
|
||||||
"node_modules/@ngxpert/hot-toast/src/styles/styles.css"
|
"node_modules/@ngxpert/hot-toast/src/styles/styles.css"
|
||||||
],
|
],
|
||||||
|
|||||||
39
idp_client/package-lock.json
generated
39
idp_client/package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"@angular/compiler": "^18.0.0",
|
"@angular/compiler": "^18.0.0",
|
||||||
"@angular/core": "^18.0.0",
|
"@angular/core": "^18.0.0",
|
||||||
"@angular/forms": "^18.0.0",
|
"@angular/forms": "^18.0.0",
|
||||||
|
"@angular/material": "^18.2.3",
|
||||||
"@angular/platform-browser": "^18.0.0",
|
"@angular/platform-browser": "^18.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||||
"@angular/router": "^18.0.0",
|
"@angular/router": "^18.0.0",
|
||||||
@@ -338,6 +339,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cdk": {
|
||||||
|
"version": "18.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.3.tgz",
|
||||||
|
"integrity": "sha512-lUcpYTxPZuntJ1FK7V2ugapCGMIhT6TUDjIGgXfS9AxGSSKgwr8HNs6Ze9pcjYC44UhP40sYAZuiaFwmE60A2A==",
|
||||||
|
"peer": true,
|
||||||
|
"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": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "18.2.1",
|
"version": "18.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz",
|
||||||
@@ -465,6 +483,23 @@
|
|||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/material": {
|
||||||
|
"version": "18.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.3.tgz",
|
||||||
|
"integrity": "sha512-JFfvXaMHMhskncaxxus4sDvie9VYdMkfYgfinkLXpZlPFyn1IzjDw0c1BcrcsuD7UxQVZ/v5tucCgq1FQfGRpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/animations": "^18.0.0 || ^19.0.0",
|
||||||
|
"@angular/cdk": "18.2.3",
|
||||||
|
"@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": {
|
"node_modules/@angular/platform-browser": {
|
||||||
"version": "18.2.1",
|
"version": "18.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz",
|
||||||
@@ -6165,7 +6200,7 @@
|
|||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
},
|
},
|
||||||
@@ -9998,7 +10033,7 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^4.4.0"
|
"entities": "^4.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@angular/compiler": "^18.0.0",
|
"@angular/compiler": "^18.0.0",
|
||||||
"@angular/core": "^18.0.0",
|
"@angular/core": "^18.0.0",
|
||||||
"@angular/forms": "^18.0.0",
|
"@angular/forms": "^18.0.0",
|
||||||
|
"@angular/material": "^18.2.3",
|
||||||
"@angular/platform-browser": "^18.0.0",
|
"@angular/platform-browser": "^18.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||||
"@angular/router": "^18.0.0",
|
"@angular/router": "^18.0.0",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { routes } from './app.routes';
|
|||||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { provideHotToastConfig } from '@ngxpert/hot-toast';
|
import { provideHotToastConfig } from '@ngxpert/hot-toast';
|
||||||
import { authInterceptor } from './core/interceptor/auth.interceptor';
|
import { authInterceptor } from './core/interceptor/auth.interceptor';
|
||||||
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), provideHotToastConfig(),]
|
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), provideHotToastConfig(), provideAnimations()]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="header">{{ client.clientName }}</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ client.description }}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button mat-button >Admins</button>
|
||||||
|
<button mat-button >Redirect URIs</button>
|
||||||
|
<button mat-flat-button color="warn" (click)="deleteClient()">Löschen</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, .2), 0px 2px 2px 0px rgba(0, 0, 0, .14), 0px 1px 5px 0px rgba(0, 0, 0, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CardComponent } from './card.component';
|
||||||
|
|
||||||
|
describe('CardComponent', () => {
|
||||||
|
let component: CardComponent;
|
||||||
|
let fixture: ComponentFixture<CardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CardComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { Client } from '../../../model/client.interface';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule],
|
||||||
|
templateUrl: './card.component.html',
|
||||||
|
styleUrl: './card.component.scss'
|
||||||
|
})
|
||||||
|
export class CardComponent {
|
||||||
|
|
||||||
|
@Input() client: Client;
|
||||||
|
@Output() onDelete = new EventEmitter<Client>();
|
||||||
|
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private toast = inject(HotToastService);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deleteClient() {
|
||||||
|
this.onDelete.emit(this.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<h2 mat-dialog-title>Neue Clientanwendung erstellen</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<mat-stepper linear orientation="horizontal" #stepper>
|
||||||
|
<mat-step [stepControl]="createClient" [editable]="true">
|
||||||
|
|
||||||
|
<form [formGroup]="createClient" >
|
||||||
|
<ng-template matStepLabel>Basic Data</ng-template>
|
||||||
|
<mat-form-field appearance="fill" >
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input matInput placeholder="Name der Anwendung" formControlName="clientName">
|
||||||
|
<mat-icon matSuffix>badge</mat-icon>
|
||||||
|
<mat-hint>Name der Anwendung</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Client Secret</mat-label>
|
||||||
|
<input matInput placeholder="Placeholder" formControlName="clientSecret">
|
||||||
|
<mat-icon matSuffix>key</mat-icon>
|
||||||
|
<mat-hint>Das kann danach nicht mehr angeschaut werden</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Beschreibung</mat-label>
|
||||||
|
<textarea matInput placeholder="Placeholder" formControlName="description" rows="4"></textarea>
|
||||||
|
<mat-icon matSuffix>description</mat-icon>
|
||||||
|
<mat-hint>Beschreibung der Anwendung</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button mat-raised-button (click)="create()" [disabled]="createClient.invalid || isSaving">
|
||||||
|
<div class="flex-row">
|
||||||
|
<mat-spinner [diameter]="16" *ngIf="isSaving"></mat-spinner>
|
||||||
|
<div>Next</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</mat-step>
|
||||||
|
<mat-step [editable]="true">
|
||||||
|
<ng-template matStepLabel>Admins und Redirect URLs</ng-template>
|
||||||
|
<div style="flex: 1 1 auto">space</div>
|
||||||
|
<div> {{ client?.clientName }}</div>
|
||||||
|
</mat-step>
|
||||||
|
</mat-stepper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-dialog-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CreateClientComponent } from './create-client.component';
|
||||||
|
|
||||||
|
describe('CreateClientComponent', () => {
|
||||||
|
let component: CreateClientComponent;
|
||||||
|
let fixture: ComponentFixture<CreateClientComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CreateClientComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreateClientComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import {MatStepper, MatStepperModule} from '@angular/material/stepper';
|
||||||
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
import { Client } from '../../../model/client.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-client',
|
||||||
|
standalone: true,
|
||||||
|
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatIconModule, MatDialogModule, MatButtonModule, MatProgressSpinnerModule, CommonModule, MatStepperModule],
|
||||||
|
templateUrl: './create-client.component.html',
|
||||||
|
styleUrl: './create-client.component.scss'
|
||||||
|
})
|
||||||
|
export class CreateClientComponent {
|
||||||
|
|
||||||
|
isSaving = false;
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private toast = inject(HotToastService);
|
||||||
|
|
||||||
|
@ViewChild('stepper') stepper: MatStepper;
|
||||||
|
|
||||||
|
createClient = new FormGroup({
|
||||||
|
clientName: new FormControl(null, [Validators.required, Validators.minLength(4), Validators.maxLength(200)]),
|
||||||
|
clientSecret: new FormControl(null, [Validators.required, Validators.minLength(4), Validators.maxLength(200)]),
|
||||||
|
description: new FormControl(null, [Validators.required, Validators.minLength(4), Validators.maxLength(500)])
|
||||||
|
})
|
||||||
|
|
||||||
|
client: Client;
|
||||||
|
|
||||||
|
|
||||||
|
create() {
|
||||||
|
if (this.createClient.invalid) { return; }
|
||||||
|
|
||||||
|
|
||||||
|
this.createClient.disable()
|
||||||
|
this.isSaving = true;
|
||||||
|
|
||||||
|
this.http.post<Client>('api/app/user/client', this.createClient.value).pipe(
|
||||||
|
this.toast.observe({
|
||||||
|
loading: 'Client erstellen',
|
||||||
|
error: 'Client konnte nicht erstellt werden',
|
||||||
|
success: 'Client erstellt'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: data => {
|
||||||
|
this.client = data;
|
||||||
|
this.createClient.enable();
|
||||||
|
this.stepper.next();
|
||||||
|
this.isSaving = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,29 @@
|
|||||||
<p>dashboard works!</p>
|
<div class="header">
|
||||||
|
<div class="title">SSO Beantastic</div>
|
||||||
|
<div> {{ userName }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-container">
|
||||||
|
<div class="flex-row">
|
||||||
|
<button mat-fab id="createClient" (click)="createClient()" [disabled]="clients.length == 10" >
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label for="createClient">
|
||||||
|
Client erstellen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row" style="gap: 4px;">
|
||||||
|
<div class="chip"><span class="counter">{{ clients.length }}</span> von 10</div>
|
||||||
|
<div>Clients erstellt</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-container__list">
|
||||||
|
@for (client of clients; track $index) {
|
||||||
|
<app-card [client]="client" (onDelete)="openDeleteDialog($event)" ></app-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-container {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 12px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.card-container__list{
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-container{
|
||||||
|
border: 1px dotted #ccc;
|
||||||
|
width: 500px;
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter{
|
||||||
|
color:rgb(252, 239, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100vw;
|
||||||
|
padding: 12px 24px;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,19 @@ import { Component, inject, OnInit } from '@angular/core';
|
|||||||
import { UserService } from '../auth/user.service';
|
import { UserService } from '../auth/user.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { CardComponent } from './components/card/card.component';
|
||||||
|
import { Client } from '../model/client.interface';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [CardComponent, MatButtonModule, MatIconModule, MatDialogModule, MatBottomSheetModule],
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrl: './dashboard.component.scss'
|
styleUrl: './dashboard.component.scss'
|
||||||
})
|
})
|
||||||
@@ -15,12 +23,15 @@ export class DashboardComponent implements OnInit {
|
|||||||
private userService: UserService = inject(UserService);
|
private userService: UserService = inject(UserService);
|
||||||
private router: Router = inject(Router);
|
private router: Router = inject(Router);
|
||||||
private http: HttpClient = inject(HttpClient);
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
private dialog: MatDialog = inject(MatDialog);
|
||||||
|
private toast: HotToastService = inject(HotToastService);
|
||||||
|
private bottomSheet = inject(MatBottomSheet);
|
||||||
|
|
||||||
|
clients: Client[] = [];
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
console.log("ONINIT")
|
|
||||||
if (!this.userService.user) {
|
if (!this.userService.user) {
|
||||||
console.log("REDIRECT")
|
|
||||||
this.router.navigateByUrl("/login");
|
this.router.navigateByUrl("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -29,11 +40,93 @@ export class DashboardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
this.http.get('api/app/user/clients').subscribe(res => {
|
this.http.get<Client[]>('api/app/user/clients').subscribe(res => {
|
||||||
console.log(res)
|
this.clients = res;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userName(): string {
|
||||||
|
return this.userService.user?.firstName + ' ' + this.userService.user?.lastName
|
||||||
|
}
|
||||||
|
|
||||||
|
createClient() {
|
||||||
|
this.dialog.open(CreateClientComponent, {
|
||||||
|
panelClass: 'create-client__dialog'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteDialog(client: Client) {
|
||||||
|
const toast = this.toast.loading(`Lösche Client ${client.clientName} ...`, { autoClose: false});
|
||||||
|
this.bottomSheet.open(DeleteClientConfirmation).afterDismissed()
|
||||||
|
.subscribe({
|
||||||
|
next: data => {
|
||||||
|
if (data) {
|
||||||
|
this.deleteClient(client, toast)
|
||||||
|
} else {
|
||||||
|
toast.updateMessage('Löschen abgebrochen');
|
||||||
|
toast.updateToast({
|
||||||
|
type: 'error',
|
||||||
|
dismissible: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteClient(client: Client, toast: CreateHotToastRef<unknown>) {
|
||||||
|
|
||||||
|
this.http.delete('api/app/user/client/' + client.id)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
|
||||||
|
toast.updateMessage(`Client ${client.clientName} gelöscht`);
|
||||||
|
toast.updateToast({
|
||||||
|
type: 'success',
|
||||||
|
dismissible: true,
|
||||||
|
|
||||||
|
});
|
||||||
|
this.load();
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.close();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
error: () => {
|
||||||
|
toast.updateMessage(`Client ${client.clientName} konnte nicht gelöscht werden`);
|
||||||
|
toast.updateToast({
|
||||||
|
type: 'error',
|
||||||
|
dismissible: true,
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'bottom-sheet-overview-example-sheet',
|
||||||
|
template: `
|
||||||
|
<h3>Soll der Client wirklich gelöscht werden?</h3>
|
||||||
|
<h5>Danach kann sich niemand mehr einloggen und alle Daten werden gelöscht.</h5>
|
||||||
|
<div class="flex-row" style="justify-content: space-between">
|
||||||
|
<button mat-flat-button color="warn" (click)="delete()">Löschen</button>
|
||||||
|
<button mat-flat-button (click)="cancel()">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule],
|
||||||
|
})
|
||||||
|
export class DeleteClientConfirmation {
|
||||||
|
private _bottomSheetRef =
|
||||||
|
inject<MatBottomSheetRef<DeleteClientConfirmation>>(MatBottomSheetRef);
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
this._bottomSheetRef.dismiss(true)
|
||||||
|
}
|
||||||
|
cancel() {
|
||||||
|
this._bottomSheetRef.dismiss(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
idp_client/src/app/model/client.interface.ts
Normal file
10
idp_client/src/app/model/client.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { RedirectUri } from "./redirect-uri.interface";
|
||||||
|
import { User } from "./user.interface";
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
admins: User;
|
||||||
|
clientName: string;
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
redirectUris: RedirectUri[];
|
||||||
|
}
|
||||||
4
idp_client/src/app/model/redirect-uri.interface.ts
Normal file
4
idp_client/src/app/model/redirect-uri.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface RedirectUri {
|
||||||
|
id?: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<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="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ html, body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: Raleway, sans-serif;
|
font-family: Raleway, sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.safe {
|
.safe {
|
||||||
@@ -13,4 +14,66 @@ html, body {
|
|||||||
|
|
||||||
.user {
|
.user {
|
||||||
background-image: url("assets/icons/user.svg");
|
background-image: url("assets/icons/user.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-client__dialog{
|
||||||
|
width: 70vw;
|
||||||
|
min-width: 500px;
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
|
||||||
|
height: 70vh;
|
||||||
|
min-height: 600px;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-spinner{
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(80, 80, 80, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-horizontal-content-container{
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-horizontal-stepper-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-dialog-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-dialog-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-stepper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-horizontal-stepper-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-horizontal-stepper-content-inactive {
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user