authentication
This commit is contained in:
@@ -6,7 +6,14 @@ DATABASE_HOST=localhost
|
|||||||
DATABASE_PORT=3306
|
DATABASE_PORT=3306
|
||||||
MYSQL_ROOT_PASSWORD=kjsdahflöijsdiu
|
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
|
# SECURITY
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=10m
|
||||||
17
api/package-lock.json
generated
17
api/package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
@@ -903,6 +904,22 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
|||||||
@@ -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 { AppService } from './app.service';
|
||||||
import { LoginDTO } from './model/dto/login.dto';
|
import { LoginDTO } from './model/dto/login.dto';
|
||||||
|
import { AuthGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): any {
|
getHello(): any {
|
||||||
|
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
login(@Body() createUserDto: LoginDTO) {
|
login(@Body() createUserDto: LoginDTO) {
|
||||||
|
|
||||||
return { success: createUserDto };
|
return { success: createUserDto };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { AppService } from './app.service';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { DatabaseModule } from './shared/database/database.module';
|
import { DatabaseModule } from './shared/database/database.module';
|
||||||
import { AuthModule } from './modules/auth/auth.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -13,8 +16,10 @@ import { AuthModule } from './modules/auth/auth.module';
|
|||||||
}),
|
}),
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
UserModule,
|
||||||
|
RoleModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService, AuthGuard],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
61
api/src/core/guards/auth.guard.ts
Normal file
61
api/src/core/guards/auth.guard.ts
Normal file
@@ -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<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
api/src/model/decorators/admin.decorator.ts
Normal file
5
api/src/model/decorators/admin.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Expose } from 'class-transformer';
|
||||||
|
|
||||||
|
export function AdminGroup() {
|
||||||
|
return Expose({ groups: ['admin'] }); // Setzt die Gruppe 'admin' automatisch
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './sso.user.entity';
|
export * from './sso.user.entity';
|
||||||
export * from './user.entity';
|
export * from './user.entity';
|
||||||
|
export * from './role.entity';
|
||||||
|
|||||||
19
api/src/model/entitites/role.entity.ts
Normal file
19
api/src/model/entitites/role.entity.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Exclude } from 'class-transformer';
|
import { Exclude, Transform } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IUser } from '../interface';
|
import { IUser } from '../interface';
|
||||||
import { SSOUser } from './sso.user.entity';
|
import { SSOUser } from './sso.user.entity';
|
||||||
import { IsEmail } from 'class-validator';
|
import { IsEmail } from 'class-validator';
|
||||||
|
import { Role } from './role.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User implements IUser {
|
export class User implements IUser {
|
||||||
@@ -35,10 +38,14 @@ export class User implements IUser {
|
|||||||
@OneToOne(() => SSOUser, (sso) => sso.user, { eager: true, cascade: true })
|
@OneToOne(() => SSOUser, (sso) => sso.user, { eager: true, cascade: true })
|
||||||
external: SSOUser;
|
external: SSOUser;
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ManyToOne(() => Role, (role) => role.user, { cascade: true })
|
||||||
|
@JoinColumn()
|
||||||
|
@Transform(({ value }) => value.name)
|
||||||
|
role: Role;
|
||||||
|
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Role } from '../entitites';
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -7,4 +9,6 @@ export interface IUser {
|
|||||||
|
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
|
|
||||||
|
role?: string | Role;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './user.repository';
|
export * from './user.repository';
|
||||||
export * from './ssouser.repository';
|
export * from './ssouser.repository';
|
||||||
|
export * from './role.repository';
|
||||||
|
|||||||
14
api/src/model/repositories/role.repository.ts
Normal file
14
api/src/model/repositories/role.repository.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
import { Role } from '../entitites';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleRepository extends Repository<Role> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(Role, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
getStandardRole(): Promise<Role> {
|
||||||
|
return this.findOne({ where: { name: 'develop' } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ import { Repository, DataSource } from 'typeorm';
|
|||||||
import { User } from '../entitites';
|
import { User } from '../entitites';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { SsoUserRepository } from './ssouser.repository';
|
import { SsoUserRepository } from './ssouser.repository';
|
||||||
|
import { RoleRepository } from './role.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository extends Repository<User> {
|
export class UserRepository extends Repository<User> {
|
||||||
constructor(
|
constructor(
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
private ssoRepo: SsoUserRepository,
|
private ssoRepo: SsoUserRepository,
|
||||||
|
private roleRepo: RoleRepository,
|
||||||
) {
|
) {
|
||||||
super(User, dataSource.createEntityManager());
|
super(User, dataSource.createEntityManager());
|
||||||
}
|
}
|
||||||
@@ -38,6 +40,7 @@ export class UserRepository extends Repository<User> {
|
|||||||
externalId: createUserDto.externalId,
|
externalId: createUserDto.externalId,
|
||||||
});
|
});
|
||||||
created.external = sso;
|
created.external = sso;
|
||||||
|
created.role = await this.roleRepo.getStandardRole();
|
||||||
const user = await this.save(created);
|
const user = await this.save(created);
|
||||||
sso.user = user;
|
sso.user = user;
|
||||||
this.ssoRepo.save(sso);
|
this.ssoRepo.save(sso);
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Post,
|
Post,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthCodeDto } from 'src/model/dto';
|
import { AuthCodeDto } from 'src/model/dto';
|
||||||
import { User } from 'src/model/entitites';
|
import { User } from 'src/model/entitites';
|
||||||
|
import { AuthGuard } from 'src/core/guards/auth.guard';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -23,4 +27,19 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
return user;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AuthService } from './auth.service';
|
|||||||
import { DatabaseModule } from 'src/shared/database/database.module';
|
import { DatabaseModule } from 'src/shared/database/database.module';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule, JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
@@ -22,5 +22,6 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
exports: [JwtModule, AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { AuthCodeDto } from 'src/model/dto';
|
import { AuthCodeDto } from 'src/model/dto';
|
||||||
import { CreateUserDto } from 'src/model/dto/create-user.dto';
|
import { CreateUserDto } from 'src/model/dto/create-user.dto';
|
||||||
import { UserRepository } from 'src/model/repositories';
|
import { UserRepository } from 'src/model/repositories';
|
||||||
@@ -21,7 +21,6 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> {
|
async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> {
|
||||||
console.log(auth);
|
|
||||||
const body = this.createAuthCodeFormData(auth.code, 'authorization_code');
|
const body = this.createAuthCodeFormData(auth.code, 'authorization_code');
|
||||||
const url = this.configService.get('SSO_TOKEN_URL');
|
const url = this.configService.get('SSO_TOKEN_URL');
|
||||||
return new Promise<User>((resolve) => {
|
return new Promise<User>((resolve) => {
|
||||||
@@ -45,7 +44,6 @@ export class AuthService {
|
|||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
}): Promise<User> {
|
}): Promise<User> {
|
||||||
console.log(access_token, refresh_token);
|
|
||||||
const payload: IExternalAccessPayload = this.jwt.decode(access_token);
|
const payload: IExternalAccessPayload = this.jwt.decode(access_token);
|
||||||
return new Promise<User>(async (resolve) => {
|
return new Promise<User>(async (resolve) => {
|
||||||
let user = await this.userRepo.findByUsername(payload.username);
|
let user = await this.userRepo.findByUsername(payload.username);
|
||||||
@@ -56,6 +54,9 @@ export class AuthService {
|
|||||||
externalId: payload.id,
|
externalId: payload.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!user.isActive) {
|
||||||
|
throw new HttpException('not active', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
user.firstName = payload.firstName;
|
user.firstName = payload.firstName;
|
||||||
user.lastName = payload.lastName;
|
user.lastName = payload.lastName;
|
||||||
@@ -82,7 +83,7 @@ export class AuthService {
|
|||||||
type: 'refresh',
|
type: 'refresh',
|
||||||
};
|
};
|
||||||
|
|
||||||
user.refreshToken = this.jwt.sign(rPay);
|
user.refreshToken = this.jwt.sign(rPay, { expiresIn: '1w' });
|
||||||
}
|
}
|
||||||
|
|
||||||
private createAuthCodeFormData(
|
private createAuthCodeFormData(
|
||||||
@@ -103,4 +104,76 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
return bodyFormData;
|
return bodyFormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserById(id: string): Promise<User> {
|
||||||
|
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<any> {
|
||||||
|
const bodyFormData = this.createAuthCodeFormData(
|
||||||
|
user.external.refreshToken,
|
||||||
|
'refresh_token',
|
||||||
|
);
|
||||||
|
const url = this.configService.get('SSO_TOKEN_URL');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.http.post<any>(url, bodyFormData).subscribe({
|
||||||
|
next: async (response) => {
|
||||||
|
user.external.accessToken = response.data.access_token;
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
resolve(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
api/src/modules/role/role.controller.spec.ts
Normal file
18
api/src/modules/role/role.controller.spec.ts
Normal file
@@ -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>(RoleController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
api/src/modules/role/role.controller.ts
Normal file
15
api/src/modules/role/role.controller.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
api/src/modules/role/role.module.ts
Normal file
12
api/src/modules/role/role.module.ts
Normal file
@@ -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 {}
|
||||||
18
api/src/modules/role/role.service.spec.ts
Normal file
18
api/src/modules/role/role.service.spec.ts
Normal file
@@ -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>(RoleService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
api/src/modules/role/role.service.ts
Normal file
11
api/src/modules/role/role.service.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
api/src/modules/user/user.controller.ts
Normal file
21
api/src/modules/user/user.controller.ts
Normal file
@@ -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<User[]> {
|
||||||
|
return this.userService.getAllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
saveUser(@Body() user: IUser) {
|
||||||
|
return this.userService.saveUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
api/src/modules/user/user.module.ts
Normal file
12
api/src/modules/user/user.module.ts
Normal file
@@ -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 {}
|
||||||
26
api/src/modules/user/user.service.ts
Normal file
26
api/src/modules/user/user.service.ts
Normal file
@@ -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<User[]> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { SSOUser, User } from 'src/model/entitites';
|
import { Role, SSOUser, User } from 'src/model/entitites';
|
||||||
import { SsoUserRepository, UserRepository } from 'src/model/repositories';
|
import { RoleRepository, SsoUserRepository, UserRepository } from 'src/model/repositories';
|
||||||
|
|
||||||
const ENTITIES = [User, SSOUser];
|
const ENTITIES = [User, SSOUser, Role];
|
||||||
const REPOSITORIES = [UserRepository, SsoUserRepository];
|
const REPOSITORIES = [UserRepository, SsoUserRepository, RoleRepository];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -32,8 +32,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
"src/styles.scss",
|
"src/styles.scss",
|
||||||
"src/styles/ag.css"
|
"src/styles/ag.css",
|
||||||
|
"node_modules/@ngxpert/hot-toast/src/styles/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -56,7 +58,13 @@
|
|||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
@@ -95,6 +103,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
|
|||||||
64
client/package-lock.json
generated
64
client/package-lock.json
generated
@@ -9,13 +9,17 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^18.0.0",
|
"@angular/animations": "^18.0.0",
|
||||||
|
"@angular/cdk": "^18.2.4",
|
||||||
"@angular/common": "^18.0.0",
|
"@angular/common": "^18.0.0",
|
||||||
"@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.4",
|
||||||
"@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",
|
||||||
|
"@ngneat/overview": "^6.0.0",
|
||||||
|
"@ngxpert/hot-toast": "^3.0.1",
|
||||||
"ag-grid-angular": "^32.1.0",
|
"ag-grid-angular": "^32.1.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.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": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "18.2.4",
|
"version": "18.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.4.tgz",
|
||||||
@@ -470,6 +490,23 @@
|
|||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"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": {
|
"node_modules/@angular/platform-browser": {
|
||||||
"version": "18.2.4",
|
"version": "18.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz",
|
||||||
@@ -3293,6 +3330,17 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@ngtools/webpack": {
|
||||||
"version": "18.2.4",
|
"version": "18.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.4.tgz",
|
||||||
@@ -3309,6 +3357,18 @@
|
|||||||
"webpack": "^5.54.0"
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -6155,7 +6215,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"
|
||||||
},
|
},
|
||||||
@@ -10000,7 +10060,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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,13 +11,17 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^18.0.0",
|
"@angular/animations": "^18.0.0",
|
||||||
|
"@angular/cdk": "^18.2.4",
|
||||||
"@angular/common": "^18.0.0",
|
"@angular/common": "^18.0.0",
|
||||||
"@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.4",
|
||||||
"@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",
|
||||||
|
"@ngneat/overview": "^6.0.0",
|
||||||
|
"@ngxpert/hot-toast": "^3.0.1",
|
||||||
"ag-grid-angular": "^32.1.0",
|
"ag-grid-angular": "^32.1.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
@@ -36,4 +40,4 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.4.2"
|
"typescript": "~5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
1
client/public/key.svg
Normal file
1
client/public/key.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" enable-background="new 0 0 100 100" height="100" viewBox="0 0 100 100" width="100" xmlns="http://www.w3.org/2000/svg"><g><path d="m88.287 60.723h-4.46v-26.525c0-4.909-3.62-8.902-8.07-8.902h-51.515c-4.449 0-8.068 3.994-8.068 8.902v26.524h-4.461c-1.104 0-2 .896-2 2v2.68c0 5.129 4.173 9.302 9.302 9.302h61.971c5.13 0 9.302-4.173 9.302-9.302v-2.68c-.001-1.104-.895-1.999-2.001-1.999zm-64.045-31.427h51.515c2.206 0 4.07 2.245 4.07 4.902v26.524h-59.653v-26.524c0-2.657 1.863-4.902 4.068-4.902zm56.744 41.408h-61.971c-2.923 0-5.302-2.378-5.302-5.302v-.68h4.461 63.653 4.46v.68c0 2.924-2.378 5.302-5.301 5.302z" fill="#2b273d"/><path d="m28.525 53.705h10.805c.552 0 1-.447 1-1v-10.807c0-.552-.448-1-1-1h-1.148c.016-.071.043-.139.043-.215 0-2.819-1.928-5.113-4.297-5.113-2.371 0-4.299 2.294-4.299 5.113 0 .076.027.144.043.215h-1.148c-.552 0-1 .448-1 1v10.807c.001.553.449 1 1.001 1zm3.105-13.021c0-1.688 1.053-3.113 2.299-3.113 1.245 0 2.297 1.426 2.297 3.113 0 .076.027.144.043.215h-4.683c.017-.072.044-.139.044-.215zm-2.105 2.214h8.805v8.807h-8.805z" fill="#8e2db2"/><g fill="#2b273d"><path d="m44.695 52.729c0 .554.448 1 1 1h19.019c4.28 0 7.763-2.873 7.763-6.405 0-3.529-3.481-6.399-7.763-6.399h-19.019c-.552 0-1 .448-1 1s.448 1 1 1h19.019c3.123 0 5.763 2.015 5.763 4.399 0 2.43-2.586 4.405-5.763 4.405h-19.019c-.552 0-1 .447-1 1z"/><path d="m45.695 47.172 1.336.369-.934 1.113.709.463.754-1.163.75 1.204.708-.482-.897-1.086 1.352-.369-.266-.782-1.271.525.107-1.47h-.877l.104 1.441-1.308-.537z"/><path d="m51.686 47.172 1.335.369-.934 1.113.709.463.754-1.163.748 1.204.709-.482-.898-1.086 1.353-.369-.267-.782-1.27.525.107-1.47h-.878l.103 1.441-1.307-.537z"/><path d="m57.675 47.172 1.334.369-.934 1.113.709.463.754-1.163.75 1.204.709-.482-.897-1.086 1.35-.369-.262-.782-1.273.525.106-1.47h-.875l.101 1.441-1.308-.537z"/><path d="m63.663 47.172 1.338.369-.937 1.113.709.463.754-1.163.75 1.204.707-.482-.893-1.086 1.35-.369-.266-.782-1.271.525.108-1.47h-.878l.102 1.441-1.304-.537z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,10 +1,16 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<!-- <div class="example-sidenav-content">
|
||||||
|
<button type="button" mat-button (click)="drawer.toggle()">
|
||||||
|
Toggle sidenav
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <div class="content" style="width: 100%; height: 100%;"> -->
|
<!-- <div class="content" style="width: 100%; height: 100%;"> -->
|
||||||
<!-- The AG Grid component, with Dimensions, CSS Theme, Row Data, and Column Definition -->
|
<!-- The AG Grid component, with Dimensions, CSS Theme, Row Data, and Column Definition -->
|
||||||
<ag-grid-angular
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
|
|
||||||
[rowData]="rowData"
|
|
||||||
[columnDefs]="colDefs"
|
|
||||||
[defaultColDef]="defaultColDef"
|
|
||||||
/>
|
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Component, inject, LOCALE_ID } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, AgGridAngular],
|
imports: [RouterOutlet,],
|
||||||
providers: [[{ provide: LOCALE_ID, useValue: 'de-DE' }]],
|
providers: [
|
||||||
|
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
||||||
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'client';
|
title = 'client';
|
||||||
|
|
||||||
defaultColDef: ColDef = {
|
|
||||||
flex: 1,
|
|
||||||
editable: true
|
|
||||||
};
|
|
||||||
|
|
||||||
private http: HttpClient = inject(HttpClient);
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.http.get('api/').subscribe({
|
|
||||||
next: n => {
|
|
||||||
console.log(n)
|
|
||||||
},
|
|
||||||
error: e => {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rowData = [
|
ngOnInit(): void {
|
||||||
{ 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 }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { provideHotToastConfig } from '@ngxpert/hot-toast';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
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 = {
|
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()]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
import { Routes } from '@angular/router';
|
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},
|
||||||
|
];
|
||||||
|
|||||||
36
client/src/app/core/auth/auth.guard.ts
Normal file
36
client/src/app/core/auth/auth.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, Router } from "@angular/router";
|
||||||
|
import { HotToastService } from "@ngxpert/hot-toast";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthenticatedGuard {
|
||||||
|
public isLoading = false;
|
||||||
|
private router = inject(Router);
|
||||||
|
private toast = inject(HotToastService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot):
|
||||||
|
Promise<boolean> {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
if (this.authService.authenticated) { return true; }
|
||||||
|
|
||||||
|
const s = await this.authService.getMe();
|
||||||
|
if (s) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const authCode = route.queryParams["code"];
|
||||||
|
if (authCode) {
|
||||||
|
const success = await this.authService.authenticateWithCode(authCode);
|
||||||
|
if (success) { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigateByUrl('/login');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
client/src/app/core/auth/auth.service.ts
Normal file
108
client/src/app/core/auth/auth.service.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { BehaviorSubject, Observable, tap, of, catchError } from 'rxjs';
|
||||||
|
import { IUser } from '../../model/interface/user.interface';
|
||||||
|
import { environment } from '../../../environments/environment.development';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
|
||||||
|
private accessTokenSubject = new BehaviorSubject<string | null>(null);
|
||||||
|
private refreshToken: string | null = null;
|
||||||
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
private route: ActivatedRoute = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
private user: IUser | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const token = localStorage.getItem('accessToken_vault');
|
||||||
|
const refresh = localStorage.getItem('refreshToken_vault');
|
||||||
|
|
||||||
|
this.accessTokenSubject.next(token);
|
||||||
|
this.refreshToken = refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getMe() {
|
||||||
|
if (!this.getAccessToken()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.http.get<IUser>('/api/auth/me').subscribe({
|
||||||
|
next: user => {
|
||||||
|
this.user = user;
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
authenticateWithCode(authcode: string) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.http.post<IUser>('/api/auth/auth-code', { code: authcode }).subscribe(user => {
|
||||||
|
this.setTokens({ accessToken: user.accessToken, refreshToken: user.refreshToken});
|
||||||
|
this.user = user;
|
||||||
|
return resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get authenticated(): boolean {
|
||||||
|
return this.user != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
login(credentials: { username: string; password: string }): Observable<any> {
|
||||||
|
return this.http.post<any>('/api/auth/login', credentials).pipe(
|
||||||
|
tap(tokens => {
|
||||||
|
this.setTokens(tokens);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setTokens(tokens: { accessToken: string; refreshToken: string }) {
|
||||||
|
this.accessTokenSubject.next(tokens.accessToken);
|
||||||
|
this.refreshToken = tokens.refreshToken;
|
||||||
|
localStorage.setItem('accessToken_vault', tokens.accessToken);
|
||||||
|
localStorage.setItem('refreshToken_vault', tokens.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessToken(): string | null {
|
||||||
|
return this.accessTokenSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAccessToken(): Observable<any> {
|
||||||
|
if (!this.refreshToken) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post<any>('/api/auth/refresh', { refreshToken: this.refreshToken }).pipe(
|
||||||
|
tap(tokens => {
|
||||||
|
this.setTokens(tokens);
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
this.logout();
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.accessTokenSubject.next(null);
|
||||||
|
this.refreshToken = null;
|
||||||
|
localStorage.removeItem('accessToken_vault');
|
||||||
|
localStorage.removeItem('refreshToken_vault');
|
||||||
|
}
|
||||||
|
|
||||||
|
public routeToLogin() {
|
||||||
|
const url = `https://sso.beantastic.de?client_id=ffc46841-26f8-4946-a57a-5a9f8f21bc13&redirect_uri=${environment.location}`;
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
client/src/app/core/interceptor/token.interceptor.ts
Normal file
52
client/src/app/core/interceptor/token.interceptor.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpEvent,
|
||||||
|
HttpRequest,
|
||||||
|
HttpErrorResponse,
|
||||||
|
HttpInterceptorFn,
|
||||||
|
HttpHandlerFn
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Observable, catchError, switchMap, throwError } from 'rxjs';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
|
||||||
|
export const tokenInterceptor: HttpInterceptorFn = (
|
||||||
|
req: HttpRequest<any>,
|
||||||
|
next: HttpHandlerFn
|
||||||
|
): Observable<HttpEvent<any>> => {
|
||||||
|
const authService: AuthService = inject(AuthService);
|
||||||
|
const toast: HotToastService = inject(HotToastService);
|
||||||
|
const accessToken = authService.getAccessToken();
|
||||||
|
let authReq = req;
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
authReq = req.clone({
|
||||||
|
setHeaders: { Authorization: `Bearer ${accessToken}` }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(authReq).pipe(
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
return authService.refreshAccessToken().pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
const newAccessToken = authService.getAccessToken();
|
||||||
|
const newAuthReq = req.clone({
|
||||||
|
setHeaders: { Authorization: `Bearer ${newAccessToken}` }
|
||||||
|
});
|
||||||
|
return next(newAuthReq);
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
authService.logout();
|
||||||
|
toast.error(err.error.message)
|
||||||
|
return throwError(() => err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
29
client/src/app/core/layout/layout.component.html
Normal file
29
client/src/app/core/layout/layout.component.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<mat-toolbar>
|
||||||
|
<button mat-icon-button (click)="drawer.toggle()">
|
||||||
|
<mat-icon>menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span>Keyvault</span>
|
||||||
|
<span class="example-spacer"></span>
|
||||||
|
<button mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
|
||||||
|
<mat-icon>favorite</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon">
|
||||||
|
<mat-icon>share</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<mat-drawer-container class="example-container" autosize>
|
||||||
|
<mat-drawer #drawer class="main_sidenav" mode="side" opened="true">
|
||||||
|
sdf
|
||||||
|
</mat-drawer>
|
||||||
|
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<!-- <div class="example-sidenav-content">
|
||||||
|
<button type="button" mat-button (click)="drawer.toggle()">
|
||||||
|
Toggle sidenav
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</mat-drawer-container>
|
||||||
24
client/src/app/core/layout/layout.component.scss
Normal file
24
client/src/app/core/layout/layout.component.scss
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-drawer-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mat-drawer, mat-toolbar {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_sidenav{
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
23
client/src/app/core/layout/layout.component.spec.ts
Normal file
23
client/src/app/core/layout/layout.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LayoutComponent } from './layout.component';
|
||||||
|
|
||||||
|
describe('LayoutComponent', () => {
|
||||||
|
let component: LayoutComponent;
|
||||||
|
let fixture: ComponentFixture<LayoutComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LayoutComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LayoutComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
client/src/app/core/layout/layout.component.ts
Normal file
17
client/src/app/core/layout/layout.component.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-layout',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule, MatIconModule, MatSidenavModule, RouterModule, MatToolbarModule],
|
||||||
|
templateUrl: './layout.component.html',
|
||||||
|
styleUrl: './layout.component.scss'
|
||||||
|
})
|
||||||
|
export class LayoutComponent {
|
||||||
|
|
||||||
|
}
|
||||||
8
client/src/app/model/interface/user.interface.ts
Normal file
8
client/src/app/model/interface/user.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface IUser {
|
||||||
|
username: string;
|
||||||
|
id: string;
|
||||||
|
lastName: string;
|
||||||
|
firstName: String;
|
||||||
|
refreshToken: string;
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@if (gridOptions || true) {
|
||||||
|
<ag-grid-angular
|
||||||
|
style="width: 100%; height: 100%;"
|
||||||
|
(gridReady)="onGridReady($event)"
|
||||||
|
[gridOptions]="gridOptions!"
|
||||||
|
/>
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AllUsersComponent } from './all-users.component';
|
||||||
|
|
||||||
|
describe('AllUsersComponent', () => {
|
||||||
|
let component: AllUsersComponent;
|
||||||
|
let fixture: ComponentFixture<AllUsersComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AllUsersComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AllUsersComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
client/src/app/modules/auth/login/login.component.html
Normal file
1
client/src/app/modules/auth/login/login.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<button mat-flat-button color="primary" (click)="authService.routeToLogin()">Login</button>
|
||||||
23
client/src/app/modules/auth/login/login.component.spec.ts
Normal file
23
client/src/app/modules/auth/login/login.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
client/src/app/modules/auth/login/login.component.ts
Normal file
14
client/src/app/modules/auth/login/login.component.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.scss'
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
public authService: AuthService = inject(AuthService);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<ag-grid-angular
|
||||||
|
style="width: 100%; height: 100%;"
|
||||||
|
|
||||||
|
[rowData]="rowData"
|
||||||
|
[columnDefs]="colDefs"
|
||||||
|
[defaultColDef]="defaultColDef"
|
||||||
|
/>
|
||||||
23
client/src/app/modules/dashboard/dashboard.component.spec.ts
Normal file
23
client/src/app/modules/dashboard/dashboard.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DashboardComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
client/src/app/modules/dashboard/dashboard.component.ts
Normal file
51
client/src/app/modules/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { AgGridAngular } from 'ag-grid-angular';
|
||||||
|
import { ColDef } from 'ag-grid-community'; // Column Definition Type Interface
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AgGridAngular],
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrl: './dashboard.component.scss'
|
||||||
|
})
|
||||||
|
export class DashboardComponent {
|
||||||
|
|
||||||
|
defaultColDef: ColDef = {
|
||||||
|
flex: 1,
|
||||||
|
editable: true
|
||||||
|
};
|
||||||
|
rowData = [
|
||||||
|
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
|
||||||
|
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
|
||||||
|
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
|
||||||
|
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
|
||||||
|
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
|
||||||
|
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
|
||||||
|
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
|
||||||
|
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
|
||||||
|
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
|
||||||
|
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Column Definitions: Defines the columns to be displayed.
|
||||||
|
colDefs: ColDef[] = [
|
||||||
|
{ field: "make" },
|
||||||
|
{
|
||||||
|
field: "model",
|
||||||
|
cellEditor: 'agSelectCellEditor',
|
||||||
|
singleClickEdit: true,
|
||||||
|
cellEditorParams: {
|
||||||
|
values: ['English', 'Spanish', 'French', 'Portuguese', '(other)'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ field: "price", type: 'number'
|
||||||
|
// cellEditor: 'agDateCellEditor',
|
||||||
|
// cellEditorParams: {
|
||||||
|
// min: '2000-01-01',
|
||||||
|
// max: '2019-12-31',
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
{ field: "electric", editable: true }
|
||||||
|
];
|
||||||
|
}
|
||||||
1
client/src/app/modules/start/start.component.html
Normal file
1
client/src/app/modules/start/start.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>start works!</p>
|
||||||
0
client/src/app/modules/start/start.component.scss
Normal file
0
client/src/app/modules/start/start.component.scss
Normal file
23
client/src/app/modules/start/start.component.spec.ts
Normal file
23
client/src/app/modules/start/start.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StartComponent } from './start.component';
|
||||||
|
|
||||||
|
describe('StartComponent', () => {
|
||||||
|
let component: StartComponent;
|
||||||
|
let fixture: ComponentFixture<StartComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [StartComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(StartComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
client/src/app/modules/start/start.component.ts
Normal file
19
client/src/app/modules/start/start.component.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-start',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './start.component.html',
|
||||||
|
styleUrl: './start.component.scss'
|
||||||
|
})
|
||||||
|
export class StartComponent {
|
||||||
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.http.get('/api/').subscribe(res => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/src/app/shared/api.service.ts
Normal file
26
client/src/app/shared/api.service.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IUser } from '../model/interface/user.interface';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
|
||||||
|
getAllUsers(): Observable<IUser[]> {
|
||||||
|
return this.http.get<IUser[]>('/api/user');
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUser(user: IUser) {
|
||||||
|
return this.http.post('/api/user', user);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoles(): Observable<{id: string, name: string}[]> {
|
||||||
|
return this.http.get<{id: string, name: string}[]>('/api/role');
|
||||||
|
}
|
||||||
|
}
|
||||||
3
client/src/environments/environment.development.ts
Normal file
3
client/src/environments/environment.development.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
location: 'http://localhost:4200'
|
||||||
|
};
|
||||||
3
client/src/environments/environment.ts
Normal file
3
client/src/environments/environment.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
location: 'https://keyvaultpro.de'
|
||||||
|
};
|
||||||
@@ -5,9 +5,11 @@
|
|||||||
<title>Client</title>
|
<title>Client</title>
|
||||||
<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" href="key.svg">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="mat-typography">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ html, body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background-color: #e2e2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Core Data Grid CSS */
|
/* Core Data Grid CSS */
|
||||||
@import "ag-grid-community/styles/ag-grid.css";
|
@import "ag-grid-community/styles/ag-grid.css";
|
||||||
/* Quartz Theme Specific CSS */
|
/* Quartz Theme Specific CSS */
|
||||||
@import 'ag-grid-community/styles/ag-theme-quartz.css';
|
@import 'ag-grid-community/styles/ag-theme-quartz.css';
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
|
|||||||
Reference in New Issue
Block a user