diff --git a/api/.env.skeleton b/api/.env.skeleton index c8179a1..736c059 100644 --- a/api/.env.skeleton +++ b/api/.env.skeleton @@ -6,7 +6,14 @@ DATABASE_HOST=localhost DATABASE_PORT=3306 MYSQL_ROOT_PASSWORD=kjsdahflöijsdiu -# SECURITY +# SSO +SSO_TOKEN_URL=https://sso.beantastic.de/api/authorize +SSO_VERIFY_URL=https://sso.beantastic.de/api/verify +SSO_REDIRECT_URI= +SSO_CLIENT_SECRET= +SSO_CLIENT_ID= + + # SECURITY JWT_SECRET= JWT_EXPIRES_IN=10m \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index cadbe00..dea2ba3 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -25,6 +25,7 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", @@ -903,6 +904,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/api/package.json b/api/package.json index a4b6b6e..b8c1f65 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,7 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", diff --git a/api/src/app.controller.ts b/api/src/app.controller.ts index babafcb..c7ea3b1 100644 --- a/api/src/app.controller.ts +++ b/api/src/app.controller.ts @@ -1,18 +1,22 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpException, HttpStatus, Post, UseGuards } from '@nestjs/common'; import { AppService } from './app.service'; import { LoginDTO } from './model/dto/login.dto'; +import { AuthGuard } from './core/guards/auth.guard'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} + @UseGuards(AuthGuard) @Get() getHello(): any { + return this.appService.getHello(); } @Post() login(@Body() createUserDto: LoginDTO) { + return { success: createUserDto }; } } diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 6d0fa76..5d1579e 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -4,6 +4,9 @@ import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from './shared/database/database.module'; import { AuthModule } from './modules/auth/auth.module'; +import { AuthGuard } from './core/guards/auth.guard'; +import { UserModule } from './modules/user/user.module'; +import { RoleModule } from './modules/role/role.module'; @Module({ imports: [ @@ -13,8 +16,10 @@ import { AuthModule } from './modules/auth/auth.module'; }), DatabaseModule, AuthModule, + UserModule, + RoleModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, AuthGuard], }) export class AppModule {} diff --git a/api/src/core/guards/auth.guard.ts b/api/src/core/guards/auth.guard.ts new file mode 100644 index 0000000..66c2a05 --- /dev/null +++ b/api/src/core/guards/auth.guard.ts @@ -0,0 +1,61 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JsonWebTokenError, JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; +import { IPayload } from 'src/model/interface'; +import { AuthService } from 'src/modules/auth/auth.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private config: ConfigService, + private authService: AuthService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('Token not provided'); + } + + try { + const secret = this.config.get('JWT_SECRET'); + // Überprüft das JWT und dekodiert es + const payload: IPayload = this.jwtService.verify(token, { secret }); + if (payload.type != 'access') { + throw new UnauthorizedException('wrong token'); + } + const user = await this.authService.getUserById(payload.id); + if (!user.isActive) { + throw new HttpException('not active', HttpStatus.FORBIDDEN); + } + request['user'] = user; + } catch (error) { + const j = error as JsonWebTokenError; + const m = j.message; + + throw new UnauthorizedException(m); + } + + return true; + } + + private extractTokenFromHeader(request: Request): string | null { + const authHeader = request.headers['authorization']; + if (!authHeader) { + return null; + } + const [type, token] = authHeader.split(' '); + return type === 'Bearer' && token ? token : null; + } +} diff --git a/api/src/model/decorators/admin.decorator.ts b/api/src/model/decorators/admin.decorator.ts new file mode 100644 index 0000000..4e7b9ae --- /dev/null +++ b/api/src/model/decorators/admin.decorator.ts @@ -0,0 +1,5 @@ +import { Expose } from 'class-transformer'; + +export function AdminGroup() { + return Expose({ groups: ['admin'] }); // Setzt die Gruppe 'admin' automatisch +} diff --git a/api/src/model/entitites/index.ts b/api/src/model/entitites/index.ts index 5965ca4..6ed37e1 100644 --- a/api/src/model/entitites/index.ts +++ b/api/src/model/entitites/index.ts @@ -1,2 +1,3 @@ export * from './sso.user.entity'; export * from './user.entity'; +export * from './role.entity'; diff --git a/api/src/model/entitites/role.entity.ts b/api/src/model/entitites/role.entity.ts new file mode 100644 index 0000000..ffbbfdd --- /dev/null +++ b/api/src/model/entitites/role.entity.ts @@ -0,0 +1,19 @@ +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +@Entity() +export class Role { + @PrimaryGeneratedColumn('uuid') + id: string; + + @OneToMany(() => User, (user) => user.role) + user: User[]; + + @Column({ nullable: true }) + name: string; +} diff --git a/api/src/model/entitites/user.entity.ts b/api/src/model/entitites/user.entity.ts index 81a7cfe..1fb532d 100644 --- a/api/src/model/entitites/user.entity.ts +++ b/api/src/model/entitites/user.entity.ts @@ -1,14 +1,17 @@ -import { Exclude } from 'class-transformer'; +import { Exclude, Transform } from 'class-transformer'; import { Column, CreateDateColumn, Entity, + JoinColumn, + ManyToOne, OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; import { IUser } from '../interface'; import { SSOUser } from './sso.user.entity'; import { IsEmail } from 'class-validator'; +import { Role } from './role.entity'; @Entity() export class User implements IUser { @@ -35,10 +38,14 @@ export class User implements IUser { @OneToOne(() => SSOUser, (sso) => sso.user, { eager: true, cascade: true }) external: SSOUser; - @Exclude() @Column({ default: true }) isActive: boolean; + @ManyToOne(() => Role, (role) => role.user, { cascade: true }) + @JoinColumn() + @Transform(({ value }) => value.name) + role: Role; + accessToken?: string; refreshToken?: string; } diff --git a/api/src/model/interface/user.interface.ts b/api/src/model/interface/user.interface.ts index cdde029..64448d7 100644 --- a/api/src/model/interface/user.interface.ts +++ b/api/src/model/interface/user.interface.ts @@ -1,3 +1,5 @@ +import { Role } from '../entitites'; + export interface IUser { id: string; username: string; @@ -7,4 +9,6 @@ export interface IUser { accessToken?: string; refreshToken?: string; + + role?: string | Role; } diff --git a/api/src/model/repositories/index.ts b/api/src/model/repositories/index.ts index f9904cb..ed41468 100644 --- a/api/src/model/repositories/index.ts +++ b/api/src/model/repositories/index.ts @@ -1,2 +1,3 @@ export * from './user.repository'; export * from './ssouser.repository'; +export * from './role.repository'; diff --git a/api/src/model/repositories/role.repository.ts b/api/src/model/repositories/role.repository.ts new file mode 100644 index 0000000..cc9b9d4 --- /dev/null +++ b/api/src/model/repositories/role.repository.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { Repository, DataSource } from 'typeorm'; +import { Role } from '../entitites'; + +@Injectable() +export class RoleRepository extends Repository { + constructor(dataSource: DataSource) { + super(Role, dataSource.createEntityManager()); + } + + getStandardRole(): Promise { + return this.findOne({ where: { name: 'develop' } }); + } +} diff --git a/api/src/model/repositories/user.repository.ts b/api/src/model/repositories/user.repository.ts index df194b6..5023142 100644 --- a/api/src/model/repositories/user.repository.ts +++ b/api/src/model/repositories/user.repository.ts @@ -3,12 +3,14 @@ import { Repository, DataSource } from 'typeorm'; import { User } from '../entitites'; import { CreateUserDto } from '../dto/create-user.dto'; import { SsoUserRepository } from './ssouser.repository'; +import { RoleRepository } from './role.repository'; @Injectable() export class UserRepository extends Repository { constructor( dataSource: DataSource, private ssoRepo: SsoUserRepository, + private roleRepo: RoleRepository, ) { super(User, dataSource.createEntityManager()); } @@ -38,6 +40,7 @@ export class UserRepository extends Repository { externalId: createUserDto.externalId, }); created.external = sso; + created.role = await this.roleRepo.getStandardRole(); const user = await this.save(created); sso.user = user; this.ssoRepo.save(sso); diff --git a/api/src/modules/auth/auth.controller.ts b/api/src/modules/auth/auth.controller.ts index 8c4cdba..d96c896 100644 --- a/api/src/modules/auth/auth.controller.ts +++ b/api/src/modules/auth/auth.controller.ts @@ -1,13 +1,17 @@ import { Body, Controller, + Get, HttpException, HttpStatus, Post, + Req, + UseGuards, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthCodeDto } from 'src/model/dto'; import { User } from 'src/model/entitites'; +import { AuthGuard } from 'src/core/guards/auth.guard'; @Controller('auth') export class AuthController { @@ -23,4 +27,19 @@ export class AuthController { } return user; } + + @UseGuards(AuthGuard) + @Get('me') + getMe(@Req() req: any) { + return req.user; + } + + @Post('refresh') + async getNewAccessToken(@Body() b: any) { + if (b.refreshToken) { + return this.authService.getNewToken(b.refreshToken); + } + + throw new HttpException('no token', HttpStatus.BAD_REQUEST); + } } diff --git a/api/src/modules/auth/auth.module.ts b/api/src/modules/auth/auth.module.ts index 9f6412f..3cbdca8 100644 --- a/api/src/modules/auth/auth.module.ts +++ b/api/src/modules/auth/auth.module.ts @@ -4,7 +4,7 @@ import { AuthService } from './auth.service'; import { DatabaseModule } from 'src/shared/database/database.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HttpModule } from '@nestjs/axios'; -import { JwtModule } from '@nestjs/jwt'; +import { JwtModule, JwtService } from '@nestjs/jwt'; @Module({ controllers: [AuthController], @@ -22,5 +22,6 @@ import { JwtModule } from '@nestjs/jwt'; }), }), ], + exports: [JwtModule, AuthService], }) export class AuthModule {} diff --git a/api/src/modules/auth/auth.service.ts b/api/src/modules/auth/auth.service.ts index 154689a..957cf2e 100644 --- a/api/src/modules/auth/auth.service.ts +++ b/api/src/modules/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { AuthCodeDto } from 'src/model/dto'; import { CreateUserDto } from 'src/model/dto/create-user.dto'; import { UserRepository } from 'src/model/repositories'; @@ -21,7 +21,6 @@ export class AuthService { } async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise { - console.log(auth); const body = this.createAuthCodeFormData(auth.code, 'authorization_code'); const url = this.configService.get('SSO_TOKEN_URL'); return new Promise((resolve) => { @@ -45,7 +44,6 @@ export class AuthService { access_token: string; refresh_token: string; }): Promise { - console.log(access_token, refresh_token); const payload: IExternalAccessPayload = this.jwt.decode(access_token); return new Promise(async (resolve) => { let user = await this.userRepo.findByUsername(payload.username); @@ -56,6 +54,9 @@ export class AuthService { externalId: payload.id, }); } + if (!user.isActive) { + throw new HttpException('not active', HttpStatus.FORBIDDEN); + } user.firstName = payload.firstName; user.lastName = payload.lastName; @@ -82,7 +83,7 @@ export class AuthService { type: 'refresh', }; - user.refreshToken = this.jwt.sign(rPay); + user.refreshToken = this.jwt.sign(rPay, { expiresIn: '1w' }); } private createAuthCodeFormData( @@ -103,4 +104,76 @@ export class AuthService { ); return bodyFormData; } + + getUserById(id: string): Promise { + return this.userRepo.findById(id); + } + + async getNewToken(refresh: string) { + try { + const payload: IPayload = this.jwt.verify(refresh); + const user = await this.getUserById(payload.id); + if (!user) { + throw new HttpException('not valid', HttpStatus.UNAUTHORIZED); + } + const s = await this.verifyExternal(user); + if (!s) { + throw new HttpException('not valid', HttpStatus.UNAUTHORIZED); + } + this.generateTokens(user); + return user; + } catch (e) { + throw new HttpException('invalid token', HttpStatus.BAD_REQUEST); + } + } + + verifyExternal(user: User) { + if (!user || !user.external) { + return false; + } + + const url = this.configService.get('SSO_VERIFY_URL'); + return new Promise((resolve) => { + this.http + .post(url, { access_token: user.external.accessToken }) + .subscribe({ + next: (response) => { + const id = response.data.id; + if (id == user.external.externalId) { + resolve(true); + } else { + resolve(false); + } + }, + error: async (error) => { + const data = error.response.data; + if (data.message == 'jwt expired') { + const s = await resolve(this.refreshExternalAccessToken(user)); + return resolve(s); + } + resolve(false); + }, + }); + }); + } + + async refreshExternalAccessToken(user: User): Promise { + const bodyFormData = this.createAuthCodeFormData( + user.external.refreshToken, + 'refresh_token', + ); + const url = this.configService.get('SSO_TOKEN_URL'); + return new Promise((resolve) => { + this.http.post(url, bodyFormData).subscribe({ + next: async (response) => { + user.external.accessToken = response.data.access_token; + await this.userRepo.save(user); + resolve(true); + }, + error: () => { + resolve(false); + }, + }); + }); + } } diff --git a/api/src/modules/role/role.controller.spec.ts b/api/src/modules/role/role.controller.spec.ts new file mode 100644 index 0000000..4b8b85f --- /dev/null +++ b/api/src/modules/role/role.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoleController } from './role.controller'; + +describe('RoleController', () => { + let controller: RoleController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RoleController], + }).compile(); + + controller = module.get(RoleController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/api/src/modules/role/role.controller.ts b/api/src/modules/role/role.controller.ts new file mode 100644 index 0000000..94d79a4 --- /dev/null +++ b/api/src/modules/role/role.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { RoleService } from './role.service'; +import { AuthGuard } from 'src/core/guards/auth.guard'; + +@UseGuards(AuthGuard) +@Controller('role') +export class RoleController { + + constructor(private readonly rolesService: RoleService) {} + + @Get() + getRoles() { + return this.rolesService.getRoles(); + } +} diff --git a/api/src/modules/role/role.module.ts b/api/src/modules/role/role.module.ts new file mode 100644 index 0000000..a88c461 --- /dev/null +++ b/api/src/modules/role/role.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { RoleController } from './role.controller'; +import { RoleService } from './role.service'; +import { DatabaseModule } from 'src/shared/database/database.module'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + controllers: [RoleController], + providers: [RoleService], + imports: [ DatabaseModule, AuthModule ] +}) +export class RoleModule {} diff --git a/api/src/modules/role/role.service.spec.ts b/api/src/modules/role/role.service.spec.ts new file mode 100644 index 0000000..e1e0c00 --- /dev/null +++ b/api/src/modules/role/role.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoleService } from './role.service'; + +describe('RoleService', () => { + let service: RoleService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RoleService], + }).compile(); + + service = module.get(RoleService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api/src/modules/role/role.service.ts b/api/src/modules/role/role.service.ts new file mode 100644 index 0000000..4829cf6 --- /dev/null +++ b/api/src/modules/role/role.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { RoleRepository } from 'src/model/repositories'; + +@Injectable() +export class RoleService { + constructor(private readonly rolesRepo: RoleRepository) {} + + getRoles() { + return this.rolesRepo.find(); + } +} diff --git a/api/src/modules/user/user.controller.ts b/api/src/modules/user/user.controller.ts new file mode 100644 index 0000000..3273737 --- /dev/null +++ b/api/src/modules/user/user.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { AuthGuard } from 'src/core/guards/auth.guard'; +import { UserService } from './user.service'; +import { User } from 'src/model/entitites'; +import { IUser } from 'src/model/interface'; + +@UseGuards(AuthGuard) +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + + @Get() + getUsers(): Promise { + return this.userService.getAllUsers(); + } + + @Post() + saveUser(@Body() user: IUser) { + return this.userService.saveUser(user); + } +} diff --git a/api/src/modules/user/user.module.ts b/api/src/modules/user/user.module.ts new file mode 100644 index 0000000..2b60631 --- /dev/null +++ b/api/src/modules/user/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; +import { AuthModule } from '../auth/auth.module'; +import { DatabaseModule } from 'src/shared/database/database.module'; + +@Module({ + controllers: [UserController], + providers: [UserService], + imports: [AuthModule, DatabaseModule], +}) +export class UserModule {} diff --git a/api/src/modules/user/user.service.ts b/api/src/modules/user/user.service.ts new file mode 100644 index 0000000..7bb960e --- /dev/null +++ b/api/src/modules/user/user.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { User } from 'src/model/entitites'; +import { IUser } from 'src/model/interface'; +import { RoleRepository, UserRepository } from 'src/model/repositories'; + +@Injectable() +export class UserService { + constructor( + private readonly userRepo: UserRepository, + private readonly roleRepo: RoleRepository, + ) {} + + getAllUsers(): Promise { + return this.userRepo.find({ + relations: ['role'], + where: [{ role: { name: 'user' } }, { role: { name: 'admin' } }], + }); + } + + async saveUser(user: IUser) { + if (typeof user.role == 'string') { + user.role = await this.roleRepo.findOneBy({ name: user.role }); + } + return this.userRepo.save(user as any); + } +} diff --git a/api/src/shared/database/database.module.ts b/api/src/shared/database/database.module.ts index 94d22ab..dbcafd7 100644 --- a/api/src/shared/database/database.module.ts +++ b/api/src/shared/database/database.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SSOUser, User } from 'src/model/entitites'; -import { SsoUserRepository, UserRepository } from 'src/model/repositories'; +import { Role, SSOUser, User } from 'src/model/entitites'; +import { RoleRepository, SsoUserRepository, UserRepository } from 'src/model/repositories'; -const ENTITIES = [User, SSOUser]; -const REPOSITORIES = [UserRepository, SsoUserRepository]; +const ENTITIES = [User, SSOUser, Role]; +const REPOSITORIES = [UserRepository, SsoUserRepository, RoleRepository]; @Module({ imports: [ diff --git a/client/angular.json b/client/angular.json index eb0b0d3..4617cdd 100644 --- a/client/angular.json +++ b/client/angular.json @@ -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": [] diff --git a/client/package-lock.json b/client/package-lock.json index 2e045b5..655adf7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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" }, diff --git a/client/package.json b/client/package.json index b0042ad..3a388ed 100644 --- a/client/package.json +++ b/client/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/client/public/favicon.ico b/client/public/favicon.ico deleted file mode 100644 index 57614f9..0000000 Binary files a/client/public/favicon.ico and /dev/null differ diff --git a/client/public/key.svg b/client/public/key.svg new file mode 100644 index 0000000..5e1ccd5 --- /dev/null +++ b/client/public/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 1afb118..294ce91 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -1,10 +1,16 @@ + + + + + + + + - + \ No newline at end of file diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 3bf59b4..cc0c5fd 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -1,5 +1,8 @@ :host { - display: flex; height: 100vh; width: 100vw; -} \ No newline at end of file + overflow: hidden; + display: flex; + flex-direction: column; +} + diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index c3803e2..5c30dae 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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 { + } + + + + + } diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index 94f5f50..ba30d7e 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -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()] }; diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index dc39edb..be19801 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -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}, +]; diff --git a/client/src/app/core/auth/auth.guard.ts b/client/src/app/core/auth/auth.guard.ts new file mode 100644 index 0000000..0cdb793 --- /dev/null +++ b/client/src/app/core/auth/auth.guard.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts new file mode 100644 index 0000000..227ad15 --- /dev/null +++ b/client/src/app/core/auth/auth.service.ts @@ -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(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('/api/auth/me').subscribe({ + next: user => { + this.user = user; + resolve(true) + }, + error: () => { + resolve(false) + } + }) + }) + } + + + authenticateWithCode(authcode: string) { + return new Promise(resolve => { + this.http.post('/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 { + return this.http.post('/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 { + if (!this.refreshToken) { + return of(null); + } + + return this.http.post('/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; + } +} diff --git a/client/src/app/core/interceptor/token.interceptor.ts b/client/src/app/core/interceptor/token.interceptor.ts new file mode 100644 index 0000000..db8ca3f --- /dev/null +++ b/client/src/app/core/interceptor/token.interceptor.ts @@ -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, + next: HttpHandlerFn +): Observable> => { + 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); + } + }) + ); + +} + diff --git a/client/src/app/core/layout/layout.component.html b/client/src/app/core/layout/layout.component.html new file mode 100644 index 0000000..f34319c --- /dev/null +++ b/client/src/app/core/layout/layout.component.html @@ -0,0 +1,29 @@ + + + Keyvault + + + + + + + +sdf + + + + + + + + \ No newline at end of file diff --git a/client/src/app/core/layout/layout.component.scss b/client/src/app/core/layout/layout.component.scss new file mode 100644 index 0000000..d4eae9c --- /dev/null +++ b/client/src/app/core/layout/layout.component.scss @@ -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; +} \ No newline at end of file diff --git a/client/src/app/core/layout/layout.component.spec.ts b/client/src/app/core/layout/layout.component.spec.ts new file mode 100644 index 0000000..09aa9fc --- /dev/null +++ b/client/src/app/core/layout/layout.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LayoutComponent } from './layout.component'; + +describe('LayoutComponent', () => { + let component: LayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LayoutComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/core/layout/layout.component.ts b/client/src/app/core/layout/layout.component.ts new file mode 100644 index 0000000..33274e2 --- /dev/null +++ b/client/src/app/core/layout/layout.component.ts @@ -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 { + +} diff --git a/client/src/app/model/interface/user.interface.ts b/client/src/app/model/interface/user.interface.ts new file mode 100644 index 0000000..af4250d --- /dev/null +++ b/client/src/app/model/interface/user.interface.ts @@ -0,0 +1,8 @@ +export interface IUser { + username: string; + id: string; + lastName: string; + firstName: String; + refreshToken: string; + accessToken: string; +} \ No newline at end of file diff --git a/client/src/app/modules/admin/all-users/all-users.component.html b/client/src/app/modules/admin/all-users/all-users.component.html new file mode 100644 index 0000000..e87e1ba --- /dev/null +++ b/client/src/app/modules/admin/all-users/all-users.component.html @@ -0,0 +1,7 @@ +@if (gridOptions || true) { + +} \ No newline at end of file diff --git a/client/src/app/modules/admin/all-users/all-users.component.scss b/client/src/app/modules/admin/all-users/all-users.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/admin/all-users/all-users.component.spec.ts b/client/src/app/modules/admin/all-users/all-users.component.spec.ts new file mode 100644 index 0000000..ec38231 --- /dev/null +++ b/client/src/app/modules/admin/all-users/all-users.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AllUsersComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AllUsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/admin/all-users/all-users.component.ts b/client/src/app/modules/admin/all-users/all-users.component.ts new file mode 100644 index 0000000..74d5634 --- /dev/null +++ b/client/src/app/modules/admin/all-users/all-users.component.ts @@ -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) + } + +} diff --git a/client/src/app/modules/auth/login/login.component.html b/client/src/app/modules/auth/login/login.component.html new file mode 100644 index 0000000..6e2f42f --- /dev/null +++ b/client/src/app/modules/auth/login/login.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/app/modules/auth/login/login.component.scss b/client/src/app/modules/auth/login/login.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/auth/login/login.component.spec.ts b/client/src/app/modules/auth/login/login.component.spec.ts new file mode 100644 index 0000000..18f3685 --- /dev/null +++ b/client/src/app/modules/auth/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/client/src/app/modules/auth/login/login.component.ts b/client/src/app/modules/auth/login/login.component.ts new file mode 100644 index 0000000..c453443 --- /dev/null +++ b/client/src/app/modules/auth/login/login.component.ts @@ -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); +} diff --git a/client/src/app/modules/dashboard/dashboard.component.html b/client/src/app/modules/dashboard/dashboard.component.html new file mode 100644 index 0000000..3190ef6 --- /dev/null +++ b/client/src/app/modules/dashboard/dashboard.component.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/client/src/app/modules/dashboard/dashboard.component.scss b/client/src/app/modules/dashboard/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/dashboard/dashboard.component.spec.ts b/client/src/app/modules/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000..30e39a2 --- /dev/null +++ b/client/src/app/modules/dashboard/dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/dashboard/dashboard.component.ts b/client/src/app/modules/dashboard/dashboard.component.ts new file mode 100644 index 0000000..e324dda --- /dev/null +++ b/client/src/app/modules/dashboard/dashboard.component.ts @@ -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 } + ]; +} diff --git a/client/src/app/modules/start/start.component.html b/client/src/app/modules/start/start.component.html new file mode 100644 index 0000000..9f40752 --- /dev/null +++ b/client/src/app/modules/start/start.component.html @@ -0,0 +1 @@ +

start works!

diff --git a/client/src/app/modules/start/start.component.scss b/client/src/app/modules/start/start.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/start/start.component.spec.ts b/client/src/app/modules/start/start.component.spec.ts new file mode 100644 index 0000000..03e2127 --- /dev/null +++ b/client/src/app/modules/start/start.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StartComponent } from './start.component'; + +describe('StartComponent', () => { + let component: StartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StartComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/start/start.component.ts b/client/src/app/modules/start/start.component.ts new file mode 100644 index 0000000..683e197 --- /dev/null +++ b/client/src/app/modules/start/start.component.ts @@ -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) + }) + } +} diff --git a/client/src/app/shared/api.service.ts b/client/src/app/shared/api.service.ts new file mode 100644 index 0000000..4e2097c --- /dev/null +++ b/client/src/app/shared/api.service.ts @@ -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 { + return this.http.get('/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'); + } +} diff --git a/client/src/environments/environment.development.ts b/client/src/environments/environment.development.ts new file mode 100644 index 0000000..c3d6285 --- /dev/null +++ b/client/src/environments/environment.development.ts @@ -0,0 +1,3 @@ +export const environment = { + location: 'http://localhost:4200' +}; diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts new file mode 100644 index 0000000..5f02542 --- /dev/null +++ b/client/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + location: 'https://keyvaultpro.de' +}; diff --git a/client/src/index.html b/client/src/index.html index cef9802..50b5df8 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -5,9 +5,11 @@ Client - + + + - + diff --git a/client/src/styles.scss b/client/src/styles.scss index ea54ccd..bb6a1ee 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -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'; \ No newline at end of file +@import 'ag-grid-community/styles/ag-theme-quartz.css'; +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }