Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/php-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build & deploy sandbox

on:
push:
branches: [ "develop" ]
branches: [ "develop"]
workflow_dispatch:

permissions:
Expand Down
2 changes: 2 additions & 0 deletions migrations/19-stream-owner.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Fix: les créateurs de streams existants doivent avoir is_owner = 1
UPDATE {prefix}user_right SET is_owner = 1 WHERE id_charity_stream IS NOT NULL;
88 changes: 77 additions & 11 deletions src/Controllers/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Controllers;

use App\Models\User;
use App\Repositories\AccessTokenRepository;
use App\Repositories\AuthorizationCodeRepository;
use App\Repositories\EventRepository;
Expand Down Expand Up @@ -170,6 +171,42 @@ public function deleteUser(Request $request, Response $response, array $args): R
return $this->redirectToRoute($request, $response, 'app_admin_index', [], ['tab' => 'users']);
}

private function provisionAdminUser(string $email, Request $request): User
{
$existing = $this->userRepository->select($email);
if ($existing) {
return $existing;
}
$user = $this->userRepository->insert($email);
$user = $this->userRepository->insertResetToken($user);
$this->sendAdminInvitationEmail($user, $request);
return $user;
}

private function sendAdminInvitationEmail(User $user, Request $request): void
{
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
$resetUrl = $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_reset_password', ["token" => $user->reset_token]);

try {
$result = $this->mailchimp->messages->send([
"message" => [
"from_email" => "contact@helloasso.io",
"from_name" => "HelloAsso",
"subject" => "Bienvenue sur HelloAsso Stream !",
"html" => $this->buildWelcomeEmail($resetUrl),
"to" => [["email" => $user->email, "type" => "to"]],
],
]);

if ($result instanceof \Exception) {
throw $result;
}
} catch (\Exception $e) {
$this->messages->addMessage('error', 'Admin ajouté mais l\'email d\'invitation n\'a pas pu être envoyé.');
}
}

/**
* Génère le contenu HTML de l'email de bienvenue envoyé aux nouveaux utilisateurs.
*/
Expand Down Expand Up @@ -242,7 +279,7 @@ public function editEvent(Request $request, Response $response, array $args): Re
$cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $event->guid);
$streams = $this->streamRepository->selectListByEvent($event);
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
$isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isEventOwner($user, $event);
$isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $event);

$data = [
"logged" => true,
Expand All @@ -255,7 +292,7 @@ public function editEvent(Request $request, Response $response, array $args): Re
"widgetDonationGoalUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_donation', ["id" => $event->guid]),
"widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_card', ["id" => $event->guid]),
"eventGoals" => $this->goalRepository->selectAmountsByEventGuid($event->guid),
"eventAdmins" => $this->userRepository->selectEventAdmins($event),
"eventAdmins" => $this->userRepository->selectAdmins($event),
"isEventOwner" => $isEventOwner,
"currentUserId" => $user->id,
];
Expand Down Expand Up @@ -309,27 +346,26 @@ public function editEventPost(Request $request, Response $response, array $args)
}

// Gestion des admins (owner ou ADMIN global uniquement)
$isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isEventOwner($user, $event);
$isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $event);

if ($isEventOwner && isset($body['add_admin'])) {
$email = trim($body['admin_email'] ?? '');
if ($email) {
$newAdmin = $this->userRepository->findOrCreate($email);
$existing = $this->userRepository->selectEventAdmins($event);
$alreadyIn = array_filter($existing, fn($a) => (int) $a['id'] === $newAdmin->id);
if (empty($alreadyIn)) {
$existing = $this->userRepository->selectAdmins($event);
if (in_array($email, array_column($existing, 'email'))) {
$this->messages->addMessage('info', "{$email} est déjà admin de cet évènement");
} else {
$newAdmin = $this->provisionAdminUser($email, $request);
$this->userRepository->insertRight($newAdmin, null, $event, false);
$this->messages->addMessage('success', "Admin {$email} ajouté");
} else {
$this->messages->addMessage('info', "{$email} est déjà admin de cet évènement");
}
}
}

if ($isEventOwner && isset($body['remove_admin'])) {
$removeId = (int) ($body['remove_admin'] ?? 0);
if ($removeId && $removeId !== $user->id) {
$this->userRepository->deleteEventRight($removeId, $event);
$this->userRepository->deleteRight($removeId, $event);
$this->messages->addMessage('success', 'Admin retiré');
}
}
Expand Down Expand Up @@ -360,7 +396,7 @@ public function newStream(Request $request, Response $response): Response
}

$stream = $this->streamRepository->insert($data['form_slug'], $data['organization_slug'], $data['title'], $event->id ?? null, $data['form_type'] ?? 'Donation');
$this->userRepository->insertRight($owner, $stream, null);
$this->userRepository->insertRight($owner, $stream, null, true);

if ($event !== null && $parentStyle) {
$donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $event->guid);
Expand Down Expand Up @@ -422,6 +458,8 @@ public function editStream(Request $request, Response $response, array $args): R
$donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/' . $formTypeUrlSegment . '/' . $charityStream->form_slug;
$routeParser = RouteContext::fromRequest($request)->getRouteParser();

$isStreamOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $charityStream);

$data = [
"logged" => true,
"charityStream" => $charityStream,
Expand All @@ -439,6 +477,9 @@ public function editStream(Request $request, Response $response, array $args): R
"widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_stream_widget_card', ["id" => $guid]),
"messages" => $this->messages->getMessages(),
"streamGoals" => $this->goalRepository->selectAmountsByStreamGuid($guid),
"streamAdmins" => $this->userRepository->selectAdmins($charityStream),
"isStreamOwner" => $isStreamOwner,
"currentUserId" => $user->id,
];

return $this->view->render($response, 'stream/edit.html.twig', $data);
Expand Down Expand Up @@ -519,6 +560,31 @@ public function editStreamPost(Request $request, Response $response, array $args
}
}

// Gestion des admins (owner ou ADMIN global uniquement)
$isStreamOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $charityStream);

if ($isStreamOwner && isset($body['add_admin'])) {
$email = trim($body['admin_email'] ?? '');
if ($email) {
$existing = $this->userRepository->selectAdmins($charityStream);
if (in_array($email, array_column($existing, 'email'))) {
$this->messages->addMessage('info', "{$email} est déjà admin de ce stream");
} else {
$newAdmin = $this->provisionAdminUser($email, $request);
$this->userRepository->insertRight($newAdmin, $charityStream, null, false);
$this->messages->addMessage('success', "Admin {$email} ajouté");
}
}
}

if ($isStreamOwner && isset($body['remove_admin'])) {
$removeId = (int) ($body['remove_admin'] ?? 0);
if ($removeId && $removeId !== $user->id) {
$this->userRepository->deleteRight($removeId, $charityStream);
$this->messages->addMessage('success', 'Admin retiré');
}
}

$this->handleWidgetFormSave($request, $guid, null);

return $this->redirectToRoute($request, $response, 'app_stream_edit', ["id" => $guid]);
Expand Down
5 changes: 4 additions & 1 deletion src/Models/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Models;

class Event
class Event implements HasRight
{
public $id;
public $guid;
Expand All @@ -12,4 +12,7 @@ class Event
public $admin;
public $is_test_mode = 0;
public $test_amount = 0;

public function getRightColumn(): string { return 'id_charity_event'; }
public function getRightId(): int { return (int) $this->id; }
}
9 changes: 9 additions & 0 deletions src/Models/HasRight.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Models;

interface HasRight
{
public function getRightColumn(): string;
public function getRightId(): int;
}
5 changes: 4 additions & 1 deletion src/Models/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Models;

class Stream
class Stream implements HasRight
{
public $id;
public $charity_event_id;
Expand All @@ -16,4 +16,7 @@ class Stream
public $admin;
public $is_test_mode = 0;
public $test_amount = 0;

public function getRightColumn(): string { return 'id_charity_stream'; }
public function getRightId(): int { return (int) $this->id; }
}
42 changes: 23 additions & 19 deletions src/Repositories/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Repositories;

use App\Models\Event;
use App\Models\HasRight;
use App\Models\Stream;
use App\Models\User;
use DateTime;
Expand Down Expand Up @@ -74,37 +75,40 @@ public function insertRight(User $user, ?Stream $stream, ?Event $event, bool $is
]);
}

public function selectEventAdmins(Event $event): array
public function selectAdmins(HasRight $entity): array
{
$stmt = $this->pdo->prepare('
$col = $entity->getRightColumn();
$stmt = $this->pdo->prepare("
SELECT u.id, u.email, ur.is_owner
FROM ' . $this->prefix . 'user_right ur
INNER JOIN ' . $this->prefix . 'users u ON u.id = ur.id_user
WHERE ur.id_charity_event = ?
FROM {$this->prefix}user_right ur
INNER JOIN {$this->prefix}users u ON u.id = ur.id_user
WHERE ur.{$col} = ?
ORDER BY ur.is_owner DESC, u.email ASC
');
$stmt->execute([$event->id]);
");
$stmt->execute([$entity->getRightId()]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

public function isEventOwner(User $user, Event $event): bool
public function isOwner(User $user, HasRight $entity): bool
{
$stmt = $this->pdo->prepare('
SELECT 1 FROM ' . $this->prefix . 'user_right
WHERE id_user = ? AND id_charity_event = ? AND is_owner = 1
$col = $entity->getRightColumn();
$stmt = $this->pdo->prepare("
SELECT 1 FROM {$this->prefix}user_right
WHERE id_user = ? AND {$col} = ? AND is_owner = 1
LIMIT 1
');
$stmt->execute([$user->id, $event->id]);
");
$stmt->execute([$user->id, $entity->getRightId()]);
return (bool) $stmt->fetch();
}

public function deleteEventRight(int $userId, Event $event): void
public function deleteRight(int $userId, HasRight $entity): void
{
$stmt = $this->pdo->prepare('
DELETE FROM ' . $this->prefix . 'user_right
WHERE id_user = ? AND id_charity_event = ?
');
$stmt->execute([$userId, $event->id]);
$col = $entity->getRightColumn();
$stmt = $this->pdo->prepare("
DELETE FROM {$this->prefix}user_right
WHERE id_user = ? AND {$col} = ?
");
$stmt->execute([$userId, $entity->getRightId()]);
}

public function select(string $email): ?User
Expand Down
29 changes: 29 additions & 0 deletions src/views/stream/edit.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,35 @@
</div>
</div>

{# ── Administrateurs ─────────────────────────────────────── #}
{% if isStreamOwner %}
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">👥 Administrateurs</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2 mb-3">
{% for admin in streamAdmins %}
<span class="badge rounded-pill {{ admin.is_owner ? 'bg-warning text-dark' : 'bg-secondary' }} d-flex align-items-center gap-1 px-3 py-2" style="font-size:0.95rem;">
{{ admin.email | e }}
{% if admin.is_owner %}
<span class="ms-1" title="Propriétaire">★</span>
{% elseif admin.id != currentUserId %}
<form method="POST" class="d-inline ms-1">
<button type="submit" name="remove_admin" value="{{ admin.id }}" class="btn-close btn-close-white" aria-label="Retirer" style="font-size:0.6rem;" onclick="return confirm('Retirer {{ admin.email | e }} ?')"></button>
</form>
{% endif %}
</span>
{% endfor %}
</div>
<form method="POST" class="d-flex gap-2" style="max-width:420px;">
<input type="email" class="form-control" name="admin_email" placeholder="Email du nouvel admin" required>
<button type="submit" class="btn btn-outline-primary text-nowrap" name="add_admin">+ Ajouter</button>
</form>
</div>
</div>
{% endif %}

{# ── Objectifs de collecte ───────────────────────────────── #}
<div class="card mb-4">
<div class="card-header">
Expand Down
Loading