set up edit for project in api
This commit is contained in:
parent
782ca27b5e
commit
c0a8a9ab82
|
|
@ -10,7 +10,7 @@ export default class extends Controller {
|
||||||
orgId: Number,
|
orgId: Number,
|
||||||
admin: Boolean
|
admin: Boolean
|
||||||
}
|
}
|
||||||
static targets = ["modal", "appList", "nameInput", "formTitle"];
|
static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"];
|
||||||
connect(){
|
connect(){
|
||||||
if(this.listProjectValue){
|
if(this.listProjectValue){
|
||||||
this.table();
|
this.table();
|
||||||
|
|
@ -137,17 +137,23 @@ export default class extends Controller {
|
||||||
|
|
||||||
async submitForm(event) {
|
async submitForm(event) {
|
||||||
event.preventDefault();
|
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 = {
|
// 1. Validate File Format
|
||||||
organizationId: this.orgIdValue,
|
const logoFile = formData.get('logo');
|
||||||
applications: formData.getAll('applications[]')
|
if (logoFile && logoFile.size > 0) {
|
||||||
};
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||||
|
if (!allowedTypes.includes(logoFile.type)) {
|
||||||
// Only include name if it wasn't disabled (new projects)
|
alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG.");
|
||||||
if (!this.nameInputTarget.disabled) {
|
return; // Stop submission
|
||||||
payload.name = formData.get('name');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
const url = this.currentProjectId
|
||||||
? `/project/edit/${this.currentProjectId}/ajax`
|
? `/project/edit/${this.currentProjectId}/ajax`
|
||||||
|
|
@ -155,17 +161,20 @@ export default class extends Controller {
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// IMPORTANT: Do NOT set Content-Type header when sending FormData with files
|
||||||
body: JSON.stringify(payload)
|
// The browser will set 'multipart/form-data' and the boundary automatically
|
||||||
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
// Use Tabulator's setData() instead of reload() for better UX if possible
|
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
if (response.status === 409) {
|
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
|
// 3. Set the name
|
||||||
this.nameInputTarget.value = project.name;
|
this.nameInputTarget.value = project.name;
|
||||||
|
console.log(project);
|
||||||
|
this.timestampSelectTarget.value = project.timestampPrecision;
|
||||||
|
this.deletionSelectTarget.value = project.deletionAllowed;
|
||||||
|
|
||||||
// 4. Check the boxes
|
// 4. Check the boxes
|
||||||
// We look for all checkboxes inside our appList target
|
// We look for all checkboxes inside our appList target
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,39 +44,66 @@ final class ProjectController extends AbstractController
|
||||||
public function new(Request $request): JsonResponse
|
public function new(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
if (!$data) {
|
$orgId = $request->request->get('organizationId');
|
||||||
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
|
$name = $request->request->get('name');
|
||||||
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
|
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
|
||||||
|
|
||||||
|
$org = $this->organizationsRepository->find($orgId);
|
||||||
if (!$org) {
|
if (!$org) {
|
||||||
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
|
return new JsonResponse(['error' => 'Organization not found'], 404);
|
||||||
}
|
}
|
||||||
$sanitizedDbName = $this->projectService->getProjectDbName($data['name'], $org->getProjectPrefix());
|
|
||||||
|
// 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])) {
|
if ($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) {
|
||||||
return new JsonResponse(['error' => 'A project with the same name already exists'], Response::HTTP_CONFLICT);
|
return new JsonResponse(['error' => 'A project with the same name already exists'], 409);
|
||||||
}
|
}
|
||||||
if(!$this->projectService->isApplicationArrayValid($data['applications'])) {
|
if ($logoFile) {
|
||||||
return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST);
|
$logoPath = $this->projectService->handleLogoUpload($logoFile, $sanitizedDbName);
|
||||||
}
|
}
|
||||||
$project = new Project();
|
$project = new Project();
|
||||||
$project->setName($data['name']);
|
$project->setName($name);
|
||||||
$project->setBddName($sanitizedDbName);
|
$project->setBddName($sanitizedDbName);
|
||||||
$project->setOrganization($org);
|
$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->persist($project);
|
||||||
|
$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ée même s'il y a une erreur API, mais OH ffs at that point.
|
||||||
|
// Remote creation logic
|
||||||
|
try {
|
||||||
$this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project);
|
$this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project);
|
||||||
$this->entityManager->flush();
|
} catch (\Exception $e) {
|
||||||
return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED);
|
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'])]
|
#[Route(path:'/edit/{id}/ajax', name: '_edit', methods: ['POST'])]
|
||||||
public function edit(Request $request, int $id): JsonResponse
|
public function edit(Request $request, int $id): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
$orgId = $request->request->get('organizationId');
|
||||||
if (!$data) {
|
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
|
||||||
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
|
|
||||||
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
|
$org = $this->organizationsRepository->findOneBy(['id' => $orgId]);
|
||||||
if(!$org) {
|
if(!$org) {
|
||||||
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
|
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
@ -84,9 +111,29 @@ final class ProjectController extends AbstractController
|
||||||
if(!$project) {
|
if(!$project) {
|
||||||
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
|
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->setModifiedAt(new \DateTimeImmutable());
|
||||||
|
$project->setTimestampPrecision($request->request->get('timestamp'));
|
||||||
|
$project->setDeletionAllowed($request->request->getBoolean('deletion'));
|
||||||
|
if ($logoPath) {
|
||||||
|
$project->setLogo($logoPath);
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($project);
|
$this->entityManager->persist($project);
|
||||||
|
// Remote editing logic
|
||||||
|
try {
|
||||||
|
$this->SSOProjectService->editRemoteProject('http://api.solutions-easy.moi', $project);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK);
|
return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK);
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +195,8 @@ final class ProjectController extends AbstractController
|
||||||
'id' => $project->getId(),
|
'id' => $project->getId(),
|
||||||
'name' => ucfirst($project->getName()),
|
'name' => ucfirst($project->getName()),
|
||||||
'applications' => $project->getApplications(),
|
'applications' => $project->getApplications(),
|
||||||
|
'timestampPrecision'=> $project->getTimestampPrecision(),
|
||||||
|
'deletionAllowed' => $project->isDeletionAllowed(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,21 @@ class Project
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?bool $isDeleted = null;
|
private ?bool $isDeleted = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255)]
|
||||||
private ?string $bddName = null;
|
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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->createdAt = new \DateTimeImmutable();
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
|
@ -146,4 +158,52 @@ class Project
|
||||||
|
|
||||||
return $this;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\Repository\AppsRepository;
|
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;
|
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||||
|
|
||||||
class ProjectService{
|
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;
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Entity\Project;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use League\Bundle\OAuth2ServerBundle\Model\Client;
|
use League\Bundle\OAuth2ServerBundle\Model\Client;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
class ProjectService
|
class ProjectService
|
||||||
{
|
{
|
||||||
|
|
@ -20,23 +21,12 @@ class ProjectService
|
||||||
public function createRemoteProject(string $clientAppUrl, Project $project): void
|
public function createRemoteProject(string $clientAppUrl, Project $project): void
|
||||||
{
|
{
|
||||||
// 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M.
|
// 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M.
|
||||||
$portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
|
$tokenResponse = $this->getTokenResponse();
|
||||||
$tokenResponse = $this->httpClient->request('POST', $this->appUrl . 'token', [
|
|
||||||
'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here
|
|
||||||
'body' => [
|
|
||||||
'grant_type' => 'client_credentials',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$accessToken = $tokenResponse->toArray()['access_token'];
|
$accessToken = $tokenResponse->toArray()['access_token'];
|
||||||
// data must match easy check database
|
// data must match easy check database
|
||||||
$projectJson = [
|
$projectJson = $this->getTokenResponse($project);
|
||||||
'id' => $project->getId(),
|
|
||||||
'projet' => $project->getName(),
|
|
||||||
'entity_id' => 3,
|
|
||||||
'bdd' => $project->getBddName(),
|
|
||||||
'isactive' => $project->isActive(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. Call the Client Application's Webhook/API
|
// 2. Call the Client Application's Webhook/API
|
||||||
$this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [
|
$this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [
|
||||||
|
|
@ -45,4 +35,41 @@ class ProjectService
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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(),
|
||||||
|
'projet' => $project->getName(),
|
||||||
|
'entity_id' => 3,
|
||||||
|
'bdd' => $project->getBddName(),
|
||||||
|
'isactive' => $project->isActive(),
|
||||||
|
'logo' => $project->getLogo(),
|
||||||
|
'timestamp'=> $project->getTimestampPrecision(),
|
||||||
|
'deletion' => $project->isDeletionAllowed()
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# New User Modal #}
|
{# New User Modal #}
|
||||||
<div class="modal fade" id="newUserModal" tabindex="-1" aria-hidden="true" data-user-target="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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
@ -99,19 +100,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Photo de profil</label>
|
<label class="form-label">Photo de profil</label>
|
||||||
<input type="file" name="pictureUrl" class="form-control" accept="image/*">
|
<input type="file" name="pictureUrl" class="form-control"
|
||||||
|
accept="image/*">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<label class="form-label"><b>Applications à associer**</b></label>
|
<label class="form-label"><b>Applications à associer**</b></label>
|
||||||
<div class="row" data-user-target="appList">
|
<div class="row" data-user-target="appList">
|
||||||
{# Applications will be injected here #}
|
{# Applications will be injected here #}
|
||||||
<div class="text-center p-3 text-muted">Chargement des applications...</div>
|
<div class="text-center p-3 text-muted">Chargement des applications...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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">Créer l'utilisateur</button>
|
<button type="submit" class="btn btn-primary">Créer l'utilisateur</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -137,18 +142,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Modal for Adding Admin #}
|
{# 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-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Ajouter un administrateur</h5>
|
<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>
|
</div>
|
||||||
<form data-action="submit->user#submitAddAdmin">
|
<form data-action="submit->user#submitAddAdmin">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Sélectionner l'utilisateur</label>
|
<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>
|
<option value="">Chargement...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -157,7 +165,9 @@
|
||||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -183,7 +193,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# APPLICATION ROW #}
|
{# APPLICATION ROW #}
|
||||||
{# TODO:remove app acces and replace wioth project overview#}
|
|
||||||
<div class="row mb-3 card no-header-bg"
|
<div class="row mb-3 card no-header-bg"
|
||||||
data-controller="project"
|
data-controller="project"
|
||||||
data-project-list-project-value="true"
|
data-project-list-project-value="true"
|
||||||
|
|
@ -203,7 +212,8 @@
|
||||||
</div>
|
</div>
|
||||||
</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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
@ -213,20 +223,63 @@
|
||||||
<form data-action="submit->project#submitForm">
|
<form data-action="submit->project#submitForm">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<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"
|
<input type="text" name="name"
|
||||||
data-project-target="nameInput"
|
data-project-target="nameInput"
|
||||||
class="form-control" required>
|
class="form-control" required>
|
||||||
</div>
|
</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">
|
<div class="row" data-project-target="appList">
|
||||||
{# Checkboxes will be injected here #}
|
{# Checkboxes will be injected here #}
|
||||||
<div class="text-center p-3">Chargement des applications...</div>
|
<div class="text-center p-3">Chargement des applications...</div>
|
||||||
</div>
|
</div>
|
||||||
|
<input name="organizationId" type="hidden" value="{{ organization.id }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue