Merge branch 'dev/api/feature' into 'develop'
Dev/api/feature See merge request easy-solutions/apps/easyportal!33
This commit is contained in:
commit
698caebaea
5
.env
5
.env
|
|
@ -48,6 +48,8 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key
|
|||
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key
|
||||
OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4
|
||||
OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066
|
||||
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
|
||||
OAUTH_SSO_IDENTIFIER_LOGIN='sso-own-identifier'
|
||||
###< league/oauth2-server-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
|
|
@ -71,4 +73,5 @@ AWS_REGION=us-east-1
|
|||
AWS_ENDPOINT=https://s3.amazonaws.com
|
||||
AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal
|
||||
###< aws/aws-sdk-php-symfony ###
|
||||
APP_URL='https://example.com'
|
||||
APP_URL='https://example.com'
|
||||
APP_DOMAIN='example.com'
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
async fetchAndRenderApplications(targetElement) {
|
||||
try {
|
||||
const response = await fetch('/application/data/all');
|
||||
const apps = await response.json();
|
||||
|
||||
targetElement.innerHTML = apps.map(app => `
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
|
||||
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
|
||||
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
|
||||
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
|
||||
${app.name}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return apps;
|
||||
} catch (error) {
|
||||
targetElement.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
|
||||
console.error("App load error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ export default class extends Controller {
|
|||
orgId: Number,
|
||||
admin: Boolean
|
||||
}
|
||||
static targets = ["modal", "appList", "nameInput", "formTitle"];
|
||||
static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"];
|
||||
connect(){
|
||||
if(this.listProjectValue){
|
||||
this.table();
|
||||
|
|
@ -137,35 +137,44 @@ export default class extends Controller {
|
|||
|
||||
async submitForm(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
const form = event.target;
|
||||
const formData = new FormData(form); // This automatically picks up the 'logo' file
|
||||
|
||||
const payload = {
|
||||
organizationId: this.orgIdValue,
|
||||
applications: formData.getAll('applications[]')
|
||||
};
|
||||
|
||||
// Only include name if it wasn't disabled (new projects)
|
||||
if (!this.nameInputTarget.disabled) {
|
||||
payload.name = formData.get('name');
|
||||
// 1. Validate File Format
|
||||
const logoFile = formData.get('logo');
|
||||
if (logoFile && logoFile.size > 0) {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (!allowedTypes.includes(logoFile.type)) {
|
||||
alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG.");
|
||||
return; // Stop submission
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Prepare for Multipart sending
|
||||
// Since we are using FormData, we don't need JSON.stringify or 'Content-Type': 'application/json'
|
||||
// We add the extra fields to the formData object
|
||||
formData.append('organizationId', this.orgIdValue);
|
||||
|
||||
const url = this.currentProjectId
|
||||
? `/project/edit/${this.currentProjectId}/ajax`
|
||||
: `/project/new/ajax`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
// IMPORTANT: Do NOT set Content-Type header when sending FormData with files
|
||||
// The browser will set 'multipart/form-data' and the boundary automatically
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.modal.hide();
|
||||
// Use Tabulator's setData() instead of reload() for better UX if possible
|
||||
location.reload();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
if (response.status === 409) {
|
||||
alert("Un projet avec ce nom existe déjà. Veuillez choisir un nom différent.");
|
||||
alert("Un projet avec ce nom existe déjà.");
|
||||
} else {
|
||||
alert(result.error || "Une erreur est survenue.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -188,6 +197,9 @@ export default class extends Controller {
|
|||
|
||||
// 3. Set the name
|
||||
this.nameInputTarget.value = project.name;
|
||||
console.log(project);
|
||||
this.timestampSelectTarget.value = project.timestampPrecision;
|
||||
this.deletionSelectTarget.value = project.deletionAllowed;
|
||||
|
||||
// 4. Check the boxes
|
||||
// We look for all checkboxes inside our appList target
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import {
|
|||
trashIconForm
|
||||
} from "../js/global.js";
|
||||
import { Modal } from "bootstrap";
|
||||
import base_controller from "./base_controller.js";
|
||||
|
||||
|
||||
export default class extends Controller {
|
||||
export default class extends base_controller {
|
||||
static values = {
|
||||
rolesArray: Array,
|
||||
selectedRoleIds: Array,
|
||||
|
|
@ -26,7 +27,7 @@ export default class extends Controller {
|
|||
orgId: Number
|
||||
}
|
||||
|
||||
static targets = ["select", "statusButton", "modal", "userSelect"];
|
||||
static targets = ["select", "statusButton", "modal", "userSelect", "appList", "emailInput", "phoneInput", "nameInput", "surnameInput"];
|
||||
|
||||
connect() {
|
||||
this.roleSelect();
|
||||
|
|
@ -1018,4 +1019,105 @@ export default class extends Controller {
|
|||
alert("Une erreur réseau est survenue.");
|
||||
}
|
||||
}
|
||||
|
||||
async openNewUserModal() {
|
||||
this.modal.show();
|
||||
// Call the shared logic and pass the target
|
||||
await this.fetchAndRenderApplications(this.appListTarget);
|
||||
}
|
||||
|
||||
async submitNewUser(event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const ucSurname = formData.get('surname').toUpperCase();
|
||||
formData.set('surname', ucSurname);
|
||||
|
||||
try {
|
||||
const response = await fetch('/user/new/ajax', { // Adjust path if prefix is different
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.modal.hide();
|
||||
form.reset(); // Clear the form
|
||||
location.reload();
|
||||
} else {
|
||||
alert("Erreur: " + (result.error || "Une erreur est survenue lors de la création."));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Erreur réseau.");
|
||||
}
|
||||
}
|
||||
|
||||
async openEditUserModal(event) {
|
||||
const userId = event.currentTarget.dataset.id;
|
||||
this.currentUserId = userId;
|
||||
this.modal.show();
|
||||
|
||||
try {
|
||||
// 1. Fetch all available apps using your shared base method
|
||||
await this.fetchAndRenderApplications(this.appListTarget);
|
||||
|
||||
// 2. Fetch specific user data WITH the orgId query parameter
|
||||
// We use this.orgIdValue which is mapped to data-user-org-id-value
|
||||
const response = await fetch(`/user/data/${userId}?orgId=${this.orgIdValue}`);
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch user data');
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
// 3. Fill text inputs
|
||||
this.emailInputTarget.value = user.email;
|
||||
this.phoneInputTarget.value = user.phoneNumber || '';
|
||||
this.nameInputTarget.value = user.name;
|
||||
this.surnameInputTarget.value = user.surname;
|
||||
|
||||
// 4. Check the application boxes
|
||||
const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]');
|
||||
|
||||
// Ensure we handle IDs as strings or numbers consistently
|
||||
const activeAppIds = user.applicationIds.map(id => id.toString());
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = activeAppIds.includes(cb.value.toString());
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des données utilisateur:", error);
|
||||
alert("Impossible de charger les informations de l'utilisateur.");
|
||||
}
|
||||
}
|
||||
|
||||
async submitEditUser(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
// Force Uppercase on Surname as requested
|
||||
formData.set('surname', formData.get('surname').toUpperCase());
|
||||
|
||||
try {
|
||||
const response = await fetch(`/user/edit/${this.currentUserId}/ajax`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.modal.hide();
|
||||
location.reload();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
alert(result.error || "Erreur lors de la mise à jour.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Erreur réseau.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566c.174.099.321.198.44.286c.119-.088.266-.187.44-.286A4.17 4.17 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.5 3.5 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294c.387.221.926.434 1.564.434a.5.5 0 0 1 0 1a4.17 4.17 0 0 1-2.06-.566A5 5 0 0 1 8 13.65a5 5 0 0 1-.44.285a4.17 4.17 0 0 1-2.06.566a.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434c.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.5 3.5 0 0 0-.436-.294A3.17 3.17 0 0 0 5.5 2.5A.5.5 0 0 1 5 2"/><path d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 827 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
|
||||
|
After Width: | Height: | Size: 609 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm0-2h16V6H4zm0 0V6zm8-5h6q.425 0 .713-.288T19 12V8q0-.425-.288-.712T18 7h-6q-.425 0-.712.288T11 8v4q0 .425.288.713T12 13m1-2V9h4v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
|
|
@ -22,7 +22,7 @@ export const TABULATOR_FR_LANG = {
|
|||
};
|
||||
|
||||
export function eyeIconLink(url) {
|
||||
return `<a href="${url}" class="p-3 align-middle color-primary" title="Accéder au profil" target="_blank">
|
||||
return `<a href="${url}" class="p-3 align-middle color-primary" title="Accéder au profil" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="35px"
|
||||
height="35px"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ security:
|
|||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
api_token_validation:
|
||||
pattern: ^/api/validate-token
|
||||
stateless: true
|
||||
oauth2: true
|
||||
oauth_userinfo:
|
||||
pattern: ^/oauth2/userinfo
|
||||
stateless: true
|
||||
|
|
@ -30,6 +34,10 @@ security:
|
|||
auth_token:
|
||||
pattern: ^/token
|
||||
stateless: true
|
||||
api_m2m:
|
||||
pattern: ^/api/v1/
|
||||
stateless: true
|
||||
oauth2: true
|
||||
api:
|
||||
pattern: ^/oauth/api
|
||||
security: true
|
||||
|
|
@ -65,6 +73,7 @@ security:
|
|||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/password_setup, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/password_reset, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
|
@ -76,8 +85,6 @@ security:
|
|||
- { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY }
|
||||
- { path: ^/, roles: ROLE_USER }
|
||||
|
||||
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ parameters:
|
|||
aws_public_url: '%env(AWS_ENDPOINT)%'
|
||||
aws_bucket: '%env(S3_PORTAL_BUCKET)%'
|
||||
app_url: '%env(APP_URL)%'
|
||||
app_domain: '%env(APP_DOMAIN)%'
|
||||
mercure_secret: '%env(MERCURE_JWT_SECRET)%'
|
||||
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
||||
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
||||
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
|
@ -28,9 +31,16 @@ services:
|
|||
App\MessageHandler\NotificationMessageHandler:
|
||||
arguments:
|
||||
$appUrl: '%app_url%'
|
||||
App\Service\SSO\ProjectService:
|
||||
arguments:
|
||||
$appUrl: '%app_url%'
|
||||
$clientIdentifier: '%oauth_sso_identifier%'
|
||||
App\EventSubscriber\:
|
||||
resource: '../src/EventSubscriber/'
|
||||
tags: ['kernel.event_subscriber']
|
||||
App\EventSubscriber\LoginSubscriber:
|
||||
arguments:
|
||||
$clientIdentifier: '%oauth_sso_identifier_login%'
|
||||
App\Service\AwsService:
|
||||
arguments:
|
||||
$awsPublicUrl: '%aws_public_url%'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
# Intro
|
||||
|
||||
The Api made some changes to the current structure of the project. These changes include the following:
|
||||
- **Security** : the security.yaml file has been updated both on the server and the client side
|
||||
- **Controllers**: Controllers need to be updated on the client side
|
||||
- **Services**: Services and token management need updates on the client side
|
||||
- **Health**: I now want to commit a Talon E over a wall IRL
|
||||
- **Entities**: Entities need to be updated on the client side to include a new field (sso_id).
|
||||
- This field will be used a lot, so add it to an entity if you are going to work with the API and the SSO. PLEASE.
|
||||
- **Roles**: A new role was added because we are doing M2M. Only God know how that work now, but it works, so I might start praying from now on.
|
||||
|
||||
|
||||
# Security
|
||||
new firewall was added. Keep the same structure for future firewalls.
|
||||
### Client side
|
||||
```yaml
|
||||
api_project:
|
||||
pattern: ^/api/v1/project #ofc, this is an example, please THINK and change the name
|
||||
stateless: true
|
||||
access_token:
|
||||
token_handler: App\Security\SsoTokenHandler
|
||||
```
|
||||
Same thing, new firewall was added.
|
||||
### Server side
|
||||
```yaml
|
||||
api_token_validation:
|
||||
pattern: ^/api/validate-token #this is NOT an example. DON'T change or it will all go to sh.t
|
||||
stateless: true
|
||||
oauth2: true
|
||||
```
|
||||
```yaml
|
||||
# A rajouter dans l'access_control aussi !!! IMPORTANT !!!
|
||||
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
|
||||
```
|
||||
|
||||
# Controllers
|
||||
On the client side, create a new controller for the API. This controller need will work in a REST manner.
|
||||
The route should be as follows:
|
||||
```php
|
||||
#[Route('/api/v1/project', name: 'api_project')] //ofc, this is an example, please THINK and change the name
|
||||
```
|
||||
Keep the same structure for the project tree, create an Api folder in the controller, a V1 folder and then create your controller.
|
||||
Here is a full example of a controller with the create method.
|
||||
On crée une organization si elle n'existe pas
|
||||
```php
|
||||
<?php
|
||||
#[Route('/api/v2/project', name: 'api_project_')]
|
||||
#[IsGranted('ROLE_API_INTERNAL')]
|
||||
class ProjectApi extends AbstractController{
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/create', name: 'create', methods: ['POST'])]
|
||||
public function createProject(Request $request): JSONResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
$projet = new Projet();
|
||||
|
||||
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
|
||||
// Si l'entité n'existe pas, on la crée
|
||||
if(!$entity){
|
||||
$this->createEntity($data['orgId'], $data['orgName']);
|
||||
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
|
||||
}
|
||||
$precision= $data['timestamp'];
|
||||
$validPrecisions = array_map(fn($case) => $case->value, TimestampPrecision::cases());
|
||||
if (!in_array($precision, $validPrecisions, true)) {
|
||||
return $this->json(['success' => false, 'message' => 'Précision d\'horodatage invalide.'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$timestampPrecision = TimestampPrecision::from($precision);
|
||||
$projet->setTimestampPrecision($timestampPrecision);
|
||||
$projet->setProjet($data['name']);
|
||||
$projet->setEntityId($entity);
|
||||
$projet->setBdd($data['bdd']);
|
||||
$projet->setIsactive($data['isActive']);
|
||||
$projet->setLogo($data['logo']);
|
||||
$projet->setDeletionAllowed($data['deletion']);
|
||||
$projet->setSsoId($data['id']); // c'est l'id du projet dans le portail, pas la bdd local
|
||||
|
||||
$this->entityManager->persist($projet);
|
||||
$this->entityManager->flush();
|
||||
return new JsonResponse(['message' => 'Project created successfully', 'project_id' => $projet->getId()], 201);
|
||||
}catch ( \Exception $e){
|
||||
return new JsonResponse(['error' => 'Failed to create project: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Services
|
||||
So, now we are getting into the thick of it. We are just COOK 🍗 ( a lot of bugs can come from here ).
|
||||
We implement a new pretty service called SsoTokenHandler. This is used to get the token received from the portal request, and validate it.
|
||||
It is validaded by doing a call back to the SSO and asking if the token is valid. ( we create a new token for the SSO so it handles M2M)
|
||||
```php
|
||||
<?php
|
||||
|
||||
// src/Security/SsoTokenHandler.php
|
||||
namespace App\Security;
|
||||
|
||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUser;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class SsoTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private HttpClientInterface $httpClient,
|
||||
private string $ssoUrl
|
||||
) {}
|
||||
|
||||
public function getUserBadgeFrom(string $accessToken): UserBadge
|
||||
{
|
||||
// 1. Call the SSO to validate the token
|
||||
// Note: You need an endpoint on your SSO that returns 200 for M2M tokens
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $this->ssoUrl . '/api/validate-token', [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $accessToken]
|
||||
]);
|
||||
|
||||
// If the SSO redirects, HttpClient might follow it to the login page.
|
||||
// Let's see the first response code.
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
// Log the content to see the HTML "Login" page that is causing the JSON error
|
||||
// dump($response->getContent(false));
|
||||
throw new BadCredentialsException();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// This will show you if you are hitting a 302 Redirect
|
||||
throw new BadCredentialsException('SSO returned invalid response: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new BadCredentialsException('Invalid SSO Token');
|
||||
}
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
// 2. Identify if it's a User or a Machine
|
||||
$identifier = $data['email'] ?? 'SYSTEM_SSO_SERVER';
|
||||
|
||||
// 3. Return the badge with a "loader" closure
|
||||
return new UserBadge($identifier, function($userIdentifier) use ($data) {
|
||||
// If it's the SSO server calling, give it a specific role
|
||||
if ($userIdentifier === 'SYSTEM_SSO_SERVER') {
|
||||
return new InMemoryUser($userIdentifier, null, ['ROLE_API_INTERNAL']);
|
||||
}
|
||||
|
||||
// Otherwise, let the normal user provider handle it (for standard users)
|
||||
// You might need to inject your actual UserProvider here if needed
|
||||
return new InMemoryUser($userIdentifier, null, ['ROLE_USER']);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Important note 1
|
||||
we need to add the portal url to the .env and declare it as a parameter in services.yaml
|
||||
```dotenv
|
||||
SSO_URL='http://portail.solutions-easy.moi'
|
||||
```
|
||||
```yaml
|
||||
parameters:
|
||||
sso_url: '%env(SSO_URL)%'
|
||||
|
||||
App\Security\SsoTokenHandler:
|
||||
arguments:
|
||||
$ssoUrl: '%sso_url%'
|
||||
```
|
||||
# Server side
|
||||
On the server side, we need to create a new client, which will be himself, same as earlier, create it, and boom we are good.
|
||||
The validate route is already created, so dw abt it.
|
||||
|
||||
```cmd
|
||||
php bin/console league:oauth2-server:create-client sso_internal_service --grant-type "client_credentials"
|
||||
```
|
||||
|
||||
now, copy the identifier, and paste it in the .env file
|
||||
please note that we have 2 client for the application because one is used for m2m and the other is used for the user, so implement both, the one ending with _LOGIN is the one for the user
|
||||
```dotenv
|
||||
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
|
||||
```
|
||||
and we are smart so what do we do? we add it to the services.yaml
|
||||
```yaml
|
||||
parameters:
|
||||
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
||||
|
||||
App\Service\SSO\ProjectService:
|
||||
arguments:
|
||||
$appUrl: '%app_url%'
|
||||
$clientIdentifier: '%oauth_sso_identifier%'
|
||||
```
|
||||
|
||||
We should be good now ( I hope ). Open the portal, try your call and check if it works, if it doesn't, check the logs and debug, you are a dev for a reason, so use your brain and debug.
|
||||
If it still doesn't work, start praying, because I have no idea what to do anymore, but it works on my side, so it should work on yours, if not, well, I don't know what to say. Good luck.
|
||||
Jokes aside, bugs often come from security problem, if the client returns a 401 error, it can be for multiple reasons and not necessarily because of the token but maybe because of the token validation.
|
||||
Another commun bug is mismatching of the data you send, so double check. GLHF ( you won't have fun, but good luck anyway )
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
If you have a problem and that you copy pasted everything, check client controller, I've put the route as /api/v2 just cause I felt like it 🤷 ENJOY
|
||||
|
|
@ -178,18 +178,54 @@ class SsoAuthenticator extends OAuth2Authenticator implements AuthenticationEntr
|
|||
**/
|
||||
if (!$user) {
|
||||
$user = new User();
|
||||
$user->setEmail($sudalysSsoUser->getEmail());
|
||||
$user->setName($sudalysSsoUser->getName());
|
||||
$user->setSurname($sudalysSsoUser->getSurname());
|
||||
$user->setSsoId($sudalysSsoUser->getId());
|
||||
$user->setEmail($ssoData->getEmail());
|
||||
$user->setPrenom($ssoData->getName());
|
||||
$user->setNom($ssoData->getSurname());
|
||||
$user->setSsoId($ssoData->getId());
|
||||
$this->em->persist($user);
|
||||
}else{
|
||||
// On met a jour l'utilisateur
|
||||
$user->setEmail($sudalysSsoUser->getEmail());
|
||||
$user->setName($sudalysSsoUser->getName());
|
||||
$user->setSurname($sudalysSsoUser->getSurname());
|
||||
$user->setEmail($ssoData->getEmail());
|
||||
$user->setPrenom($ssoData->getName());
|
||||
$user->setNom($ssoData->getSurname());
|
||||
$this->em->persist($user);
|
||||
}
|
||||
|
||||
//handle UOs links
|
||||
$ssoArray = $ssoData->toArray();
|
||||
$uoData = $ssoArray['uos'] ?? [];
|
||||
foreach ($uoData as $uo) {
|
||||
$ssoOrgId = $uo['id'];
|
||||
|
||||
$entity = $this->em->getRepository(Entity::class)->findOneBy(['ssoId' => $ssoOrgId]);
|
||||
if (!$entity) {
|
||||
$entity = new Entity();
|
||||
$entity->setSsoId($ssoOrgId);
|
||||
$entity->setNom($uo['name']);
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
$role = $this->em->getRepository(Roles::class)->findOneBy(['name' => $uo['role']]);
|
||||
|
||||
// Check if the user-organization link already exists
|
||||
$existingLink = $this->em->getRepository(UsersOrganizations::class)->findOneBy([
|
||||
'users' => $user,
|
||||
'organizations' => $entity
|
||||
]);
|
||||
|
||||
if (!$existingLink) {
|
||||
// Create a new link if it doesn't exist
|
||||
$newLink = new UsersOrganizations();
|
||||
$newLink->setUsers($user);
|
||||
$newLink->setOrganizations($entity);
|
||||
$newLink->setRole($role);
|
||||
$this->em->persist($newLink);
|
||||
} else {
|
||||
// Update the role if the link already exists
|
||||
$existingLink->setRole($role);
|
||||
$existingLink->setModifiedAt(new \DateTimeImmutable());
|
||||
$this->em->persist($existingLink);
|
||||
}
|
||||
}
|
||||
$this->em->flush();
|
||||
return $user;
|
||||
}
|
||||
|
|
@ -320,8 +356,10 @@ If there is a scope or grand error, delete the client do the following first
|
|||
php bin/console league:oauth2-server:delete-client <identifier>
|
||||
```
|
||||
Identifier can be found in the database oauth2_client table
|
||||
The recreate the client and enter the scopes and grant types after creating the client directly in the db
|
||||
To recreate the client and enter the scopes and grant types after creating the client directly in the db
|
||||
```text
|
||||
scopes = email profile openid
|
||||
grants = authorization_code
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,4 +31,139 @@ Get Access to the following with the following authorisations:
|
|||
## Organizations Roles
|
||||
Organizations roles are specific to individual Organizations. They include:
|
||||
- **Organization Admin**: Has full access to all organization features and settings. Can manage users of the organizations.
|
||||
- **Organization User**: Has limited access to organization features and settings. Can view projects and applications, can manage own information
|
||||
- **Organization User**: Has limited access to organization features and settings. Can view projects and applications, can manage own information
|
||||
|
||||
|
||||
# Set up
|
||||
Like for the sso, we need to create roles in the system. create the following command and the create the roles.
|
||||
``` php
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-role',
|
||||
description: 'Creates a new role in the database'
|
||||
)]
|
||||
class CreateRoleCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role'); // role name required
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$roleName = trim($input->getArgument('name'));
|
||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||
|
||||
// Ensure not empty
|
||||
if ($roleName === '') {
|
||||
$output->writeln('<error>The role name cannot be empty</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Check if role already exists
|
||||
$existing = $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $roleName]);
|
||||
|
||||
if ($existing) {
|
||||
$output->writeln("<comment>Role '{$roleName}' already exists.</comment>");
|
||||
return Command::SUCCESS; // not failure, just redundant
|
||||
}
|
||||
|
||||
// Create and persist new role
|
||||
$role = new Roles();
|
||||
$role->setName($roleName);
|
||||
|
||||
$this->entityManager->persist($role);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$output->writeln("<info>Role '{$roleName}' created successfully!</info>");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
```
|
||||
```php
|
||||
#[AsCommand(
|
||||
name: 'app:delete-role',
|
||||
description: 'Deletes a role from the database'
|
||||
)]
|
||||
class DeleteRoleCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role to delete');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$roleName = trim($input->getArgument('name'));
|
||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||
|
||||
if ($roleName === '') {
|
||||
$output->writeln('<error>The role name cannot be empty</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Find the role
|
||||
$role = $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $roleName]);
|
||||
|
||||
if (!$role) {
|
||||
$output->writeln("<error>Role '{$roleName}' not found.</error>");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Check if role is being used (optional safety check)
|
||||
$usageCount = $this->entityManager->getRepository(\App\Entity\UsersOrganizations::class)
|
||||
->count(['role' => $role]);
|
||||
|
||||
if ($usageCount > 0) {
|
||||
$output->writeln("<error>Cannot delete role '{$roleName}' - it is assigned to {$usageCount} user(s).</error>");
|
||||
$output->writeln('<comment>Remove all assignments first, then try again.</comment>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Confirmation prompt
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ConfirmationQuestion(
|
||||
"Are you sure you want to delete role '{$roleName}'? [y/N] ",
|
||||
false
|
||||
);
|
||||
|
||||
if (!$helper->ask($input, $output, $question)) {
|
||||
$output->writeln('<comment>Operation cancelled.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Delete the role
|
||||
$this->entityManager->remove($role);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$output->writeln("<info>Role '{$roleName}' deleted successfully!</info>");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
```
|
||||
``` bash
|
||||
php bin/console app:create-role USER
|
||||
php bin/console app:create-role ADMIN
|
||||
```
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260216155056 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP SEQUENCE user_organizaton_app_id_seq CASCADE');
|
||||
$this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, user_organization_id INT NOT NULL, application_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_BEF66DF12014CF51 ON user_organization_app (user_organization_id)');
|
||||
$this->addSql('CREATE INDEX IDX_BEF66DF13E030ACD ON user_organization_app (application_id)');
|
||||
$this->addSql('COMMENT ON COLUMN user_organization_app.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF12014CF51 FOREIGN KEY (user_organization_id) REFERENCES users_organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF13E030ACD FOREIGN KEY (application_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc72014cf51');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc73e030acd');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc7d60322ac');
|
||||
$this->addSql('DROP TABLE user_organizaton_app');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('CREATE SEQUENCE user_organizaton_app_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE user_organizaton_app (id SERIAL NOT NULL, user_organization_id INT NOT NULL, role_id INT DEFAULT NULL, application_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX idx_2c952fc72014cf51 ON user_organizaton_app (user_organization_id)');
|
||||
$this->addSql('CREATE INDEX idx_2c952fc73e030acd ON user_organizaton_app (application_id)');
|
||||
$this->addSql('CREATE INDEX idx_2c952fc7d60322ac ON user_organizaton_app (role_id)');
|
||||
$this->addSql('COMMENT ON COLUMN user_organizaton_app.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc72014cf51 FOREIGN KEY (user_organization_id) REFERENCES users_organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc73e030acd FOREIGN KEY (application_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc7d60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF12014CF51');
|
||||
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF13E030ACD');
|
||||
$this->addSql('DROP TABLE user_organization_app');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260218111608 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE project ADD logo VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('ALTER TABLE project DROP logo');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260218111821 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE project ADD timestamp_precision VARCHAR(10) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE project ADD deletion_allowed BOOLEAN DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE project ADD audits_enabled BOOLEAN DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('ALTER TABLE project DROP timestamp_precision');
|
||||
$this->addSql('ALTER TABLE project DROP deletion_allowed');
|
||||
$this->addSql('ALTER TABLE project DROP audits_enabled');
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ class DeleteRoleCommand extends Command
|
|||
}
|
||||
|
||||
// Check if role is being used (optional safety check)
|
||||
$usageCount = $this->entityManager->getRepository(\App\Entity\UserOrganizatonApp::class)
|
||||
$usageCount = $this->entityManager->getRepository(\App\Entity\UsersOrganizations::class)
|
||||
->count(['role' => $role]);
|
||||
|
||||
if ($usageCount > 0) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\AccessTokenService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\UserService;
|
||||
|
|
@ -20,18 +21,39 @@ class OAuth2Controller extends AbstractController
|
|||
{
|
||||
|
||||
|
||||
public function __construct(private readonly LoggerService $loggerService, private readonly UserService $userService)
|
||||
public function __construct(private readonly LoggerService $loggerService, private readonly UserService $userService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/oauth2/userinfo', name: 'userinfo', methods: ['GET'])]
|
||||
public function userinfo(Request $request): JsonResponse
|
||||
public function userinfo(Request $request, UsersOrganizationsRepository $uoRepository): JsonResponse
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if (!$user) {
|
||||
$this->loggerService->logAccessDenied($user->getId());
|
||||
return new JsonResponse(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
$uos = $uoRepository->findBy(['users' => $user]);
|
||||
|
||||
$result = [];
|
||||
foreach ($uos as $uo) {
|
||||
$result[] = ['organization' => [
|
||||
'id' => $uo->getOrganization()->getId(),
|
||||
'name' => $uo->getOrganization()->getName(),
|
||||
'role' => $uo->getRole()->getName()
|
||||
]
|
||||
];
|
||||
if ($uo->getRole()->getName() === "ADMIN") {
|
||||
$projets = $uo->getOrganization()->getProjects()->toArray();
|
||||
$result[count($result) - 1]['organization']['projects'] = array_map(function ($projet) {
|
||||
return [
|
||||
'id' => $projet->getId(),
|
||||
// 'name' => $projet->getName()
|
||||
];
|
||||
}, $projets);
|
||||
}
|
||||
}
|
||||
|
||||
$this->loggerService->logUserAction($user->getId(), $user->getId(), 'Accessed userinfo endpoint');
|
||||
return new JsonResponse([
|
||||
|
|
@ -39,6 +61,7 @@ class OAuth2Controller extends AbstractController
|
|||
'name' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
'surname' => $user->getSurname(),
|
||||
'uos' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +87,8 @@ class OAuth2Controller extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/oauth2/revoke_tokens', name: 'revoke_tokens', methods: ['POST'])]
|
||||
public function revokeTokens(Security $security, Request $request, AccessTokenService $accessTokenService, LoggerInterface $logger): Response{
|
||||
public function revokeTokens(Security $security, Request $request, AccessTokenService $accessTokenService, LoggerInterface $logger): Response
|
||||
{
|
||||
//Check if the user have valid access token
|
||||
$data = json_decode($request->getContent(), true);
|
||||
$userIdentifier = $data['user_identifier'];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Entity\Actions;
|
|||
use App\Entity\Apps;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Form\OrganizationForm;
|
||||
use App\Repository\OrganizationsRepository;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Repository\AppsRepository;
|
|||
use App\Repository\OrganizationsRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Service\ProjectService;
|
||||
use App\Service\SSO\ProjectService as SSOProjectService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
|
@ -21,11 +22,14 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly OrganizationsRepository $organizationsRepository,
|
||||
private readonly ProjectRepository $projectRepository,
|
||||
private readonly ProjectService $projectService,
|
||||
private readonly UserService $userService, private readonly AppsRepository $appsRepository)
|
||||
private readonly AppsRepository $appsRepository,
|
||||
private readonly SSOProjectService $SSOProjectService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -41,38 +45,70 @@ final class ProjectController extends AbstractController
|
|||
public function new(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!$data) {
|
||||
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
|
||||
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
|
||||
if(!$org) {
|
||||
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
|
||||
|
||||
$orgId = $request->request->get('organizationId');
|
||||
$name = $request->request->get('name');
|
||||
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
|
||||
|
||||
$org = $this->organizationsRepository->find($orgId);
|
||||
if (!$org) {
|
||||
return new JsonResponse(['error' => 'Organization not found'], 404);
|
||||
}
|
||||
$sanitizedDbName = $this->projectService->getProjectDbName($data['name'], $org->getProjectPrefix());
|
||||
if($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) {
|
||||
return new JsonResponse(['error' => 'A project with the same name already exists'], Response::HTTP_CONFLICT);
|
||||
|
||||
// 2. Handle File Upload
|
||||
$logoFile = $request->files->get('logo');
|
||||
$logoPath = null;
|
||||
|
||||
|
||||
|
||||
// 3. Project Creation
|
||||
$sanitizedDbName = $this->projectService->getProjectDbName($name, $org->getProjectPrefix());
|
||||
if ($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) {
|
||||
return new JsonResponse(['error' => 'A project with the same name already exists'], 409);
|
||||
}
|
||||
if(!$this->projectService->isApplicationArrayValid($data['applications'])) {
|
||||
return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST);
|
||||
if ($logoFile) {
|
||||
$logoPath = $this->projectService->handleLogoUpload($logoFile, $sanitizedDbName);
|
||||
}
|
||||
$project = new Project();
|
||||
$project->setName($data['name']);
|
||||
$project->setName($name);
|
||||
$project->setBddName($sanitizedDbName);
|
||||
$project->setOrganization($org);
|
||||
$project->setApplications($data['applications']);
|
||||
$project->setApplications($applications);
|
||||
|
||||
$project->setTimestampPrecision($request->request->get('timestamp'));
|
||||
$project->setDeletionAllowed($request->request->getBoolean('deletion'));
|
||||
|
||||
if ($logoPath) {
|
||||
$project->setLogo($logoPath);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
$this->entityManager->flush();
|
||||
return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED);
|
||||
$this->entityManager->flush(); //On met le flush avant parce qu'on a besoin de l'ID du projet pour la création distante.
|
||||
//Oui ducoup c'est chiant parce que le projet est créé même s'il y a une erreur API, mais OH ffs at that point. ducoup s'il y a un pb, vue que la gestion de projet et fait pas le super admin, il faudra recree le projet dans la bdd corespondant à l'appli qui fonctionne pas
|
||||
// Remote creation logic
|
||||
|
||||
try {
|
||||
foreach ($project->getApplications() as $appId) {
|
||||
$app = $this->appsRepository->find($appId);
|
||||
$clientUrl = 'https://' . $app->getSubDomain() . '.' .$this->getParameter('app_domain') ;
|
||||
$this->SSOProjectService->createRemoteProject($clientUrl, $project);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return new JsonResponse(['message' => 'Project created successfully'], 201);
|
||||
}
|
||||
|
||||
#[Route(path:'/edit/{id}/ajax', name: '_edit', methods: ['POST'])]
|
||||
public function edit(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!$data) {
|
||||
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
|
||||
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
|
||||
$orgId = $request->request->get('organizationId');
|
||||
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
|
||||
|
||||
$org = $this->organizationsRepository->findOneBy(['id' => $orgId]);
|
||||
if(!$org) {
|
||||
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
|
@ -80,9 +116,34 @@ final class ProjectController extends AbstractController
|
|||
if(!$project) {
|
||||
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
$project->setApplications($data['applications']);
|
||||
$logoFile = $request->files->get('logo');
|
||||
$logoPath = null;
|
||||
|
||||
if ($logoFile) {
|
||||
$logoPath = $this->projectService->handleLogoUpload($logoFile, $project->getBddName());
|
||||
}
|
||||
|
||||
$project->setApplications($applications);
|
||||
$project->setModifiedAt(new \DateTimeImmutable());
|
||||
$project->setTimestampPrecision($request->request->get('timestamp'));
|
||||
$project->setDeletionAllowed($request->request->getBoolean('deletion'));
|
||||
if ($logoPath) {
|
||||
$project->setLogo($logoPath);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
// Remote editing logic
|
||||
try {
|
||||
foreach ($project->getApplications() as $appId) {
|
||||
$app = $this->appsRepository->find($appId);
|
||||
$clientUrl = 'https://' . $app->getSubDomain() . '.' .$this->getParameter('app_domain') ;
|
||||
$this->SSOProjectService->editRemoteProject($clientUrl, $project);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK);
|
||||
}
|
||||
|
|
@ -144,6 +205,8 @@ final class ProjectController extends AbstractController
|
|||
'id' => $project->getId(),
|
||||
'name' => ucfirst($project->getName()),
|
||||
'applications' => $project->getApplications(),
|
||||
'timestampPrecision'=> $project->getTimestampPrecision(),
|
||||
'deletionAllowed' => $project->isDeletionAllowed(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +218,16 @@ final class ProjectController extends AbstractController
|
|||
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
$project->setIsDeleted(true);
|
||||
try {
|
||||
foreach ($project->getApplications() as $appId) {
|
||||
$app = $this->appsRepository->find($appId);
|
||||
$clientUrl = 'https://' . $app->getSubDomain() . '.' .$this->getParameter('app_domain') ;
|
||||
$this->SSOProjectService->deleteRemoteProject($clientUrl, $project->getId());
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
$this->entityManager->persist($project);
|
||||
$this->entityManager->flush();
|
||||
return new JsonResponse(['message' => 'Project deleted successfully'], Response::HTTP_OK);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace App\Controller;
|
|||
use App\Entity\Apps;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Form\UserForm;
|
||||
use App\Repository\AppsRepository;
|
||||
|
|
@ -195,88 +195,88 @@ class UserController extends AbstractController
|
|||
|
||||
}
|
||||
|
||||
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
try {
|
||||
$actingUser =$this->getUser();
|
||||
|
||||
$user = new User();
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
$orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'organisation n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
} else{
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||
|
||||
// Case : User exists -> link him to given organization if not already linked, else error message
|
||||
if ($existingUser && $org) {
|
||||
$this->userService->addExistingUserToOrganization(
|
||||
$existingUser,
|
||||
$org,
|
||||
);
|
||||
|
||||
if ($this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$existingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
"Super Admin linked user to organization",
|
||||
$org->getId(),
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
// Case : user doesn't already exist
|
||||
|
||||
$picture = $form->get('pictureUrl')->getData();
|
||||
$this->userService->createNewUser($user, $actingUser, $picture);
|
||||
|
||||
$this->userService->linkUserToOrganization(
|
||||
$user,
|
||||
$org,
|
||||
);
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
return $this->render('user/new.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $form->createView(),
|
||||
'organizationId' => $orgId,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
|
||||
if ($orgId) {
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
}
|
||||
// #[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
// public function new(Request $request): Response
|
||||
// {
|
||||
// $this->denyAccessUnlessGranted('ROLE_USER');
|
||||
// try {
|
||||
// $actingUser =$this->getUser();
|
||||
//
|
||||
// $user = new User();
|
||||
// $form = $this->createForm(UserForm::class, $user);
|
||||
// $form->handleRequest($request);
|
||||
//
|
||||
// $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
|
||||
// if ($orgId) {
|
||||
// $org = $this->organizationRepository->find($orgId);
|
||||
// if (!$org) {
|
||||
// $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
// $this->addFlash('danger', "L'organisation n'existe pas.");
|
||||
// throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
// }
|
||||
// if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
|
||||
// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
// $this->addFlash('danger', "Accès non autorisé.");
|
||||
// throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
// }
|
||||
// } else{
|
||||
// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
// $this->addFlash('danger', "Accès non autorisé.");
|
||||
// throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
// }
|
||||
//
|
||||
// if ($form->isSubmitted() && $form->isValid()) {
|
||||
// $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||
//
|
||||
// // Case : User exists -> link him to given organization if not already linked, else error message
|
||||
// if ($existingUser && $org) {
|
||||
// $this->userService->addExistingUserToOrganization(
|
||||
// $existingUser,
|
||||
// $org,
|
||||
// );
|
||||
//
|
||||
// if ($this->isGranted('ROLE_ADMIN')) {
|
||||
// $this->loggerService->logSuperAdmin(
|
||||
// $existingUser->getId(),
|
||||
// $actingUser->getUserIdentifier(),
|
||||
// "Super Admin linked user to organization",
|
||||
// $org->getId(),
|
||||
// );
|
||||
// }
|
||||
// $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
|
||||
// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
// }
|
||||
//
|
||||
// // Case : user doesn't already exist
|
||||
//
|
||||
// $picture = $form->get('pictureUrl')->getData();
|
||||
// $this->userService->createNewUser($user, $actingUser, $picture);
|
||||
//
|
||||
// $this->userService->linkUserToOrganization(
|
||||
// $user,
|
||||
// $org,
|
||||
// );
|
||||
// $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||||
// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
// }
|
||||
//
|
||||
// return $this->render('user/new.html.twig', [
|
||||
// 'user' => $user,
|
||||
// 'form' => $form->createView(),
|
||||
// 'organizationId' => $orgId,
|
||||
// ]);
|
||||
//
|
||||
// } catch (\Exception $e) {
|
||||
// $this->errorLogger->critical($e->getMessage());
|
||||
//
|
||||
// if ($orgId) {
|
||||
// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||
// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
// }
|
||||
// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||
// return $this->redirectToRoute('user_index');
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Endpoint to activate/deactivate a user (soft delete)
|
||||
|
|
@ -832,6 +832,174 @@ class UserController extends AbstractController
|
|||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[Route('/new/ajax', name: 'new_ajax', methods: ['POST'])]
|
||||
public function newUserAjax(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->getUser();
|
||||
|
||||
try {
|
||||
$data = $request->request->all();
|
||||
$orgId = $data['organizationId'] ?? null;
|
||||
$selectedApps = $data['applications'] ?? [];
|
||||
|
||||
//unset data that are not part of the User entity to avoid form errors
|
||||
unset($data['organizationId'], $data['applications']);
|
||||
$user = new User();
|
||||
|
||||
$form = $this->createForm(UserForm::class, $user, [
|
||||
'csrf_protection' => false,
|
||||
'allow_extra_fields' => true,
|
||||
]);
|
||||
|
||||
$form->submit($data, false);
|
||||
if (!$orgId) {
|
||||
return $this->json(['error' => 'ID Organisation manquant.'], 400);
|
||||
}
|
||||
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
return $this->json(['error' => "L'organisation n'existe pas."], 404);
|
||||
}
|
||||
|
||||
// 3. Permissions Check
|
||||
if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
return $this->json(['error' => "Accès non autorisé."], 403);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$email = $user->getEmail();
|
||||
$existingUser = $this->userRepository->findOneBy(['email' => $email]);
|
||||
|
||||
// CASE A: User exists -> Add to org
|
||||
if ($existingUser) {
|
||||
// Check if already in org to avoid logic errors or duplicate logs
|
||||
$this->userService->addExistingUserToOrganization($existingUser, $org, $selectedApps);
|
||||
|
||||
if ($this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$existingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
"Super Admin linked user to organization via AJAX",
|
||||
$org->getId(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'Utilisateur existant ajouté à l\'organisation.'
|
||||
]);
|
||||
}
|
||||
|
||||
// CASE B: New User -> Create
|
||||
// Fetch picture from $request->files since it's a multipart request
|
||||
$picture = $request->files->get('pictureUrl');
|
||||
|
||||
$this->userService->createNewUser($user, $actingUser, $picture);
|
||||
$this->userService->linkUserToOrganization($user, $org, $selectedApps);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'Nouvel utilisateur créé et ajouté.'
|
||||
]);
|
||||
}
|
||||
|
||||
// If form is invalid, return the specific errors
|
||||
return $this->json(['error' => 'Données de formulaire invalides.'], 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->errorLogger->critical("AJAX User Creation Error: " . $e->getMessage());
|
||||
return $this->json(['error' => 'Une erreur interne est survenue.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/data/{id}', name: 'user_data_json', methods: ['GET'])]
|
||||
public function userData(User $user, Request $request): JsonResponse {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $this->getUser()->getUserIdentifier());
|
||||
return $this->json(['error' => "L'organisation n'existe pas."], 404);
|
||||
}
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]);
|
||||
$apps = $this->userOrganizationAppService->getUserApplicationByOrganization($uo);
|
||||
return $this->json([
|
||||
'email' => $user->getEmail(),
|
||||
'name' => $user->getName(),
|
||||
'surname' => $user->getSurname(),
|
||||
'phoneNumber' => $user->getPhoneNumber(),
|
||||
'applicationIds' => array_map(fn($app) => $app->getId(), $apps),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/edit/{id}/ajax', name: 'edit_ajax', methods: ['POST'])]
|
||||
public function editAjax(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->getUser();
|
||||
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
return $this->json(['error' => "L'utilisateur n'existe pas."], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->userService->isAdminOfUser($user)) {
|
||||
return $this->json(['error' => "Accès non autorisé."], 403);
|
||||
}
|
||||
|
||||
$data = $request->request->all();
|
||||
$orgId = $data['organizationId'] ?? null;
|
||||
$selectedApps = $data['applications'] ?? [];
|
||||
|
||||
// 1. Clean data for the form (remove non-entity fields)
|
||||
unset($data['organizationId'], $data['applications']);
|
||||
|
||||
$form = $this->createForm(UserForm::class, $user, [
|
||||
'csrf_protection' => false,
|
||||
'allow_extra_fields' => true,
|
||||
]);
|
||||
|
||||
$form->submit($data, false);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// 2. Handle User Info & Picture
|
||||
$picture = $request->files->get('pictureUrl');
|
||||
$this->userService->formatUserData($user, $picture);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// 3. Handle Organization-specific Application Sync
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if ($org) {
|
||||
// Logic to sync applications for THIS specific organization
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]);
|
||||
$this->userOrganizationAppService->syncUserApplicationsByOrganization($uo, $selectedApps);
|
||||
|
||||
// Create Action Log
|
||||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Logging
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited via AJAX');
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin($user->getId(), $actingUser->getUserIdentifier(), "Super Admin edited user via AJAX");
|
||||
}
|
||||
|
||||
return $this->json(['success' => true, 'message' => 'Informations modifiées avec succès.']);
|
||||
}
|
||||
|
||||
return $this->json(['error' => 'Données de formulaire invalides.'], 400);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
return $this->json(['error' => 'Une erreur est survenue lors de la modification.'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\api\Security;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class SecurityController extends AbstractController{
|
||||
|
||||
#[Route('/api/validate-token', name: 'api_validate_token')]
|
||||
public function validate(): JsonResponse
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
return $this->json([
|
||||
'valid' => true,
|
||||
'email' => ($user instanceof \App\Entity\User) ? $user->getUserIdentifier() : null,
|
||||
'scopes' => $this->container->get('security.token_storage')->getToken()->getScopes(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\api\v1\user;
|
||||
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\RolesRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\LoggerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route(path: '/api/v1/users', name: 'api_v1_user_')]
|
||||
class UserController extends AbstractController{
|
||||
public function __construct(private readonly UsersOrganizationsRepository $uoRepository, private readonly LoggerService $loggerService, private readonly EntityManagerInterface $entityManager,)
|
||||
{
|
||||
}
|
||||
|
||||
/*Function that get all the users that a user is admin of*/
|
||||
#[Route(path: '/admin/{id}', name: 'get_user_users', methods: ['GET'])]
|
||||
public function getUserUsers($id, UserRepository $userRepository): JsonResponse
|
||||
{
|
||||
$result = [];
|
||||
$user = $userRepository->find($id);
|
||||
if (!$user) {
|
||||
return $this->json(['error' => 'User not found'], 404);
|
||||
}
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$uos = $this->uoRepository->findBy(['user' => $user, 'role' => $roleAdmin]);
|
||||
foreach ($uos as $uo) {
|
||||
$result[] = [
|
||||
'id' => $uo->getUsers()->getId(),
|
||||
'name' => $uo->getUsers()->getName(),
|
||||
'email' => $uo->getUsers()->getEmail(),
|
||||
];
|
||||
}
|
||||
return $this->json($result);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,4 +32,9 @@ final class AccessToken implements AccessTokenEntityInterface
|
|||
->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
|
||||
}
|
||||
|
||||
public function setUserIdentifier(?string $userIdentifier): void
|
||||
{
|
||||
$this->userIdentifier = $userIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -38,9 +38,9 @@ class Apps
|
|||
private ?string $descriptionSmall = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserOrganizatonApp>
|
||||
* @var Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'application')]
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'application')]
|
||||
private Collection $userOrganizatonApps;
|
||||
|
||||
/**
|
||||
|
|
@ -152,14 +152,14 @@ class Apps
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserOrganizatonApp>
|
||||
* @return Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
public function getUserOrganizatonApps(): Collection
|
||||
{
|
||||
return $this->userOrganizatonApps;
|
||||
}
|
||||
|
||||
public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) {
|
||||
$this->userOrganizatonApps->add($userOrganizatonApp);
|
||||
|
|
@ -169,7 +169,7 @@ class Apps
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) {
|
||||
if ($userOrganizatonApp->getApplication() === $this) {
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ class Organizations
|
|||
private Collection $actions;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserOrganizatonApp>
|
||||
* @var Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')]
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'organization')]
|
||||
private Collection $userOrganizatonApps;
|
||||
|
||||
#[ORM\Column(length: 4, nullable: true)]
|
||||
|
|
@ -238,14 +238,14 @@ class Organizations
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserOrganizatonApp>
|
||||
* @return Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
public function getUserOrganizatonApps(): Collection
|
||||
{
|
||||
return $this->userOrganizatonApps;
|
||||
}
|
||||
|
||||
public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) {
|
||||
$this->userOrganizatonApps->add($userOrganizatonApp);
|
||||
|
|
@ -255,7 +255,7 @@ class Organizations
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
|
|
|
|||
|
|
@ -35,9 +35,21 @@ class Project
|
|||
#[ORM\Column]
|
||||
private ?bool $isDeleted = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $bddName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $logo = null;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
private ?string $timestampPrecision = null;
|
||||
|
||||
#[ORM\Column()]
|
||||
private ?bool $deletionAllowed = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $auditsEnabled = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
|
|
@ -146,4 +158,52 @@ class Project
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLogo(): ?string
|
||||
{
|
||||
return $this->logo;
|
||||
}
|
||||
|
||||
public function setLogo(?string $logo): static
|
||||
{
|
||||
$this->logo = $logo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimestampPrecision(): ?string
|
||||
{
|
||||
return $this->timestampPrecision;
|
||||
}
|
||||
|
||||
public function setTimestampPrecision(?string $timestampPrecision): static
|
||||
{
|
||||
$this->timestampPrecision = $timestampPrecision;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDeletionAllowed(): ?bool
|
||||
{
|
||||
return $this->deletionAllowed;
|
||||
}
|
||||
|
||||
public function setDeletionAllowed(?bool $deletionAllowed): static
|
||||
{
|
||||
$this->deletionAllowed = $deletionAllowed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAuditsEnabled(): ?bool
|
||||
{
|
||||
return $this->auditsEnabled;
|
||||
}
|
||||
|
||||
public function setAuditsEnabled(?bool $auditsEnabled): static
|
||||
{
|
||||
$this->auditsEnabled = $auditsEnabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserOrganizatonAppRepository;
|
||||
use App\Repository\UserOrganizationAppRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserOrganizatonAppRepository::class)]
|
||||
class UserOrganizatonApp
|
||||
#[ORM\Entity(repositoryClass: UserOrganizationAppRepository::class)]
|
||||
class UserOrganizationApp
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
|
@ -16,17 +16,15 @@ class UserOrganizatonApp
|
|||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?Roles $role = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $isActive;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'userOrganizatonApps')]
|
||||
#[ORM\ManyToOne(inversedBy: 'userOrganizationApps')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?UsersOrganizations $userOrganization = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'userOrganizatonApps')]
|
||||
#[ORM\ManyToOne(inversedBy: 'userOrganizationApps')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Apps $application = null;
|
||||
|
||||
|
|
@ -53,18 +51,6 @@ class UserOrganizatonApp
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getRole(): ?Roles
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(?Roles $role): static
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isActive(): ?bool
|
||||
{
|
||||
return $this->isActive;
|
||||
|
|
@ -30,9 +30,9 @@ class UsersOrganizations
|
|||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserOrganizatonApp>
|
||||
* @var Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'userOrganization')]
|
||||
#[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'userOrganization')]
|
||||
private Collection $userOrganizatonApps;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
|
|
@ -98,14 +98,14 @@ class UsersOrganizations
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserOrganizatonApp>
|
||||
* @return Collection<int, UserOrganizationApp>
|
||||
*/
|
||||
public function getUserOrganizatonApps(): Collection
|
||||
{
|
||||
return $this->userOrganizatonApps;
|
||||
}
|
||||
|
||||
public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) {
|
||||
$this->userOrganizatonApps->add($userOrganizatonApp);
|
||||
|
|
@ -115,7 +115,7 @@ class UsersOrganizations
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static
|
||||
public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static
|
||||
{
|
||||
if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ class LoginSubscriber implements EventSubscriberInterface
|
|||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(EntityManagerInterface $entityManager,
|
||||
private string $clientIdentifier)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
|
@ -28,19 +29,37 @@ class LoginSubscriber implements EventSubscriberInterface
|
|||
|
||||
public function onLoginSuccess(LoginSuccessEvent $event): void
|
||||
{
|
||||
$user = $event->getUser();
|
||||
if($user) {
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getUserIdentifier()]);
|
||||
$passportUser = $event->getUser();
|
||||
|
||||
// 1. Check if we have a user at all
|
||||
if (!$passportUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. IMPORTANT: Check if this is a real User entity from your DB.
|
||||
// If it's a Machine/Client login, it will be an instance of
|
||||
// League\Bundle\OAuth2ServerBundle\Security\User\ClientCredentialsUser
|
||||
if (!$passportUser instanceof \App\Entity\User) {
|
||||
// It's a machine (M2M), so we don't track "last connection" or create manual tokens
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we know it's a real human user
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy([
|
||||
'email' => $passportUser->getUserIdentifier()
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
$user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris')));
|
||||
|
||||
$easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['name' => 'EasySolution']);
|
||||
if($easySolution) {
|
||||
$easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
|
||||
if ($easySolution) {
|
||||
$accessToken = new AccessToken(
|
||||
identifier: bin2hex(random_bytes(40)), // Generate unique identifier
|
||||
identifier: bin2hex(random_bytes(40)),
|
||||
expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')),
|
||||
client: $easySolution,
|
||||
userIdentifier: $user->getUserIdentifier(),
|
||||
scopes: ['email profile openid apps:easySolutions'] // Empty array if no specific scopes needed
|
||||
scopes: ['email', 'profile', 'openid', 'apps:easySolutions']
|
||||
);
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->persist($accessToken);
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ final class AccessTokenRepository implements AccessTokenRepositoryInterface
|
|||
/** @var int|string|null $userIdentifier */
|
||||
$accessToken = new AccessTokenEntity();
|
||||
$accessToken->setClient($clientEntity);
|
||||
$accessToken->setUserIdentifier($userIdentifier);
|
||||
|
||||
$accessToken->setUserIdentifier($userIdentifier ?? $clientEntity->getIdentifier());
|
||||
foreach ($scopes as $scope) {
|
||||
$accessToken->addScope($scope);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<UserOrganizatonApp>
|
||||
* @extends ServiceEntityRepository<UserOrganizationApp>
|
||||
*/
|
||||
class UserOrganizatonAppRepository extends ServiceEntityRepository
|
||||
class UserOrganizationAppRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, UserOrganizatonApp::class);
|
||||
parent::__construct($registry, UserOrganizationApp::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
|
|
@ -5,7 +5,7 @@ namespace App\Service;
|
|||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\LoggerService;
|
||||
|
|
@ -145,7 +145,7 @@ class OrganizationsService
|
|||
$adminUOs = $this->uoRepository->findBy(['organization' => $data['organization'], 'isActive' => true]);
|
||||
|
||||
foreach ($adminUOs as $adminUO) {
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizationApp::class)
|
||||
->findOneBy([
|
||||
'userOrganization' => $adminUO,
|
||||
'role' => $roleAdmin,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@
|
|||
namespace App\Service;
|
||||
|
||||
use App\Repository\AppsRepository;
|
||||
use App\Service\LoggerService;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
|
||||
class ProjectService{
|
||||
|
||||
|
||||
public function __construct(private readonly AppsRepository $appsRepository)
|
||||
|
||||
public function __construct(private readonly AppsRepository $appsRepository, private readonly Security $security, private readonly LoggerService $loggerService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -40,4 +44,43 @@ class ProjectService{
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleLogoUpload($logoFile, $projectBddName): ?string
|
||||
{
|
||||
// 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures')
|
||||
$destinationDir = 'uploads/project_logos';
|
||||
|
||||
// 2. Create the directory if it doesn't exist
|
||||
if (!file_exists($destinationDir)) {
|
||||
// 0755 is the standard permission (Owner: read/write/exec, Others: read/exec)
|
||||
if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir));
|
||||
}
|
||||
}
|
||||
|
||||
$extension = $logoFile->guessExtension();
|
||||
|
||||
// Sanitize the filename to remove special characters/spaces to prevent filesystem errors
|
||||
$customFilename = $projectBddName . '.' . $extension;
|
||||
|
||||
try {
|
||||
// 4. Move the file to the destination directory
|
||||
$logoFile->move($destinationDir, $customFilename);
|
||||
|
||||
// 5. Update the user entity with the relative path
|
||||
// Ensure you store the path relative to your public folder usually
|
||||
return $destinationDir . '/' . $customFilename;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// 6. Log the critical error as requested
|
||||
$this->loggerService->logError('File upload failed',[
|
||||
'target_user_id' => $this->security->getUser()->getId(),
|
||||
'message' => $e->getMessage(),
|
||||
'file_name' => $customFilename,
|
||||
]);
|
||||
|
||||
// Optional: Re-throw the exception if you want the controller/user to know the upload failed
|
||||
throw new FileException('File upload failed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\SSO;
|
||||
|
||||
|
||||
use App\Entity\Project;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Bundle\OAuth2ServerBundle\Model\Client;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class ProjectService
|
||||
{
|
||||
public function __construct(private readonly HttpClientInterface $httpClient,
|
||||
private string $appUrl,
|
||||
private string $clientIdentifier, private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
// Inside your SSO Server Service
|
||||
public function createRemoteProject(string $clientAppUrl, Project $project): void
|
||||
{
|
||||
// 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M.
|
||||
$tokenResponse = $this->getTokenResponse();
|
||||
|
||||
$accessToken = $tokenResponse->toArray()['access_token'];
|
||||
// data must match easy check database
|
||||
$projectJson = $this->getProjectToJson($project);
|
||||
|
||||
|
||||
// 2. Call the Client Application's Webhook/API
|
||||
$this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
|
||||
'json' => $projectJson
|
||||
]);
|
||||
}
|
||||
|
||||
public function editRemoteProject(string $clientAppUrl, Project $project): void
|
||||
{
|
||||
$tokenResponse = $this->getTokenResponse();
|
||||
|
||||
$accessToken = $tokenResponse->toArray()['access_token'];
|
||||
// data must match easy check database
|
||||
$projectJson = $this->getProjectToJson($project);
|
||||
// 2. Call the Client Application's Webhook/API
|
||||
$this->httpClient->request('PUT', $clientAppUrl . '/api/v1/project/edit/'. $project->getId(), [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
|
||||
'json' => $projectJson
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function deleteRemoteProject(string $clientAppUrl, int $projectId): void
|
||||
{
|
||||
$tokenResponse = $this->getTokenResponse();
|
||||
|
||||
$accessToken = $tokenResponse->toArray()['access_token'];
|
||||
|
||||
// 2. Call the Client Application's Webhook/API
|
||||
$this->httpClient->request('DELETE', $clientAppUrl . '/api/v1/project/delete/'. $projectId, [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getTokenResponse(): ResponseInterface{
|
||||
$portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
|
||||
return $this->httpClient->request('POST', $this->appUrl . 'token', [
|
||||
'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here
|
||||
'body' => [
|
||||
'grant_type' => 'client_credentials',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProjectToJson(Project $project): array {
|
||||
return [
|
||||
'id' => $project->getId(),
|
||||
'name' => $project->getName(),
|
||||
'orgId' => $project->getOrganization()->getId(),
|
||||
'orgName' => $project->getOrganization()->getName(),
|
||||
'bdd' => $project->getBddName(),
|
||||
'isActive' => $project->isActive(),
|
||||
'logo' => $project->getLogo(),
|
||||
'timestamp'=> $project->getTimestampPrecision(),
|
||||
'deletion' => $project->isDeletionAllowed()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ namespace App\Service;
|
|||
use App\Entity\Apps;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\UserOrganizationAppRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\UserService;
|
||||
|
|
@ -16,7 +18,15 @@ use Symfony\Bundle\SecurityBundle\Security;
|
|||
|
||||
class UserOrganizationAppService
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService)
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ActionService $actionService,
|
||||
private readonly Security $security,
|
||||
private readonly UserService $userService,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly LoggerService $loggerService,
|
||||
private readonly UsersOrganizationsRepository $usersOrganizationsRepository,
|
||||
private readonly UserOrganizationAppRepository $uoaRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +86,9 @@ class UserOrganizationAppService
|
|||
public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization, Apps $app = null): void
|
||||
{
|
||||
if($app) {
|
||||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]);
|
||||
$uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]);
|
||||
} else {
|
||||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]);
|
||||
$uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'isActive' => true]);
|
||||
}
|
||||
foreach ($uoas as $uoa) {
|
||||
try{
|
||||
|
|
@ -98,156 +108,6 @@ class UserOrganizationAppService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes user roles for a specific application within an organization.
|
||||
*
|
||||
* This method handles the complete lifecycle of user-application role assignments:
|
||||
* - Activates/deactivates existing role links based on selection
|
||||
* - Creates new role assignments for newly selected roles
|
||||
* - Updates the user's global Symfony security roles when ADMIN/SUPER_ADMIN roles are assigned
|
||||
*
|
||||
* @param UsersOrganizations $uo The user-organization relationship
|
||||
* @param Apps $application The target application
|
||||
* @param array $selectedRoleIds Array of role IDs that should be active for this user-app combination
|
||||
* @param User $actingUser The user performing this action (for audit logging)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception If role entities cannot be found or persisted
|
||||
*/
|
||||
public function syncRolesForUserOrganizationApp(
|
||||
UsersOrganizations $uo,
|
||||
Apps $application,
|
||||
array $selectedRoleIds,
|
||||
User $actingUser
|
||||
): void {
|
||||
|
||||
// Fetch existing UserOrganizationApp links for this user and application
|
||||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy([
|
||||
'userOrganization' => $uo,
|
||||
'application' => $application,
|
||||
]);
|
||||
|
||||
$currentRoleIds = [];
|
||||
// Process existing role links - activate or deactivate based on selection
|
||||
foreach ($uoas as $uoa) {
|
||||
$roleId = $uoa->getRole()->getId();
|
||||
$currentRoleIds[] = $roleId;
|
||||
$roleName = $uoa->getRole()->getName();
|
||||
|
||||
if (in_array((string) $roleId, $selectedRoleIds, true)) {
|
||||
// Role is selected - ensure it's active
|
||||
if (!$uoa->isActive()) {
|
||||
$uoa->setIsActive(true);
|
||||
$this->entityManager->persist($uoa);
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction(
|
||||
"Re-activate user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
"App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
);
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $roleName, true);
|
||||
}
|
||||
// Ensure ADMIN role is assigned if SUPER ADMIN is activated
|
||||
if ($roleName === 'SUPER ADMIN') {
|
||||
$this->ensureAdminRoleForSuperAdmin($uoa);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Role is not selected - ensure it's inactive
|
||||
if ($uoa->isActive()) {
|
||||
$uoa->setIsActive(false);
|
||||
$this->entityManager->persist($uoa);
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction(
|
||||
"Deactivate user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
"App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
);
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $roleName, false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new role assignments for roles that don't exist yet
|
||||
foreach ($selectedRoleIds as $roleId) {
|
||||
if (!in_array($roleId, $currentRoleIds)) {
|
||||
$role = $this->entityManager->getRepository(Roles::class)->find($roleId);
|
||||
if ($role) {
|
||||
// Create new user-organization-application role link
|
||||
$newUoa = new UserOrganizatonApp();
|
||||
$newUoa->setUserOrganization($uo);
|
||||
$newUoa->setApplication($application);
|
||||
$newUoa->setRole($role);
|
||||
$newUoa->setIsActive(true);
|
||||
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($role->getName(), ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $role->getName(), true);
|
||||
}
|
||||
// Ensure ADMIN role is assigned if SUPER ADMIN is activated
|
||||
if ($role->getName() === 'SUPER ADMIN') {
|
||||
$this->ensureAdminRoleForSuperAdmin($newUoa);
|
||||
}
|
||||
$this->entityManager->persist($newUoa);
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction("New user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
"App: {$application->getName()}, Role: {$role->getName()} for user {$uo->getUsers()->getUserIdentifier()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute the role Admin to the user if the user has the role Super Admin
|
||||
*
|
||||
* @param UserOrganizatonApp $uoa
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ensureAdminRoleForSuperAdmin(UserOrganizatonApp $uoa): void
|
||||
{
|
||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy([
|
||||
'userOrganization' => $uoa->getUserOrganization(),
|
||||
'application' => $uoa->getApplication(),
|
||||
'role' => $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN'])
|
||||
]);
|
||||
if(!$uoaAdmin) {
|
||||
$uoaAdmin = new UserOrganizatonApp();
|
||||
$uoaAdmin->setUserOrganization($uoa->getUserOrganization());
|
||||
$uoaAdmin->setApplication($uoa->getApplication());
|
||||
$uoaAdmin->setRole($this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']));
|
||||
$uoaAdmin->setIsActive(true);
|
||||
$this->entityManager->persist($uoaAdmin);
|
||||
}
|
||||
// If the ADMIN role link exists but is inactive, activate it
|
||||
if ($uoaAdmin && !$uoaAdmin->isActive()) {
|
||||
$uoaAdmin->setIsActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users applications links for a given user
|
||||
|
|
@ -257,10 +117,10 @@ class UserOrganizationAppService
|
|||
*/
|
||||
public function getUserApplications(User $user): array
|
||||
{
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'isActive' => true]);
|
||||
$apps = [];
|
||||
foreach ($uos as $uo) {
|
||||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]);
|
||||
$uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]);
|
||||
foreach ($uoas as $uoa) {
|
||||
$app = $uoa->getApplication();
|
||||
if (!in_array($app, $apps, true)) {
|
||||
|
|
@ -270,4 +130,82 @@ class UserOrganizationAppService
|
|||
}
|
||||
return $apps;
|
||||
}
|
||||
|
||||
public function getUserApplicationByOrganization(UsersOrganizations $uo): array
|
||||
{
|
||||
$uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]);
|
||||
$apps = [];
|
||||
foreach ($uoas as $uoa) {
|
||||
$app = $uoa->getApplication();
|
||||
if (!in_array($app, $apps, true)) {
|
||||
$apps[] = $app;
|
||||
}
|
||||
}
|
||||
return $apps;
|
||||
}
|
||||
|
||||
public function syncUserApplicationsByOrganization(UsersOrganizations $uo, array $selectedApps): void
|
||||
{
|
||||
// 1. Get all currently active applications for this specific User-Organization link
|
||||
$currentUolas = $this->uoaRepository->findBy([
|
||||
'userOrganization' => $uo,
|
||||
'isActive' => true
|
||||
]);
|
||||
|
||||
// Track which app IDs are currently active in the DB
|
||||
$currentAppIds = array_map(fn($uoa) => $uoa->getApplication()->getId(), $currentUolas);
|
||||
|
||||
// 2. REMOVAL: Deactivate apps that are in the DB but NOT in the new selection
|
||||
foreach ($currentUolas as $uoa) {
|
||||
$appId = $uoa->getApplication()->getId();
|
||||
if (!in_array($appId, $selectedApps)) {
|
||||
$uoa->setIsActive(false);
|
||||
$this->actionService->createAction(
|
||||
"Deactivate UOA link",
|
||||
$uo->getUsers(),
|
||||
$uo->getOrganization(),
|
||||
"App: " . $uoa->getApplication()->getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ADDITION / REACTIVATION: Handle the selected apps
|
||||
foreach ($selectedApps as $appId) {
|
||||
$app = $this->entityManager->getRepository(Apps::class)->find($appId);
|
||||
if (!$app) continue;
|
||||
|
||||
// Check if a record (active or inactive) already exists
|
||||
$existingUOA = $this->uoaRepository->findOneBy([
|
||||
'userOrganization' => $uo,
|
||||
'application' => $app
|
||||
]);
|
||||
|
||||
if (!$existingUOA) {
|
||||
// Create new if it never existed
|
||||
$newUOA = new UserOrganizationApp();
|
||||
$newUOA->setUserOrganization($uo);
|
||||
$newUOA->setApplication($app);
|
||||
$newUOA->setIsActive(true);
|
||||
$this->entityManager->persist($newUOA);
|
||||
|
||||
$this->actionService->createAction(
|
||||
"Activate UOA link",
|
||||
$uo->getUsers(),
|
||||
$uo->getOrganization(),
|
||||
"App: " . $app->getName()
|
||||
);
|
||||
} elseif (!$existingUOA->isActive()) {
|
||||
// Reactivate if it was previously disabled
|
||||
$existingUOA->setIsActive(true);
|
||||
$this->actionService->createAction(
|
||||
"Reactivate UOA link",
|
||||
$uo->getUsers(),
|
||||
$uo->getOrganization(),
|
||||
"App: " . $app->getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
namespace App\Service;
|
||||
|
||||
|
||||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\RolesRepository;
|
||||
use DateTimeImmutable;
|
||||
|
|
@ -361,7 +362,7 @@ class UserService
|
|||
->findBy(['users' => $user, 'isActive' => true]);
|
||||
$hasRole = false;
|
||||
foreach ($uos as $uo) {
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizationApp::class)
|
||||
->findBy([
|
||||
'userOrganization' => $uo,
|
||||
'isActive' => true,
|
||||
|
|
@ -533,17 +534,17 @@ class UserService
|
|||
*
|
||||
* @param User $user
|
||||
* @param Organizations $organization
|
||||
* @return void
|
||||
* @param array $selectedApps
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handleExistingUser(User $user, Organizations $organization): int
|
||||
public function reactivateUser(User $user, Organizations $organization, array $selectedApps): int
|
||||
{
|
||||
if (!$user->isActive()) {
|
||||
$user->setIsActive(true);
|
||||
$this->entityManager->persist($user);
|
||||
}
|
||||
$uo = $this->linkUserToOrganization($user, $organization);
|
||||
|
||||
return $uo->getId();
|
||||
return $this->linkUserToOrganization($user, $organization, $selectedApps)->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -554,13 +555,15 @@ class UserService
|
|||
* Handle picture if provided
|
||||
*
|
||||
* @param User $user
|
||||
* @param $picture
|
||||
* @param bool $setPassword
|
||||
* @return void
|
||||
*/
|
||||
public function formatUserData(User $user, $picture, bool $setPassword = false): void
|
||||
{
|
||||
// capitalize name and surname
|
||||
$user->setName(ucfirst(strtolower($user->getName())));
|
||||
$user->setSurname(ucfirst(strtolower($user->getSurname())));
|
||||
$user->setSurname(strtoupper($user->getSurname()));
|
||||
|
||||
// trim strings
|
||||
$user->setName(trim($user->getName()));
|
||||
|
|
@ -589,11 +592,12 @@ class UserService
|
|||
public function addExistingUserToOrganization(
|
||||
User $existingUser,
|
||||
Organizations $org,
|
||||
array $selectedApps
|
||||
): int
|
||||
{
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
try {
|
||||
$uoId = $this->handleExistingUser($existingUser, $org);
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
$uoId = $this->reactivateUser($existingUser, $org, $selectedApps);
|
||||
$this->loggerService->logExistingUserAddedToOrg(
|
||||
$existingUser->getId(),
|
||||
$org->getId(),
|
||||
|
|
@ -647,6 +651,7 @@ class UserService
|
|||
public function linkUserToOrganization(
|
||||
User $user,
|
||||
Organizations $org,
|
||||
array $selectedApps
|
||||
): UsersOrganizations
|
||||
{
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
|
|
@ -660,6 +665,7 @@ class UserService
|
|||
$uo->setRole($roleUser);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->linkUOToApps($uo, $selectedApps);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->loggerService->logUserOrganizationLinkCreated(
|
||||
|
|
@ -731,4 +737,18 @@ class UserService
|
|||
}
|
||||
}
|
||||
|
||||
private function linkUOToApps(UsersOrganizations $uo, array $selectedApps):void
|
||||
{
|
||||
foreach ($selectedApps as $appId){
|
||||
$uoa = new UserOrganizationApp();
|
||||
$uoa->setUserOrganization($uo);
|
||||
$app = $this->entityManager->getRepository(Apps::class)->find($appId);
|
||||
if ($app) {
|
||||
$uoa->setApplication($app);
|
||||
$this->entityManager->persist($uoa);
|
||||
}
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
{% set isSA = is_granted('ROLE_SUPER_ADMIN')%}
|
||||
{% set isSA = is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
|
|
@ -52,20 +52,75 @@
|
|||
{# User tables #}
|
||||
<div class="col-9">
|
||||
<div class="row mb-3 d-flex gap-2 ">
|
||||
<div class="col mb-3 card no-header-bg">
|
||||
<div class="col mb-3 card no-header-bg"
|
||||
data-controller="user"
|
||||
data-user-org-id-value="{{ organization.id }}"
|
||||
data-user-new-value="true"
|
||||
data-user-list-small-value="true">
|
||||
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>
|
||||
Nouveaux utilisateurs
|
||||
</h2>
|
||||
<a href="{{ path('user_new', {'organizationId': organization.id}) }}"
|
||||
class="btn btn-primary">Ajouter un utilisateur</a>
|
||||
<h2>Nouveaux utilisateurs</h2>
|
||||
{# Button to trigger modal #}
|
||||
<button type="button" class="btn btn-primary" data-action="click->user#openNewUserModal">
|
||||
Ajouter un utilisateur
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListSmall" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
data-user-new-value="true"
|
||||
data-user-list-small-value="true"
|
||||
data-user-org-id-value="{{ organization.id }}">
|
||||
<div id="tabulator-userListSmall"></div>
|
||||
</div>
|
||||
|
||||
{# New User Modal #}
|
||||
<div class="modal fade" id="newUserModal" tabindex="-1" aria-hidden="true"
|
||||
data-user-target="modal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Créer un nouvel utilisateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form data-action="submit->user#submitNewUser">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email*</label>
|
||||
<input type="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Prénom*</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Nom*</label>
|
||||
<input type="text" name="surname" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Numéro de téléphone</label>
|
||||
<input type="number" name="phoneNumber" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Photo de profil</label>
|
||||
<input type="file" name="pictureUrl" class="form-control"
|
||||
accept="image/*">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<label class="form-label"><b>Applications à associer**</b></label>
|
||||
<div class="row" data-user-target="appList">
|
||||
{# Applications will be injected here #}
|
||||
<div class="text-center p-3 text-muted">Chargement des applications...
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Créer l'utilisateur</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -87,18 +142,21 @@
|
|||
</div>
|
||||
|
||||
{# Modal for Adding Admin #}
|
||||
<div class="modal fade" id="addAdminModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
|
||||
<div class="modal fade" id="addAdminModal" tabindex="-1" aria-hidden="true"
|
||||
data-user-target="modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter un administrateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form data-action="submit->user#submitAddAdmin">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sélectionner l'utilisateur</label>
|
||||
<select name="userId" class="form-select" data-user-target="userSelect" required>
|
||||
<select name="userId" class="form-select" data-user-target="userSelect"
|
||||
required>
|
||||
<option value="">Chargement...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -107,7 +165,9 @@
|
|||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -133,12 +193,11 @@
|
|||
</div>
|
||||
|
||||
{# APPLICATION ROW #}
|
||||
{# TODO:remove app acces and replace wioth project overview#}
|
||||
<div class="row mb-3 card no-header-bg"
|
||||
data-controller="project"
|
||||
data-project-list-project-value="true"
|
||||
data-project-org-id-value="{{ organization.id }}"
|
||||
data-project-admin-value="{{ isSA ? 'true' : 'false' }}">
|
||||
data-project-admin-value="{{ isSA ? 'true' : 'false' }}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Mes Projets</h2>
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
|
|
@ -153,7 +212,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="createProjectModal" tabindex="-1" aria-hidden="true" data-project-target="modal">
|
||||
<div class="modal fade" id="createProjectModal" tabindex="-1" aria-hidden="true"
|
||||
data-project-target="modal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
|
|
@ -163,20 +223,63 @@
|
|||
<form data-action="submit->project#submitForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nom du projet</label>
|
||||
<label class="form-label">
|
||||
<i class="color-primary">{{ ux_icon('bi:input-cursor-text', {height: '15px', width: '15px'}) }}</i>
|
||||
Nom du projet</label>
|
||||
<input type="text" name="name"
|
||||
data-project-target="nameInput"
|
||||
class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="mb-3 col-6">
|
||||
<label class="form-label">
|
||||
<i class="color-primary">
|
||||
{{ ux_icon('bi:calendar', {height: '15px', width: '15px'}) }}
|
||||
</i>Horodatage</label>
|
||||
<select name="timestamp" class="form-select"
|
||||
data-project-target="timestampSelect" required>
|
||||
<option value="day">Jour uniquement (YYYY-MM-DD)</option>
|
||||
<option value="full">Horodatage complet (YYYY-MM-DD HH:MM:SS)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-6">
|
||||
<label class="form-label">
|
||||
<i class="color-primary">
|
||||
{{ ux_icon('bi:trash3', {height: '15px', width: '15px'}) }}
|
||||
</i>
|
||||
Autorisation de suppression</label>
|
||||
<select name="deletion" class="form-select"
|
||||
data-project-target="deletionSelect" required>
|
||||
<option value="true">Suppression autorisée</option>
|
||||
<option value="false">Suppression interdite</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<i class="color-primary">
|
||||
{{ ux_icon('material-symbols:picture-in-picture-outline-rounded', {height: '15px', width: '15px'}) }}
|
||||
</i>
|
||||
Logo de projet
|
||||
</label>
|
||||
<input type="file" name="logo" class="form-control"
|
||||
accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Applications</label>
|
||||
<label class="form-label">
|
||||
<i class="color-primary">{{ ux_icon('bi:grid-3x3-gap', {height: '15px', width: '15px'}) }}</i>
|
||||
Applications</label>
|
||||
<div class="row" data-project-target="appList">
|
||||
{# Checkboxes will be injected here #}
|
||||
<div class="text-center p-3">Chargement des applications...</div>
|
||||
</div>
|
||||
<input name="organizationId" type="hidden" value="{{ organization.id }}">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -189,7 +292,7 @@
|
|||
<div class="col-3 m-auto">
|
||||
<div class="card "
|
||||
data-controller="organization"
|
||||
data-organization-activities-value = "true"
|
||||
data-organization-activities-value="true"
|
||||
data-organization-id-value="{{ organization.id }}">
|
||||
|
||||
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
||||
|
|
|
|||
|
|
@ -33,111 +33,18 @@
|
|||
{% include 'user/userInformation.html.twig' %}
|
||||
|
||||
|
||||
<div class="card border-0 no-header-bg ">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h1>Information d'organisation</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body ms-4">
|
||||
{# <div class="card border-0 no-header-bg ">#}
|
||||
{# <div class="card-header">#}
|
||||
{# <div class="card-title">#}
|
||||
{# <h1>Information d'organisation</h1>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# <div class="card-body ms-4">#}
|
||||
{# TODO: dynamic number of project#}
|
||||
<p><b>Projet : </b>69 projets vous sont attribués</p>
|
||||
</div>
|
||||
|
||||
|
||||
{# <div class="card-body">#}
|
||||
{# <div class="row g-2">#}
|
||||
{# {% for app in apps %}#}
|
||||
{# <div class="col-12 col-md-6">#}
|
||||
{# <div class="card h-100">#}
|
||||
{# <div class="card-header d-flex gap-2">#}
|
||||
{# {% if app.logoMiniUrl %}#}
|
||||
{# <img src="{{ asset(appli.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"#}
|
||||
{# class="rounded-circle" style="width:50px; height:50px;">#}
|
||||
{# {% endif %}#}
|
||||
{# <div class="card-title">#}
|
||||
{# <h1>{{ app.name|title }}</h1>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
|
||||
{# <div class="card-body">#}
|
||||
{# <div class="row">#}
|
||||
{# <p>#}
|
||||
{# <b>Description :</b>#}
|
||||
{# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#}
|
||||
{# </p>#}
|
||||
{# </div>#}
|
||||
|
||||
{# #}{# find appGroup once, used in both editable and read-only branches #}
|
||||
{# {% set appGroup = data.uoas[app.id]|default(null) %}#}
|
||||
|
||||
{# {% if canEdit %}#}
|
||||
{# <form method="POST"#}
|
||||
{# action="{{ path('user_application_role', { id: data.singleUo.id }) }}">#}
|
||||
{# <div class="form-group mb-3">#}
|
||||
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
|
||||
{# <div class="form-check">#}
|
||||
{# {% if appGroup %}#}
|
||||
{# {% for role in data.rolesArray %}#}
|
||||
{# <input class="form-check-input" type="checkbox"#}
|
||||
{# name="roles[]"#}
|
||||
{# value="{{ role.id }}"#}
|
||||
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
|
||||
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
|
||||
{# <label class="form-check"#}
|
||||
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
|
||||
{# {% if role.name == 'USER' %}#}
|
||||
{# Accès#}
|
||||
{# {% else %}#}
|
||||
{# {{ role.name|capitalize }}#}
|
||||
{# {% endif %}#}
|
||||
{# </label>#}
|
||||
{# {% endfor %}#}
|
||||
{# {% else %}#}
|
||||
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# <button type="submit" name="appId" value="{{ app.id }}"#}
|
||||
{# class="btn btn-primary mt-2">#}
|
||||
{# Sauvegarder#}
|
||||
{# </button>#}
|
||||
{# </div>#}
|
||||
{# </form>#}
|
||||
{# {% else %}#}
|
||||
{# <div class="form-group mb-3">#}
|
||||
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
|
||||
{# <div class="form-check">#}
|
||||
{# {% if appGroup %}#}
|
||||
{# {% for role in data.rolesArray %}#}
|
||||
{# <input class="form-check-input" type="checkbox"#}
|
||||
{# disabled#}
|
||||
{# name="roles[]"#}
|
||||
{# value="{{ role.id }}"#}
|
||||
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
|
||||
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
|
||||
{# <label class="form-check"#}
|
||||
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
|
||||
{# {% if role.name == 'USER' %}#}
|
||||
{# Accès#}
|
||||
{# {% else %}#}
|
||||
{# {{ role.name|capitalize }}#}
|
||||
{# {% endif %}#}
|
||||
{# </label>#}
|
||||
{# {% endfor %}#}
|
||||
{# {% else %}#}
|
||||
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endfor %}#}
|
||||
{# </div>#}
|
||||
{# <p><b>Projet : </b>69 projets vous sont attribués</p>#}
|
||||
{# </div>#}
|
||||
|
||||
</div>
|
||||
{# </div>#}
|
||||
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,80 @@
|
|||
{% block body %}
|
||||
<div class="card no-header-bg border-0 ">
|
||||
<div class="card no-header-bg border-0"
|
||||
data-controller="user"
|
||||
data-user-org-id-value="{{organizationId}}">
|
||||
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.pictureUrl is not empty %}
|
||||
<img src="{{asset(user.pictureUrl)}}" alt="user" class="rounded-circle"
|
||||
style="width:40px; height:40px;">
|
||||
<img src="{{ asset(user.pictureUrl) }}" alt="user" class="rounded-circle" style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
<div class="card-title ">
|
||||
<div class="card-title">
|
||||
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ path('user_edit', {'id': user.id}) }}"
|
||||
class="btn btn-primary">Modifier</a>
|
||||
{# Trigger the edit modal with the user ID #}
|
||||
<button type="button" class="btn btn-primary"
|
||||
data-action="click->user#openEditUserModal"
|
||||
data-id="{{ user.id }}">
|
||||
Modifier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body ms-4">
|
||||
<p><b>Email: </b>{{ user.email }}</p>
|
||||
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
|
||||
à {{ user.lastConnection|date('H:m:s') }} </p>
|
||||
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }} à {{ user.lastConnection|date('H:m') }}</p>
|
||||
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
|
||||
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
|
||||
</div>
|
||||
|
||||
{# Reusable Edit Modal #}
|
||||
<div class="modal fade" id="editUserModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifier l'utilisateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form data-action="submit->user#submitEditUser">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Email*</label>
|
||||
<input type="email" name="email" class="form-control" data-user-target="emailInput" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Numéro de téléphone</label>
|
||||
<input type="text" name="phoneNumber" class="form-control" data-user-target="phoneInput">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Prénom*</label>
|
||||
<input type="text" name="name" class="form-control" data-user-target="nameInput" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Nom*</label>
|
||||
<input type="text" name="surname" class="form-control" data-user-target="surnameInput" required>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
||||
<hr>
|
||||
<label class="form-label">**Accès aux applications**</label>
|
||||
<div class="row" data-user-target="appList">
|
||||
{# Checkboxes loaded here #}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace App\Tests\Controller;
|
|||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\AwsService;
|
||||
use App\Tests\Functional\AbstractFunctional;
|
||||
|
|
@ -348,10 +348,10 @@ class OrganizationController extends AbstractFunctional
|
|||
self::assertCount(1, $this->entityManager->getRepository(Apps::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(Roles::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UserOrganizationApp::class)->findAll());
|
||||
self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted());
|
||||
self::assertFalse($this->entityManager->getRepository(UsersOrganizations::class)->find($uoLink->getId())->isActive());
|
||||
self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoaLink->getId())->isActive());
|
||||
self::assertFalse($this->entityManager->getRepository(UserOrganizationApp::class)->find($uoaLink->getId())->isActive());
|
||||
self::assertSelectorNotExists('#tabulator-org');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use App\Entity\Apps;
|
|||
use App\Entity\Roles;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Tests\Functional\AbstractFunctional;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
|
||||
|
|
@ -542,7 +542,7 @@ class UserController extends AbstractFunctional
|
|||
$this->client->followRedirect();
|
||||
self::assertCount(2, $this->entityManager->getRepository(User::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll());
|
||||
self::assertCount(1, $this->entityManager->getRepository(UserOrganizationApp::class)->findAll());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use App\Entity\Notification;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
|
|
@ -85,8 +85,8 @@ abstract class AbstractFunctional extends WebTestCase
|
|||
return $uo;
|
||||
}
|
||||
|
||||
protected function createUOALink(UsersOrganizations $uo, Apps $app, Roles $role): UserOrganizatonApp{
|
||||
$uoa = new UserOrganizatonApp();
|
||||
protected function createUOALink(UsersOrganizations $uo, Apps $app, Roles $role): UserOrganizationApp{
|
||||
$uoa = new UserOrganizationApp();
|
||||
$uoa->setUserOrganization($uo);
|
||||
$uoa->setApplication($app);
|
||||
$uoa->setRole($role);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Entity\Apps;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\AwsService;
|
||||
|
|
@ -186,7 +186,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
$adminRole->setName('ADMIN');
|
||||
|
||||
// 4. Setup UOA Logic (Proof that user is Admin of an App)
|
||||
$uoa = new UserOrganizatonApp();
|
||||
$uoa = new UserOrganizationApp();
|
||||
$this->setEntityId($uoa, 777);
|
||||
$uoa->setUserOrganization($adminUO);
|
||||
$uoa->setRole($adminRole);
|
||||
|
|
@ -209,7 +209,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[Roles::class, $rolesRepo],
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
// 6. Expectations
|
||||
|
|
@ -246,7 +246,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
|
||||
$roleAdmin = new Roles();
|
||||
|
||||
$uoa = new UserOrganizatonApp(); // active admin link
|
||||
$uoa = new UserOrganizationApp(); // active admin link
|
||||
|
||||
// Mocks setup
|
||||
$rolesRepo = $this->createMock(EntityRepository::class);
|
||||
|
|
@ -259,7 +259,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[Roles::class, $rolesRepo],
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
// Expectations: Notification service should NEVER be called
|
||||
|
|
@ -294,7 +294,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[Roles::class, $rolesRepo],
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
// 4. Expectations: ensure NOTHING happens
|
||||
|
|
@ -326,7 +326,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
$adminRole = new Roles();
|
||||
$adminRole->setName('ADMIN');
|
||||
|
||||
$uoa = new UserOrganizatonApp();
|
||||
$uoa = new UserOrganizationApp();
|
||||
$uoa->setUserOrganization($adminUO);
|
||||
$uoa->setRole($adminRole);
|
||||
$uoa->setIsActive(true);
|
||||
|
|
@ -342,7 +342,7 @@ class OrganizationsServiceTest extends TestCase
|
|||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[Roles::class, $rolesRepo],
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
// 5. Dynamic Expectations
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Entity\Apps;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
|
|
@ -78,7 +78,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
|
||||
$uo = new UsersOrganizations(); $this->setEntityId($uo, 99);
|
||||
|
||||
$uoa = new UserOrganizatonApp();
|
||||
$uoa = new UserOrganizationApp();
|
||||
$this->setEntityId($uoa, 500);
|
||||
$uoa->setApplication($app1);
|
||||
$uoa->setRole($role);
|
||||
|
|
@ -121,7 +121,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$role = new Roles();
|
||||
$this->setEntityId($role, 10);
|
||||
|
||||
$uoa = new UserOrganizatonApp();
|
||||
$uoa = new UserOrganizationApp();
|
||||
$this->setEntityId($uoa, 555);
|
||||
$uoa->setApplication($app);
|
||||
$uoa->setRole($role);
|
||||
|
|
@ -159,7 +159,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$app = new Apps(); $this->setEntityId($app, 1);
|
||||
$role = new Roles(); $this->setEntityId($role, 1);
|
||||
|
||||
$realUoa = new UserOrganizatonApp();
|
||||
$realUoa = new UserOrganizationApp();
|
||||
$this->setEntityId($realUoa, 100);
|
||||
$realUoa->setApplication($app);
|
||||
$realUoa->setRole($role);
|
||||
|
|
@ -209,12 +209,12 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$roleRepo->method('find')->with($roleId)->willReturn($role);
|
||||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
[Roles::class, $roleRepo],
|
||||
]);
|
||||
|
||||
// Expect creation
|
||||
$this->entityManager->expects($this->once())->method('persist')->with($this->isInstanceOf(UserOrganizatonApp::class));
|
||||
$this->entityManager->expects($this->once())->method('persist')->with($this->isInstanceOf(UserOrganizationApp::class));
|
||||
$this->entityManager->expects($this->once())->method('flush');
|
||||
$this->actionService->expects($this->once())->method('createAction');
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$role = new Roles(); $this->setEntityId($role, 30);
|
||||
$role->setName('VIEWER');
|
||||
|
||||
$existingUoa = new UserOrganizatonApp();
|
||||
$existingUoa = new UserOrganizationApp();
|
||||
$this->setEntityId($existingUoa, 999);
|
||||
$existingUoa->setRole($role);
|
||||
$existingUoa->setApplication($app);
|
||||
|
|
@ -251,7 +251,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$uoaRepo->method('findBy')->willReturn([$existingUoa]);
|
||||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
// We pass empty array [] as selected roles -> expect deactivation
|
||||
|
|
@ -296,7 +296,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
$roleRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole);
|
||||
|
||||
$this->entityManager->method('getRepository')->willReturnMap([
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
[Roles::class, $roleRepo],
|
||||
]);
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ class UserOrganizationAppServiceTest extends TestCase
|
|||
// - The new ADMIN link (automatically created)
|
||||
$this->entityManager->expects($this->exactly(2))
|
||||
->method('persist')
|
||||
->with($this->isInstanceOf(UserOrganizatonApp::class));
|
||||
->with($this->isInstanceOf(UserOrganizationApp::class));
|
||||
|
||||
// Run
|
||||
$this->service->syncRolesForUserOrganizationApp($uo, $app, ['100'], $actingUser);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace App\Tests\Service;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UserOrganizationApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Event\UserCreatedEvent;
|
||||
use App\Service\ActionService;
|
||||
|
|
@ -328,7 +328,7 @@ class UserServiceTest extends TestCase
|
|||
|
||||
// 4. UserOrganizatonApp mock (The link checking if they are admin active)
|
||||
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||
$uoa = new UserOrganizatonApp();
|
||||
$uoa = new UserOrganizationApp();
|
||||
$uoaRepo->method('findOneBy')->willReturn($uoa); // Returns an object, so true
|
||||
|
||||
// Configure EntityManager to return these repos based on class
|
||||
|
|
@ -336,7 +336,7 @@ class UserServiceTest extends TestCase
|
|||
[User::class, $userRepo],
|
||||
[UsersOrganizations::class, $uoRepo],
|
||||
[Roles::class, $rolesRepo],
|
||||
[UserOrganizatonApp::class, $uoaRepo],
|
||||
[UserOrganizationApp::class, $uoaRepo],
|
||||
]);
|
||||
|
||||
$result = $this->userService->isAdminOfOrganization($org);
|
||||
|
|
|
|||
Loading…
Reference in New Issue