| @@ -2400,6 +2400,16 @@ components: | |||||
| type: string | type: string | ||||
| active: | active: | ||||
| type: boolean | type: boolean | ||||
| roles: | |||||
| readOnly: true | |||||
| type: array | |||||
| items: | |||||
| type: string | |||||
| token: | |||||
| readOnly: true | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| createdAt: | createdAt: | ||||
| readOnly: true | readOnly: true | ||||
| type: | type: | ||||
| @@ -2474,6 +2484,16 @@ components: | |||||
| type: string | type: string | ||||
| active: | active: | ||||
| type: boolean | type: boolean | ||||
| roles: | |||||
| readOnly: true | |||||
| type: array | |||||
| items: | |||||
| type: string | |||||
| token: | |||||
| readOnly: true | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| createdAt: | createdAt: | ||||
| readOnly: true | readOnly: true | ||||
| type: | type: | ||||
| @@ -2905,31 +2925,8 @@ components: | |||||
| AuthResponse: | AuthResponse: | ||||
| type: object | type: object | ||||
| properties: | properties: | ||||
| token: | |||||
| type: string | |||||
| readOnly: true | |||||
| id: | |||||
| type: integer | |||||
| readOnly: true | |||||
| email: | |||||
| type: string | |||||
| readOnly: true | |||||
| firstName: | |||||
| type: string | |||||
| readOnly: true | |||||
| lastName: | |||||
| type: string | |||||
| readOnly: true | |||||
| roles: | |||||
| type: array | |||||
| items: | |||||
| type: string | |||||
| readOnly: true | |||||
| user: | user: | ||||
| type: object | |||||
| allOf: | |||||
| - | |||||
| $ref: '#/components/schemas/User' | |||||
| $ref: '#/components/schemas/User.jsonld' | |||||
| readOnly: true | readOnly: true | ||||
| responses: { } | responses: { } | ||||
| parameters: { } | parameters: { } | ||||
| @@ -87,6 +87,8 @@ export const userForm = new FormGroup({ | |||||
| fullName: new FormControl(null, []), | fullName: new FormControl(null, []), | ||||
| password: new FormControl(null, []), | password: new FormControl(null, []), | ||||
| active: new FormControl(null, []), | active: new FormControl(null, []), | ||||
| roles: new FormControl(null, []), | |||||
| token: new FormControl(null, []), | |||||
| createdAt: new FormControl(null, []) | createdAt: new FormControl(null, []) | ||||
| }); | }); | ||||
| @@ -100,6 +102,8 @@ export const userJsonldForm = new FormGroup({ | |||||
| fullName: new FormControl(null, []), | fullName: new FormControl(null, []), | ||||
| password: new FormControl(null, []), | password: new FormControl(null, []), | ||||
| active: new FormControl(null, []), | active: new FormControl(null, []), | ||||
| roles: new FormControl(null, []), | |||||
| token: new FormControl(null, []), | |||||
| createdAt: new FormControl(null, []) | createdAt: new FormControl(null, []) | ||||
| }); | }); | ||||
| @@ -193,11 +197,5 @@ export const credentialsForm = new FormGroup({ | |||||
| }); | }); | ||||
| export const authResponseForm = new FormGroup({ | export const authResponseForm = new FormGroup({ | ||||
| token: new FormControl(null, []), | |||||
| id: new FormControl(null, []), | |||||
| email: new FormControl(null, []), | |||||
| firstName: new FormControl(null, []), | |||||
| lastName: new FormControl(null, []), | |||||
| roles: new FormControl(null, []), | |||||
| user: new FormControl(null, []) | user: new FormControl(null, []) | ||||
| }); | }); | ||||
| @@ -1,2 +1 @@ | |||||
| export * from './alert'; | |||||
| export * from './user'; | |||||
| export * from './alert'; | |||||
| @@ -1,12 +0,0 @@ | |||||
| import {UserJsonld} from "@app/core/api/v1"; | |||||
| export class User { | |||||
| id?: string; | |||||
| email?: string; | |||||
| password?: string; | |||||
| firstName?: string; | |||||
| lastName?: string; | |||||
| roles?: string[]; | |||||
| token?: string; | |||||
| userResource?: UserJsonld; | |||||
| } | |||||
| @@ -4,21 +4,19 @@ import { HttpClient } from '@angular/common/http'; | |||||
| import { BehaviorSubject, Observable } from 'rxjs'; | import { BehaviorSubject, Observable } from 'rxjs'; | ||||
| import { map } from 'rxjs/operators'; | import { map } from 'rxjs/operators'; | ||||
| import { environment } from '@environments/environment'; | |||||
| import { User } from '@app/_models'; | |||||
| import {AuthService} from "@app/core/api/v1"; | |||||
| import {AuthService, UserJsonld} from "@app/core/api/v1"; | |||||
| @Injectable({ providedIn: 'root' }) | @Injectable({ providedIn: 'root' }) | ||||
| export class AccountService { | export class AccountService { | ||||
| private userSubject: BehaviorSubject<User | null>; | |||||
| public user: Observable<User | null>; | |||||
| private userSubject: BehaviorSubject<UserJsonld | null>; | |||||
| public user: Observable<UserJsonld | null>; | |||||
| constructor( | constructor( | ||||
| private router: Router, | private router: Router, | ||||
| private http: HttpClient, | private http: HttpClient, | ||||
| private loginCheckService: AuthService | |||||
| private authService: AuthService | |||||
| ) { | ) { | ||||
| this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); | this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); | ||||
| this.user = this.userSubject.asObservable(); | this.user = this.userSubject.asObservable(); | ||||
| @@ -29,24 +27,12 @@ export class AccountService { | |||||
| } | } | ||||
| login(email: string, password: string) { | login(email: string, password: string) { | ||||
| return this.loginCheckService.postCredentialsItem( | |||||
| { email, password } | |||||
| ).pipe(map(authResponse => { | |||||
| // store user details and jwt token in local storage to keep user logged in between page refreshes | |||||
| localStorage.setItem('user', JSON.stringify(authResponse.user)); | |||||
| if (authResponse.user) { | |||||
| this.userSubject.next(authResponse.user); | |||||
| } | |||||
| return authResponse.user; | |||||
| })); | |||||
| // | |||||
| // return this.http.post<User>(`${environment.apiUrl}/auth`, { email, password }) | |||||
| // .pipe(map(user => { | |||||
| // // store user details and jwt token in local storage to keep user logged in between page refreshes | |||||
| // localStorage.setItem('user', JSON.stringify(user)); | |||||
| // this.userSubject.next(user); | |||||
| // return user; | |||||
| // })); | |||||
| return this.authService.postCredentialsItem({ email, password }) | |||||
| .pipe(map(response => { | |||||
| localStorage.setItem('user', JSON.stringify(response.user)); | |||||
| this.userSubject.next(response.user || null); | |||||
| return response.user; | |||||
| })); | |||||
| } | } | ||||
| logout() { | logout() { | ||||
| @@ -73,43 +59,4 @@ export class AccountService { | |||||
| } | } | ||||
| return false; | return false; | ||||
| } | } | ||||
| register(user: User) { | |||||
| return this.http.post(`${environment.apiUrl}/users/register`, user); | |||||
| } | |||||
| getAll() { | |||||
| return this.http.get<User[]>(`${environment.apiUrl}/users`); | |||||
| } | |||||
| getById(id: string) { | |||||
| return this.http.get<User>(`${environment.apiUrl}/users/${id}`); | |||||
| } | |||||
| update(id: string, params: any) { | |||||
| return this.http.put(`${environment.apiUrl}/users/${id}`, params) | |||||
| .pipe(map(x => { | |||||
| // update stored user if the logged in user updated their own record | |||||
| if (id == this.userValue?.id) { | |||||
| // update local storage | |||||
| const user = { ...this.userValue, ...params }; | |||||
| localStorage.setItem('user', JSON.stringify(user)); | |||||
| // publish updated user to subscribers | |||||
| this.userSubject.next(user); | |||||
| } | |||||
| return x; | |||||
| })); | |||||
| } | |||||
| delete(id: string) { | |||||
| return this.http.delete(`${environment.apiUrl}/users/${id}`) | |||||
| .pipe(map(x => { | |||||
| // auto logout if the logged in user deleted their own record | |||||
| if (id == this.userValue?.id) { | |||||
| this.logout(); | |||||
| } | |||||
| return x; | |||||
| })); | |||||
| } | |||||
| } | } | ||||
| @@ -1,23 +0,0 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {HttpClient, HttpHeaders} from "@angular/common/http"; | |||||
| import {User} from "@app/_models"; | |||||
| import {environment} from "@environments/environment"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class LogService { | |||||
| constructor( | |||||
| private httpClient: HttpClient | |||||
| ) { | |||||
| } | |||||
| getLogs(lines: number) { | |||||
| return this.httpClient.get( | |||||
| `${environment.apiUrl}/logs/logs/${lines}`, | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -3,14 +3,12 @@ import { Routes, RouterModule } from '@angular/router'; | |||||
| import { LayoutComponent } from './layout.component'; | import { LayoutComponent } from './layout.component'; | ||||
| import { LoginComponent } from './login.component'; | import { LoginComponent } from './login.component'; | ||||
| import { RegisterComponent } from './register.component'; | |||||
| const routes: Routes = [ | const routes: Routes = [ | ||||
| { | { | ||||
| path: '', component: LayoutComponent, | path: '', component: LayoutComponent, | ||||
| children: [ | children: [ | ||||
| { path: 'login', component: LoginComponent }, | { path: 'login', component: LoginComponent }, | ||||
| { path: 'register', component: RegisterComponent } | |||||
| ] | ] | ||||
| } | } | ||||
| ]; | ]; | ||||
| @@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common'; | |||||
| import { AccountRoutingModule } from './account-routing.module'; | import { AccountRoutingModule } from './account-routing.module'; | ||||
| import { LayoutComponent } from './layout.component'; | import { LayoutComponent } from './layout.component'; | ||||
| import { LoginComponent } from './login.component'; | import { LoginComponent } from './login.component'; | ||||
| import { RegisterComponent } from './register.component'; | |||||
| @NgModule({ | @NgModule({ | ||||
| imports: [ | imports: [ | ||||
| @@ -16,7 +15,6 @@ import { RegisterComponent } from './register.component'; | |||||
| declarations: [ | declarations: [ | ||||
| LayoutComponent, | LayoutComponent, | ||||
| LoginComponent, | LoginComponent, | ||||
| RegisterComponent | |||||
| ] | ] | ||||
| }) | }) | ||||
| export class AccountModule { } | export class AccountModule { } | ||||
| @@ -1,43 +0,0 @@ | |||||
| <div class="card"> | |||||
| <h4 class="card-header">Register</h4> | |||||
| <div class="card-body"> | |||||
| <form [formGroup]="form" (ngSubmit)="onSubmit()"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">First Name</label> | |||||
| <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" /> | |||||
| <div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['firstName'].hasError('required')">First Name is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Last Name</label> | |||||
| <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" /> | |||||
| <div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['lastName'].hasError('required')">Last Name is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Username</label> | |||||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Password</label> | |||||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||||
| <div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div> | |||||
| </div> | |||||
| </div> | |||||
| <div> | |||||
| <button [disabled]="loading" class="btn btn-primary"> | |||||
| <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span> | |||||
| Register | |||||
| </button> | |||||
| <a routerLink="../login" class="btn btn-link">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| @@ -1,59 +0,0 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | |||||
| import { Router, ActivatedRoute } from '@angular/router'; | |||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||||
| import { first } from 'rxjs/operators'; | |||||
| import { AccountService, AlertService } from '@app/_services'; | |||||
| @Component({ templateUrl: 'register.component.html' }) | |||||
| export class RegisterComponent implements OnInit { | |||||
| form!: FormGroup; | |||||
| loading = false; | |||||
| submitted = false; | |||||
| constructor( | |||||
| private formBuilder: FormBuilder, | |||||
| private route: ActivatedRoute, | |||||
| private router: Router, | |||||
| private accountService: AccountService, | |||||
| private alertService: AlertService | |||||
| ) { } | |||||
| ngOnInit() { | |||||
| this.form = this.formBuilder.group({ | |||||
| firstName: ['', Validators.required], | |||||
| lastName: ['', Validators.required], | |||||
| username: ['', Validators.required], | |||||
| password: ['', [Validators.required, Validators.minLength(6)]] | |||||
| }); | |||||
| } | |||||
| // convenience getter for easy access to form fields | |||||
| get f() { return this.form.controls; } | |||||
| onSubmit() { | |||||
| this.submitted = true; | |||||
| // reset alerts on submit | |||||
| this.alertService.clear(); | |||||
| // stop here if form is invalid | |||||
| if (this.form.invalid) { | |||||
| return; | |||||
| } | |||||
| this.loading = true; | |||||
| this.accountService.register(this.form.value) | |||||
| .pipe(first()) | |||||
| .subscribe({ | |||||
| next: () => { | |||||
| this.alertService.success('Registration successful', { keepAfterRouteChange: true }); | |||||
| this.router.navigate(['../login'], { relativeTo: this.route }); | |||||
| }, | |||||
| error: error => { | |||||
| this.alertService.error(error); | |||||
| this.loading = false; | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| @@ -1,5 +1,4 @@ | |||||
| import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core'; | import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core'; | ||||
| import {LogService} from "@app/_services/log.service"; | |||||
| import {FormControl} from "@angular/forms"; | import {FormControl} from "@angular/forms"; | ||||
| @Component({ | @Component({ | ||||
| @@ -1,8 +1,8 @@ | |||||
| import {AfterViewInit, Component, OnInit} from '@angular/core'; | import {AfterViewInit, Component, OnInit} from '@angular/core'; | ||||
| import {User} from '@app/_models'; | |||||
| import {AccountService} from '@app/_services'; | import {AccountService} from '@app/_services'; | ||||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | import {AppHelperService} from "@app/_helpers/app-helper.service"; | ||||
| import {UserJsonld} from "@app/core/api/v1"; | |||||
| @Component({ | @Component({ | ||||
| templateUrl: 'home.component.html', | templateUrl: 'home.component.html', | ||||
| @@ -10,7 +10,7 @@ import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||||
| }) | }) | ||||
| export class HomeComponent implements OnInit, AfterViewInit { | export class HomeComponent implements OnInit, AfterViewInit { | ||||
| protected user: User | null; | |||||
| protected user: UserJsonld | null; | |||||
| constructor( | constructor( | ||||
| private accountService: AccountService, | private accountService: AccountService, | ||||
| @@ -49,8 +49,8 @@ export class UserDetailComponent implements OnInit, AfterViewInit { | |||||
| } | } | ||||
| setIsCurrentUser() { | setIsCurrentUser() { | ||||
| if (this.accountService.userValue?.userResource) { | |||||
| let user = this.accountService.userValue?.userResource; | |||||
| if (this.accountService.userValue) { | |||||
| let user = this.accountService.userValue; | |||||
| this.isCurrentUser = this.appHelperService.extractId(this.user.id) == user?.id; | this.isCurrentUser = this.appHelperService.extractId(this.user.id) == user?.id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,11 +1,11 @@ | |||||
| import {ChangeDetectorRef, Component, Inject, OnInit, Renderer2} from '@angular/core'; | import {ChangeDetectorRef, Component, Inject, OnInit, Renderer2} from '@angular/core'; | ||||
| import {AccountService} from './_services'; | import {AccountService} from './_services'; | ||||
| import {User} from './_models'; | |||||
| import {TranslateService} from "@ngx-translate/core"; | import {TranslateService} from "@ngx-translate/core"; | ||||
| import {environment} from "@environments/environment"; | import {environment} from "@environments/environment"; | ||||
| import {Subscription} from "rxjs"; | import {Subscription} from "rxjs"; | ||||
| import {LoadingService} from "@app/_services/loading.service"; | import {LoadingService} from "@app/_services/loading.service"; | ||||
| import {DOCUMENT} from "@angular/common"; | import {DOCUMENT} from "@angular/common"; | ||||
| import {UserJsonld} from "@app/core/api/v1"; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-root', | selector: 'app-root', | ||||
| @@ -14,7 +14,7 @@ import {DOCUMENT} from "@angular/common"; | |||||
| }) | }) | ||||
| export class AppComponent implements OnInit { | export class AppComponent implements OnInit { | ||||
| protected readonly environment = environment; | protected readonly environment = environment; | ||||
| protected user?: User | null; | |||||
| protected user?: UserJsonld | null; | |||||
| protected loadingSub: Subscription; | protected loadingSub: Subscription; | ||||
| protected loading: boolean = false; | protected loading: boolean = false; | ||||
| protected isLeftHanded = false; | protected isLeftHanded = false; | ||||
| @@ -55,7 +55,7 @@ export class AppComponent implements OnInit { | |||||
| // TODO: Hilfsfunktion - entfernen | // TODO: Hilfsfunktion - entfernen | ||||
| copyTokenToClipboard() { | copyTokenToClipboard() { | ||||
| const el = document.createElement('textarea'); | const el = document.createElement('textarea'); | ||||
| el.value = this.user?.token !== undefined ? this.user.token : ""; | |||||
| el.value = this.user?.token || ""; | |||||
| document.body.appendChild(el); | document.body.appendChild(el); | ||||
| el.select(); | el.select(); | ||||
| document.execCommand('copy'); | document.execCommand('copy'); | ||||
| @@ -33,7 +33,6 @@ model/apiUsersGetCollection200Response.ts | |||||
| model/apiVesselsGetCollection200Response.ts | model/apiVesselsGetCollection200Response.ts | ||||
| model/apiZonesGetCollection200Response.ts | model/apiZonesGetCollection200Response.ts | ||||
| model/authResponse.ts | model/authResponse.ts | ||||
| model/authResponseUser.ts | |||||
| model/credentials.ts | model/credentials.ts | ||||
| model/location.ts | model/location.ts | ||||
| model/locationJsonld.ts | model/locationJsonld.ts | ||||
| @@ -9,16 +9,10 @@ | |||||
| * https://openapi-generator.tech | * https://openapi-generator.tech | ||||
| * Do not edit the class manually. | * Do not edit the class manually. | ||||
| */ | */ | ||||
| import { AuthResponseUser } from './authResponseUser'; | |||||
| import { UserJsonld } from './userJsonld'; | |||||
| export interface AuthResponse { | export interface AuthResponse { | ||||
| readonly token?: string; | |||||
| readonly id?: number; | |||||
| readonly email?: string; | |||||
| readonly firstName?: string; | |||||
| readonly lastName?: string; | |||||
| roles?: Array<string>; | |||||
| user?: AuthResponseUser; | |||||
| readonly user?: UserJsonld; | |||||
| } | } | ||||
| @@ -9,9 +9,13 @@ | |||||
| * https://openapi-generator.tech | * https://openapi-generator.tech | ||||
| * Do not edit the class manually. | * Do not edit the class manually. | ||||
| */ | */ | ||||
| import { LocationJsonldContext } from './locationJsonldContext'; | |||||
| export interface AuthResponseUser { | export interface AuthResponseUser { | ||||
| context?: LocationJsonldContext; | |||||
| readonly id?: any | null; | |||||
| readonly type?: any | null; | |||||
| readonly dbId?: any | null; | readonly dbId?: any | null; | ||||
| email: any | null; | email: any | null; | ||||
| firstName: any | null; | firstName: any | null; | ||||
| @@ -24,6 +28,8 @@ export interface AuthResponseUser { | |||||
| */ | */ | ||||
| password?: any | null; | password?: any | null; | ||||
| active?: any | null; | active?: any | null; | ||||
| roles?: any | null; | |||||
| readonly token?: any | null; | |||||
| readonly createdAt?: any | null; | readonly createdAt?: any | null; | ||||
| } | } | ||||
| @@ -13,7 +13,6 @@ export * from './apiUsersGetCollection200Response'; | |||||
| export * from './apiVesselsGetCollection200Response'; | export * from './apiVesselsGetCollection200Response'; | ||||
| export * from './apiZonesGetCollection200Response'; | export * from './apiZonesGetCollection200Response'; | ||||
| export * from './authResponse'; | export * from './authResponse'; | ||||
| export * from './authResponseUser'; | |||||
| export * from './credentials'; | export * from './credentials'; | ||||
| export * from './location'; | export * from './location'; | ||||
| export * from './locationJsonld'; | export * from './locationJsonld'; | ||||
| @@ -27,6 +27,8 @@ export interface User { | |||||
| */ | */ | ||||
| password?: string; | password?: string; | ||||
| active?: boolean; | active?: boolean; | ||||
| roles?: Array<string>; | |||||
| readonly token?: string | null; | |||||
| readonly createdAt?: string | null; | readonly createdAt?: string | null; | ||||
| } | } | ||||
| @@ -31,6 +31,8 @@ export interface UserJsonld { | |||||
| */ | */ | ||||
| password?: string; | password?: string; | ||||
| active?: boolean; | active?: boolean; | ||||
| roles?: Array<string>; | |||||
| readonly token?: string | null; | |||||
| readonly createdAt?: string | null; | readonly createdAt?: string | null; | ||||
| } | } | ||||
| @@ -24,6 +24,7 @@ | |||||
| "symfony/flex": "^2", | "symfony/flex": "^2", | ||||
| "symfony/form": "7.1.*", | "symfony/form": "7.1.*", | ||||
| "symfony/framework-bundle": "7.1.*", | "symfony/framework-bundle": "7.1.*", | ||||
| "symfony/http-client": "7.1.*", | |||||
| "symfony/property-access": "7.1.*", | "symfony/property-access": "7.1.*", | ||||
| "symfony/property-info": "7.1.*", | "symfony/property-info": "7.1.*", | ||||
| "symfony/runtime": "7.1.*", | "symfony/runtime": "7.1.*", | ||||
| @@ -4,7 +4,7 @@ | |||||
| "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||
| "This file is @generated automatically" | "This file is @generated automatically" | ||||
| ], | ], | ||||
| "content-hash": "85ee4ff0a9e8228f5ef5dae1cf910077", | |||||
| "content-hash": "1d97e4496ebfcc30dce08cc0102bd47e", | |||||
| "packages": [ | "packages": [ | ||||
| { | { | ||||
| "name": "api-platform/doctrine-common", | "name": "api-platform/doctrine-common", | ||||
| @@ -4876,6 +4876,178 @@ | |||||
| ], | ], | ||||
| "time": "2024-09-20T13:35:23+00:00" | "time": "2024-09-20T13:35:23+00:00" | ||||
| }, | }, | ||||
| { | |||||
| "name": "symfony/http-client", | |||||
| "version": "v7.1.10", | |||||
| "source": { | |||||
| "type": "git", | |||||
| "url": "https://github.com/symfony/http-client.git", | |||||
| "reference": "e221bfd51e70f12568e2f84890e6a5ffed0c35c7" | |||||
| }, | |||||
| "dist": { | |||||
| "type": "zip", | |||||
| "url": "https://api.github.com/repos/symfony/http-client/zipball/e221bfd51e70f12568e2f84890e6a5ffed0c35c7", | |||||
| "reference": "e221bfd51e70f12568e2f84890e6a5ffed0c35c7", | |||||
| "shasum": "" | |||||
| }, | |||||
| "require": { | |||||
| "php": ">=8.2", | |||||
| "psr/log": "^1|^2|^3", | |||||
| "symfony/deprecation-contracts": "^2.5|^3", | |||||
| "symfony/http-client-contracts": "~3.4.4|^3.5.2", | |||||
| "symfony/service-contracts": "^2.5|^3" | |||||
| }, | |||||
| "conflict": { | |||||
| "php-http/discovery": "<1.15", | |||||
| "symfony/http-foundation": "<6.4" | |||||
| }, | |||||
| "provide": { | |||||
| "php-http/async-client-implementation": "*", | |||||
| "php-http/client-implementation": "*", | |||||
| "psr/http-client-implementation": "1.0", | |||||
| "symfony/http-client-implementation": "3.0" | |||||
| }, | |||||
| "require-dev": { | |||||
| "amphp/amp": "^2.5", | |||||
| "amphp/http-client": "^4.2.1", | |||||
| "amphp/http-tunnel": "^1.0", | |||||
| "amphp/socket": "^1.1", | |||||
| "guzzlehttp/promises": "^1.4|^2.0", | |||||
| "nyholm/psr7": "^1.0", | |||||
| "php-http/httplug": "^1.0|^2.0", | |||||
| "psr/http-client": "^1.0", | |||||
| "symfony/dependency-injection": "^6.4|^7.0", | |||||
| "symfony/http-kernel": "^6.4|^7.0", | |||||
| "symfony/messenger": "^6.4|^7.0", | |||||
| "symfony/process": "^6.4|^7.0", | |||||
| "symfony/rate-limiter": "^6.4|^7.0", | |||||
| "symfony/stopwatch": "^6.4|^7.0" | |||||
| }, | |||||
| "type": "library", | |||||
| "autoload": { | |||||
| "psr-4": { | |||||
| "Symfony\\Component\\HttpClient\\": "" | |||||
| }, | |||||
| "exclude-from-classmap": [ | |||||
| "/Tests/" | |||||
| ] | |||||
| }, | |||||
| "notification-url": "https://packagist.org/downloads/", | |||||
| "license": [ | |||||
| "MIT" | |||||
| ], | |||||
| "authors": [ | |||||
| { | |||||
| "name": "Nicolas Grekas", | |||||
| "email": "p@tchwork.com" | |||||
| }, | |||||
| { | |||||
| "name": "Symfony Community", | |||||
| "homepage": "https://symfony.com/contributors" | |||||
| } | |||||
| ], | |||||
| "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", | |||||
| "homepage": "https://symfony.com", | |||||
| "keywords": [ | |||||
| "http" | |||||
| ], | |||||
| "support": { | |||||
| "source": "https://github.com/symfony/http-client/tree/v7.1.10" | |||||
| }, | |||||
| "funding": [ | |||||
| { | |||||
| "url": "https://symfony.com/sponsor", | |||||
| "type": "custom" | |||||
| }, | |||||
| { | |||||
| "url": "https://github.com/fabpot", | |||||
| "type": "github" | |||||
| }, | |||||
| { | |||||
| "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", | |||||
| "type": "tidelift" | |||||
| } | |||||
| ], | |||||
| "time": "2024-12-30T18:35:03+00:00" | |||||
| }, | |||||
| { | |||||
| "name": "symfony/http-client-contracts", | |||||
| "version": "v3.5.2", | |||||
| "source": { | |||||
| "type": "git", | |||||
| "url": "https://github.com/symfony/http-client-contracts.git", | |||||
| "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" | |||||
| }, | |||||
| "dist": { | |||||
| "type": "zip", | |||||
| "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", | |||||
| "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", | |||||
| "shasum": "" | |||||
| }, | |||||
| "require": { | |||||
| "php": ">=8.1" | |||||
| }, | |||||
| "type": "library", | |||||
| "extra": { | |||||
| "thanks": { | |||||
| "url": "https://github.com/symfony/contracts", | |||||
| "name": "symfony/contracts" | |||||
| }, | |||||
| "branch-alias": { | |||||
| "dev-main": "3.5-dev" | |||||
| } | |||||
| }, | |||||
| "autoload": { | |||||
| "psr-4": { | |||||
| "Symfony\\Contracts\\HttpClient\\": "" | |||||
| }, | |||||
| "exclude-from-classmap": [ | |||||
| "/Test/" | |||||
| ] | |||||
| }, | |||||
| "notification-url": "https://packagist.org/downloads/", | |||||
| "license": [ | |||||
| "MIT" | |||||
| ], | |||||
| "authors": [ | |||||
| { | |||||
| "name": "Nicolas Grekas", | |||||
| "email": "p@tchwork.com" | |||||
| }, | |||||
| { | |||||
| "name": "Symfony Community", | |||||
| "homepage": "https://symfony.com/contributors" | |||||
| } | |||||
| ], | |||||
| "description": "Generic abstractions related to HTTP clients", | |||||
| "homepage": "https://symfony.com", | |||||
| "keywords": [ | |||||
| "abstractions", | |||||
| "contracts", | |||||
| "decoupling", | |||||
| "interfaces", | |||||
| "interoperability", | |||||
| "standards" | |||||
| ], | |||||
| "support": { | |||||
| "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" | |||||
| }, | |||||
| "funding": [ | |||||
| { | |||||
| "url": "https://symfony.com/sponsor", | |||||
| "type": "custom" | |||||
| }, | |||||
| { | |||||
| "url": "https://github.com/fabpot", | |||||
| "type": "github" | |||||
| }, | |||||
| { | |||||
| "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", | |||||
| "type": "tidelift" | |||||
| } | |||||
| ], | |||||
| "time": "2024-12-07T08:49:48+00:00" | |||||
| }, | |||||
| { | { | ||||
| "name": "symfony/http-foundation", | "name": "symfony/http-foundation", | ||||
| "version": "v7.1.5", | "version": "v7.1.5", | ||||
| @@ -8644,7 +8816,7 @@ | |||||
| ], | ], | ||||
| "aliases": [], | "aliases": [], | ||||
| "minimum-stability": "stable", | "minimum-stability": "stable", | ||||
| "stability-flags": [], | |||||
| "stability-flags": {}, | |||||
| "prefer-stable": true, | "prefer-stable": true, | ||||
| "prefer-lowest": false, | "prefer-lowest": false, | ||||
| "platform": { | "platform": { | ||||
| @@ -8652,6 +8824,6 @@ | |||||
| "ext-ctype": "*", | "ext-ctype": "*", | ||||
| "ext-iconv": "*" | "ext-iconv": "*" | ||||
| }, | }, | ||||
| "platform-dev": [], | |||||
| "platform-dev": {}, | |||||
| "plugin-api-version": "2.6.0" | "plugin-api-version": "2.6.0" | ||||
| } | } | ||||
| @@ -94,6 +94,12 @@ class UserApi | |||||
| #[ApiProperty(security: 'object === null or is_granted("EDIT", object)')] | #[ApiProperty(security: 'object === null or is_granted("EDIT", object)')] | ||||
| public bool $active; | public bool $active; | ||||
| #[ApiProperty(writable: false)] | |||||
| public array $roles = []; | |||||
| #[ApiProperty(writable: false)] | |||||
| public ?string $token = null; | |||||
| #[ApiProperty(writable: false)] | #[ApiProperty(writable: false)] | ||||
| public ?\DateTimeImmutable $createdAt = null; | public ?\DateTimeImmutable $createdAt = null; | ||||
| @@ -42,6 +42,7 @@ class UserEntityToApiMapper implements MapperInterface | |||||
| $dto->firstName = $entity->getFirstName(); | $dto->firstName = $entity->getFirstName(); | ||||
| $dto->lastName = $entity->getLastName(); | $dto->lastName = $entity->getLastName(); | ||||
| $dto->image = $entity->getImage(); | $dto->image = $entity->getImage(); | ||||
| $dto->roles = $entity->getRoles(); | |||||
| $dto->imageUrl = $this->fileUrlService->getFileUrl($entity->getImage()); | $dto->imageUrl = $this->fileUrlService->getFileUrl($entity->getImage()); | ||||
| $dto->fullName = $entity->getFirstName() . " " . $entity->getLastName(); | $dto->fullName = $entity->getFirstName() . " " . $entity->getLastName(); | ||||
| $dto->createdAt = $entity->getCreatedAt(); | $dto->createdAt = $entity->getCreatedAt(); | ||||
| @@ -38,41 +38,11 @@ final class AuthDecorator implements OpenApiFactoryInterface | |||||
| $schemas['AuthResponse'] = new \ArrayObject([ | $schemas['AuthResponse'] = new \ArrayObject([ | ||||
| 'type' => 'object', | 'type' => 'object', | ||||
| 'properties' => [ | 'properties' => [ | ||||
| 'token' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'id' => [ | |||||
| 'type' => 'integer', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'email' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'firstName' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'lastName' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'roles' => [ | |||||
| 'type' => 'array', | |||||
| 'items' => [ | |||||
| 'type' => 'string' | |||||
| ], | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'user' => [ | 'user' => [ | ||||
| 'type' => 'object', | |||||
| 'allOf' => [ | |||||
| ['$ref' => '#/components/schemas/User'] | |||||
| ], | |||||
| '$ref' => '#/components/schemas/User.jsonld', | |||||
| 'readOnly' => true, | 'readOnly' => true, | ||||
| ], | |||||
| ], | |||||
| ] | |||||
| ] | |||||
| ]); | ]); | ||||
| $pathItem = new PathItem( | $pathItem = new PathItem( | ||||
| @@ -2,8 +2,14 @@ | |||||
| // src/Security/JwtAuthenticator.php | // src/Security/JwtAuthenticator.php | ||||
| namespace App\Security; | namespace App\Security; | ||||
| use ApiPlatform\Metadata\Get; | |||||
| use ApiPlatform\Metadata\IriConverterInterface; | |||||
| use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; | |||||
| use ApiPlatform\State\SerializerContextBuilderInterface; | |||||
| use App\ApiResource\UserApi; | use App\ApiResource\UserApi; | ||||
| use App\Entity\User; | |||||
| use App\Repository\UserRepository; | use App\Repository\UserRepository; | ||||
| use App\State\EntityToDtoStateProvider; | |||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | use Symfony\Component\HttpFoundation\Request; | ||||
| @@ -15,14 +21,19 @@ use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | |||||
| use Symfony\Component\Serializer\SerializerInterface; | |||||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | |||||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | use Symfonycasts\MicroMapper\MicroMapperInterface; | ||||
| class JwtAuthenticator extends AbstractAuthenticator | class JwtAuthenticator extends AbstractAuthenticator | ||||
| { | { | ||||
| public function __construct( | public function __construct( | ||||
| private JWTTokenManagerInterface $jwtManager, | private JWTTokenManagerInterface $jwtManager, | ||||
| private UserRepository $userRepository, | |||||
| private MicroMapperInterface $microMapper | |||||
| private MicroMapperInterface $microMapper, | |||||
| private HttpClientInterface $httpClient, | |||||
| private SerializerInterface $serializer, | |||||
| private SerializerContextBuilderInterface $serializerContextBuilder, | |||||
| ) {} | ) {} | ||||
| public function supports(Request $request): ?bool | public function supports(Request $request): ?bool | ||||
| @@ -49,21 +60,22 @@ class JwtAuthenticator extends AbstractAuthenticator | |||||
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | ||||
| { | { | ||||
| /** @var User $user */ | |||||
| $user = $token->getUser(); | $user = $token->getUser(); | ||||
| $userApi = $this->microMapper->map($user, UserApi::class); | |||||
| $jwtToken = $this->jwtManager->create($user); | |||||
| // NOTE: This is a necessary workaround, since it is ot possible to map this in the usual api platform style at this point | |||||
| $userApiArray = []; | |||||
| $userApiArray['@id'] = '/api/users/' . $user->getId(); | |||||
| $userApiArray['@type'] = 'User'; | |||||
| foreach (get_object_vars($userApi) as $property => $value) { | |||||
| if ($property !== 'id') { | |||||
| $userApiArray[$property] = $value; | |||||
| } | |||||
| } | |||||
| $userApiArray['token'] = $this->jwtManager->create($user); | |||||
| return new JsonResponse([ | |||||
| 'token' => $jwtToken, | |||||
| 'id' => $user->getId(), | |||||
| 'email' => $user->getEmail(), | |||||
| 'firstName' => $user->getFirstName(), | |||||
| 'lastName' => $user->getLastName(), | |||||
| 'roles' => $user->getRoles(), | |||||
| 'user' => $this->microMapper->map( | |||||
| $user, UserApi::class | |||||
| ) | |||||
| ]); | |||||
| return new JsonResponse(['user' => $userApiArray]); | |||||
| } | } | ||||
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | ||||