From 5cd00a1ff12139a179009f42968d5d01e7f92d37 Mon Sep 17 00:00:00 2001 From: stephan Date: Mon, 13 Jul 2026 10:08:25 -0400 Subject: [PATCH] Enhance event access management for promoters by adding invitation and revocation functionalities. Update the UI to handle success and error messages, and improve session handling for actor identification. This aligns with MSIN-4401 requirements for a more robust event management experience. --- inc_tableau_eve_acces.php | 44 ++- php/inc_fx_eve_acces.php | 82 ++++- php/inc_fx_eve_acces_promoteur.php | 483 ++++++++++++++++++++++++- sql/MSIN-4401-com-invite-team-flag.sql | 15 + 4 files changed, 614 insertions(+), 10 deletions(-) create mode 100644 sql/MSIN-4401-com-invite-team-flag.sql diff --git a/inc_tableau_eve_acces.php b/inc_tableau_eve_acces.php index 5c759e6..a8b8bf6 100644 --- a/inc_tableau_eve_acces.php +++ b/inc_tableau_eve_acces.php @@ -16,16 +16,54 @@ if (isset($_GET['promoteur_eve_id'])) { $intEveId = intval(base64_decode(urldecode($_GET['promoteur_eve_id']))); } -$strRedirect = $vDomaine . (($strLangue === 'en') ? '/account' : '/compte'); +$strLangue = ($strLangue === 'en') ? 'en' : 'fr'; +$strBasePath = ($strLangue === 'en') ? '/account' : '/compte'; +$strRedirect = $vDomaine . $strBasePath; if (!empty($_GET['promoteur_eve_id'])) { $strRedirect .= '/inc_tableau_promoteur?promoteur_eve_id=' . urlencode($_GET['promoteur_eve_id']); } +$intActor = intval($_SESSION['com_id'] ?? 0); +if (!empty($_SESSION['usa_id'])) { + $intActor = intval($_SESSION['usa_id']); +} + if ($intEveId <= 0 || (empty($_SESSION['com_id']) && empty($_SESSION['usa_id'])) - || !fxEveAccesCanTeamInvite($_SESSION['com_id'] ?? 0, $intEveId)) { + || !fxEveAccesCanTeamInvite($intActor, $intEveId)) { header('Location: ' . $strRedirect); exit; } -fxEveAccesPromoteurRenderPage($intEveId, $strLangue); +$strFlash = ''; +$strError = ''; +$strSelfUrl = $vDomaine . $strBasePath . '/inc_tableau_eve_acces?promoteur_eve_id=' + . urlencode(base64_encode($intEveId)) . '&lng=' . $strLangue; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $strAction = (string)($_POST['eve_acces_action'] ?? ''); + + if ($strAction === 'invite') { + $arrResult = fxEveAccesPromoteurInvite($intEveId, $intActor, $_POST, $strLangue); + if (($arrResult['state'] ?? '') === 'success') { + header('Location: ' . $strSelfUrl . '&ok=' . urlencode($arrResult['message'])); + exit; + } + $strError = $arrResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue); + } elseif ($strAction === 'revoke') { + $arrResult = fxEveAccesPromoteurRevoke(intval($_POST['ea_id'] ?? 0), $intActor, $intEveId); + if (($arrResult['state'] ?? '') === 'success') { + header('Location: ' . $strSelfUrl . '&ok=' . urlencode( + fxEveAccesPromoteurT('eve_acces_promoteur_ok_revoked', $strLangue) + )); + exit; + } + $strError = fxEveAccesPromoteurT('eve_acces_promoteur_err_revoke', $strLangue); + } +} + +if (!empty($_GET['ok'])) { + $strFlash = (string)$_GET['ok']; +} + +fxEveAccesPromoteurRenderPage($intEveId, $strLangue, $strFlash, $strError); diff --git a/php/inc_fx_eve_acces.php b/php/inc_fx_eve_acces.php index 9dd299f..2277509 100644 --- a/php/inc_fx_eve_acces.php +++ b/php/inc_fx_eve_acces.php @@ -93,6 +93,74 @@ function fxEveAccesGetRoles($blnActifOnly = true) return $objDatabase->fxGetResults($sql); } +/** Kits proposables par un promoteur (role_delegable = 1, hors migration_legacy). */ +function fxEveAccesGetDelegableRoles() +{ + global $objDatabase; + + if (!fxEveAccesIsEnabled()) { + return array(); + } + + $blnHasDelegable = false; + $tabCol = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_eve_roles LIKE 'role_delegable'"); + if ($tabCol != null && count($tabCol) > 0) { + $blnHasDelegable = true; + } + + if (!$blnHasDelegable) { + return array(); + } + + $sql = "SELECT role_id, role_code, role_label_fr, role_label_en, role_description_fr + FROM inscriptions_eve_roles + WHERE role_actif = 1 + AND role_delegable = 1 + AND role_code <> 'migration_legacy' + ORDER BY role_tri ASC, role_label_fr ASC"; + $tab = $objDatabase->fxGetResults($sql); + + return ($tab != null) ? $tab : array(); +} + +/** Le kit est-il délégable (invite promoteur) ? */ +function fxEveAccesRoleIsDelegable($intRoleId) +{ + global $objDatabase; + + $intRoleId = intval($intRoleId); + if ($intRoleId <= 0 || !fxEveAccesIsEnabled()) { + return false; + } + + $tabCol = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_eve_roles LIKE 'role_delegable'"); + if ($tabCol == null || count($tabCol) === 0) { + return false; + } + + $row = $objDatabase->fxGetRow( + "SELECT role_id, role_code, role_delegable, role_actif + FROM inscriptions_eve_roles WHERE role_id = $intRoleId LIMIT 1" + ); + if ($row === null || intval($row['role_actif']) !== 1) { + return false; + } + if (($row['role_code'] ?? '') === 'migration_legacy') { + return false; + } + + return intval($row['role_delegable'] ?? 0) === 1; +} + +function fxEveAccesCanTeamManage($intComId, $intEveId) +{ + if (fxEveAccesIsSuperAdminSession()) { + return true; + } + + return fxEveAccesHasPermission(intval($intComId), intval($intEveId), 'team.manage'); +} + /** Promoteur peut gérer / inviter des accès v2 sur cet événement ? */ function fxEveAccesCanTeamInvite($intComId, $intEveId) { @@ -121,7 +189,7 @@ function fxEveAccesListByEveId($intEveId, $blnActifOnly = true) } $sql = "SELECT ea.ea_id, ea.com_id, ea.eve_id, ea.role_id, ea.ea_statut, - ea.ea_created_at, ea.ea_expires_at, ea.ea_expire_days, + ea.ea_created_at, ea.ea_expires_at, ea.ea_expire_days, ea.ea_granted_by, r.role_code, r.role_label_fr, r.role_label_en, c.com_prenom, c.com_nom, c.com_courriel, c.com_lastvisit FROM inscriptions_eve_acces ea @@ -697,9 +765,19 @@ function fxEveAccesGrant($intComId, $intEveId, $intRoleId, $intExpireDays, $intG $intEaId = intval($objDatabase->fxGetVar( "SELECT ea_id FROM inscriptions_eve_acces - WHERE com_id = $intComId AND eve_id = $intEveId LIMIT 1" + WHERE com_id = $intComId AND eve_id = $intEveId AND role_id = $intRoleId + LIMIT 1" )); + if ($intEaId <= 0) { + // Repli si unique legacy (com+eve) encore en place sur certains environnements + $intEaId = intval($objDatabase->fxGetVar( + "SELECT ea_id FROM inscriptions_eve_acces + WHERE com_id = $intComId AND eve_id = $intEveId + ORDER BY ea_id DESC LIMIT 1" + )); + } + if ($intEaId <= 0) { return array('state' => 'error', 'message' => 'Acces non enregistre'); } diff --git a/php/inc_fx_eve_acces_promoteur.php b/php/inc_fx_eve_acces_promoteur.php index d68bcd5..720ffb4 100644 --- a/php/inc_fx_eve_acces_promoteur.php +++ b/php/inc_fx_eve_acces_promoteur.php @@ -33,6 +33,43 @@ function fxEveAccesPromoteurT($strClef, $strLangue = 'fr') 'eve_acces_promoteur_col_statut' => array('fr' => 'Statut', 'en' => 'Status'), 'eve_acces_promoteur_col_depuis' => array('fr' => 'Depuis', 'en' => 'Since'), 'eve_acces_promoteur_col_expire' => array('fr' => 'Expire', 'en' => 'Expires'), + 'eve_acces_promoteur_col_action' => array('fr' => 'Action', 'en' => 'Action'), + 'eve_acces_promoteur_field_courriel' => array('fr' => 'Courriel', 'en' => 'Email'), + 'eve_acces_promoteur_field_prenom' => array('fr' => 'Prénom', 'en' => 'First name'), + 'eve_acces_promoteur_field_nom' => array('fr' => 'Nom', 'en' => 'Last name'), + 'eve_acces_promoteur_field_kit' => array('fr' => 'Kit d\'accès', 'en' => 'Access kit'), + 'eve_acces_promoteur_field_jours' => array('fr' => 'Durée (jours)', 'en' => 'Duration (days)'), + 'eve_acces_promoteur_jours_help' => array( + 'fr' => '0 = sans expiration. Utilisé surtout pour les nouveaux accès.', + 'en' => '0 = no expiration. Mainly for new access grants.', + ), + 'eve_acces_promoteur_btn_invite' => array('fr' => 'Assigner l\'accès', 'en' => 'Grant access'), + 'eve_acces_promoteur_btn_revoke' => array('fr' => 'Retirer', 'en' => 'Revoke'), + 'eve_acces_promoteur_no_kits' => array( + 'fr' => 'Aucun kit délégable disponible. Demandez à un super admin d\'activer « délégable » sur un kit.', + 'en' => 'No delegable kit available. Ask a super admin to mark a kit as delegable.', + ), + 'eve_acces_promoteur_ok_existing' => array( + 'fr' => 'Accès assigné au compte existant. Un courriel a été envoyé.', + 'en' => 'Access granted to the existing account. An email was sent.', + ), + 'eve_acces_promoteur_ok_created' => array( + 'fr' => 'Compte créé et accès assigné. Un courriel avec le lien de mot de passe a été envoyé.', + 'en' => 'Account created and access granted. An email with the password link was sent.', + ), + 'eve_acces_promoteur_ok_revoked' => array('fr' => 'Accès retiré.', 'en' => 'Access revoked.'), + 'eve_acces_promoteur_err_courriel' => array('fr' => 'Courriel invalide.', 'en' => 'Invalid email.'), + 'eve_acces_promoteur_err_kit' => array('fr' => 'Kit non autorisé.', 'en' => 'Kit not allowed.'), + 'eve_acces_promoteur_err_nom' => array( + 'fr' => 'Prénom et nom requis pour créer un nouveau compte.', + 'en' => 'First and last name required to create a new account.', + ), + 'eve_acces_promoteur_err_inactive' => array( + 'fr' => 'Un compte existe avec ce courriel mais il est inactif. Contactez le support.', + 'en' => 'An account exists with this email but it is inactive. Contact support.', + ), + 'eve_acces_promoteur_err_revoke' => array('fr' => 'Retrait impossible.', 'en' => 'Unable to revoke.'), + 'eve_acces_promoteur_err_generic' => array('fr' => 'Une erreur est survenue.', 'en' => 'An error occurred.'), ); $strLang = ($strLangue === 'en') ? 'en' : 'fr'; if (isset($arrFallback[$strClef][$strLang])) { @@ -63,8 +100,361 @@ function fxEveAccesPromoteurMenuLabel($strLangue = 'fr') return fxEveAccesPromoteurT('tableau_promoteur_menu_eve_acces', $strLangue); } -/** MSIN-4401 — rendu page promoteur (liste + emplacement invitation). */ -function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') +function fxEveAccesPromoteurHasInviteTeamColumn() +{ + global $objDatabase; + + static $blnHas = null; + if ($blnHas !== null) { + return $blnHas; + } + + $tab = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_invite_team'"); + $blnHas = ($tab != null && count($tab) > 0); + + return $blnHas; +} + +/** + * MSIN-4401 — crée un compte MS1 minimal pour invitation promoteur. + * + * @return array{state:string,message?:string,com_id?:int,reset_token?:string} + */ +function fxEveAccesPromoteurCreateCompteInvite($strCourriel, $strPrenom, $strNom, $strLangue, $intGrantedBy) +{ + global $objDatabase; + + $strCourriel = trim(strtolower((string)$strCourriel)); + $strPrenom = trim((string)$strPrenom); + $strNom = trim((string)$strNom); + $strLangue = ($strLangue === 'en') ? 'en' : 'fr'; + + if ($strCourriel === '' || !filter_var($strCourriel, FILTER_VALIDATE_EMAIL)) { + return array('state' => 'error', 'message' => 'courriel'); + } + if ($strPrenom === '' || $strNom === '') { + return array('state' => 'error', 'message' => 'nom'); + } + + $strToken = uniqid('inv', true); + $strPasswordHash = password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT); + $strNow = fxGetDateTime(); + $strInviteFlag = fxEveAccesPromoteurHasInviteTeamColumn() + ? ', com_invite_team = 1' + : ''; + + $blnHasConfirme = false; + $tabConfirme = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_confirme'"); + if ($tabConfirme != null && count($tabConfirme) > 0) { + $blnHasConfirme = true; + } + $blnHasResetDate = false; + $tabResetDate = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_date'"); + if ($tabResetDate != null && count($tabResetDate) > 0) { + $blnHasResetDate = true; + } + + $blnHasNoteInterne = false; + $tabNote = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_note_interne'"); + if ($tabNote != null && count($tabNote) > 0) { + $blnHasNoteInterne = true; + } + $blnHasResetToken = false; + $tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'"); + if ($tabResetTok != null && count($tabResetTok) > 0) { + $blnHasResetToken = true; + } + + $sql = "INSERT INTO inscriptions_comptes SET + com_prenom = '" . $objDatabase->fxEscape($strPrenom) . "', + com_nom = '" . $objDatabase->fxEscape($strNom) . "', + com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "', + com_login = '" . $objDatabase->fxEscape($strCourriel) . "', + com_password = '" . $objDatabase->fxEscape($strPasswordHash) . "', + com_langue = '" . $objDatabase->fxEscape($strLangue) . "', + com_actif = 1, + com_creation = '" . $objDatabase->fxEscape($strNow) . "' + $strInviteFlag"; + + if ($blnHasConfirme) { + $sql .= ', com_confirme = 1'; + } + if ($blnHasResetToken) { + $sql .= ", com_reset_token = '" . $objDatabase->fxEscape($strToken) . "'"; + } + if ($blnHasResetDate) { + $sql .= ", com_reset_date = '" . $objDatabase->fxEscape($strNow) . "'"; + } + if ($blnHasNoteInterne) { + $sql .= ", com_note_interne = 'MSIN-4401 invitation promoteur (par com_id " + . intval($intGrantedBy) . ")'"; + } + + if ($objDatabase->fxQuery($sql) === false) { + return array('state' => 'error', 'message' => 'insert'); + } + + $intComId = intval($objDatabase->fxGetVar( + "SELECT com_id FROM inscriptions_comptes + WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "' + ORDER BY com_id DESC LIMIT 1" + )); + + if ($intComId <= 0) { + return array('state' => 'error', 'message' => 'insert'); + } + + return array( + 'state' => 'success', + 'com_id' => $intComId, + 'reset_token' => $strToken, + ); +} + +function fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnNewAccount, $strResetToken = '') +{ + global $vDomaine, $vEmail, $vClient; + + if (!function_exists('fxSendMail')) { + require_once __DIR__ . '/inc_mail.php'; + } + if (!function_exists('fxGetMailFormat')) { + require_once __DIR__ . '/inc_fonctions.php'; + } + + $strLangue = ($strLangue === 'en') ? 'en' : 'fr'; + $strEveNom = fxUnescape($arrEve['eve_nom_' . $strLangue] ?? ($arrEve['eve_nom_fr'] ?? '')); + $strKit = ($strLangue === 'en') + ? ($arrRole['role_label_en'] ?? $arrRole['role_label_fr'] ?? '') + : ($arrRole['role_label_fr'] ?? ''); + $strPrenom = fxUnescape($arrCompte['com_prenom'] ?? ''); + $strLogin = $arrCompte['com_login'] ?? $arrCompte['com_courriel']; + $intEveId = intval($arrEve['eve_id'] ?? 0); + $strEveParam = urlencode(base64_encode($intEveId)); + $strComptePath = ($strLangue === 'en') ? '/account' : '/compte'; + $strAccessUrl = $vDomaine . $strComptePath . '/inc_tableau_eve_acces?promoteur_eve_id=' . $strEveParam; + + if ($blnNewAccount && $strResetToken !== '') { + if ($strLangue === 'fr') { + $subject = 'Accès promoteur — créez votre mot de passe'; + $message = 'Bonjour ' . $strPrenom . ',

' + . 'Un accès vous a été accordé pour ' . htmlspecialchars($strEveNom, ENT_QUOTES, 'UTF-8') . '' + . ' (kit : ' . htmlspecialchars($strKit, ENT_QUOTES, 'UTF-8') . ').

' + . 'Un compte a été créé avec le courriel ' . htmlspecialchars($strLogin, ENT_QUOTES, 'UTF-8') . '.
' + . 'Cliquez ci-dessous pour choisir votre mot de passe, puis connectez-vous.'; + $attachment_label = 'Choisir mon mot de passe'; + } else { + $subject = 'Promoter access — set your password'; + $message = 'Hello ' . $strPrenom . ',

' + . 'You were granted access to ' . htmlspecialchars($strEveNom, ENT_QUOTES, 'UTF-8') . '' + . ' (kit: ' . htmlspecialchars($strKit, ENT_QUOTES, 'UTF-8') . ').

' + . 'An account was created with ' . htmlspecialchars($strLogin, ENT_QUOTES, 'UTF-8') . '.
' + . 'Click below to set your password, then sign in.'; + $attachment_label = 'Set my password'; + } + $attachment_url = $vDomaine . $strComptePath . '/reset?t=' . urlencode($strResetToken) + . '&m=' . urlencode($arrCompte['com_courriel']) + . '&id=' . intval($arrCompte['com_id']); + } else { + if ($strLangue === 'fr') { + $subject = 'Nouvel accès — ' . $strEveNom; + $message = 'Bonjour ' . $strPrenom . ',

' + . 'Un accès vous a été accordé pour ' . htmlspecialchars($strEveNom, ENT_QUOTES, 'UTF-8') . '' + . ' (kit : ' . htmlspecialchars($strKit, ENT_QUOTES, 'UTF-8') . ').

' + . 'Connectez-vous avec votre compte existant (' . htmlspecialchars($strLogin, ENT_QUOTES, 'UTF-8') . ').'; + $attachment_label = 'Ouvrir mon compte'; + } else { + $subject = 'New access — ' . $strEveNom; + $message = 'Hello ' . $strPrenom . ',

' + . 'You were granted access to ' . htmlspecialchars($strEveNom, ENT_QUOTES, 'UTF-8') . '' + . ' (kit: ' . htmlspecialchars($strKit, ENT_QUOTES, 'UTF-8') . ').

' + . 'Sign in with your existing account (' . htmlspecialchars($strLogin, ENT_QUOTES, 'UTF-8') . ').'; + $attachment_label = 'Open my account'; + } + $attachment_url = $vDomaine . $strComptePath; + } + + $body = fxGetMailFormat(false, false, true, false, $strLangue); + $body = str_replace('%logo%', $vDomaine . '/images/logoms1inscription.png', $body); + $body = str_replace('%to_mail%', $arrCompte['com_courriel'], $body); + $body = str_replace('%subject%', $subject, $body); + $body = str_replace('%message%', $message, $body); + $body = str_replace('%attachment_url%', $attachment_url, $body); + $body = str_replace('%attachment_label%', $attachment_label, $body); + + fxSendMail( + $subject, + $body, + $arrCompte['com_courriel'], + trim(($arrCompte['com_prenom'] ?? '') . ' ' . ($arrCompte['com_nom'] ?? '')), + $vEmail, + $vClient, + 1, + 0 + ); + + return true; +} + +/** + * MSIN-4401 — invitation / assignation par promoteur. + * + * @return array{state:string,message:string,created?:bool} + */ +function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangue = 'fr') +{ + global $objDatabase; + + $intEveId = intval($intEveId); + $intGrantedBy = intval($intGrantedBy); + $strLangue = ($strLangue === 'en') ? 'en' : 'fr'; + + if (!fxEveAccesCanTeamInvite($intGrantedBy, $intEveId)) { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue)); + } + + $strCourriel = trim(strtolower((string)($arrPost['invite_courriel'] ?? ''))); + $strPrenom = trim((string)($arrPost['invite_prenom'] ?? '')); + $strNom = trim((string)($arrPost['invite_nom'] ?? '')); + $intRoleId = intval($arrPost['invite_role_id'] ?? 0); + $intJours = intval($arrPost['invite_jours'] ?? 0); + if ($intJours < 0) { + $intJours = 0; + } + if ($intJours > 3650) { + $intJours = 3650; + } + + if ($strCourriel === '' || !filter_var($strCourriel, FILTER_VALIDATE_EMAIL)) { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_courriel', $strLangue)); + } + if (!fxEveAccesRoleIsDelegable($intRoleId)) { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_kit', $strLangue)); + } + + $arrEve = $objDatabase->fxGetRow( + 'SELECT eve_id, eve_nom_fr, eve_nom_en FROM inscriptions_evenements WHERE eve_id = ' . $intEveId . ' LIMIT 1' + ); + $arrRole = $objDatabase->fxGetRow( + 'SELECT role_id, role_code, role_label_fr, role_label_en FROM inscriptions_eve_roles WHERE role_id = ' . $intRoleId . ' LIMIT 1' + ); + if ($arrEve === null || $arrRole === null) { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue)); + } + + $rowExisting = $objDatabase->fxGetRow( + "SELECT com_id, com_prenom, com_nom, com_courriel, com_login, com_actif + FROM inscriptions_comptes + WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "' + ORDER BY com_actif DESC, com_id DESC + LIMIT 1" + ); + + $blnCreated = false; + $strResetToken = ''; + $arrCompte = null; + + if ($rowExisting !== null) { + if (intval($rowExisting['com_actif']) !== 1) { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue)); + } + $arrCompte = $rowExisting; + if ($strPrenom !== '' && trim((string)$arrCompte['com_prenom']) === '') { + $objDatabase->fxQuery( + "UPDATE inscriptions_comptes SET com_prenom = '" . $objDatabase->fxEscape($strPrenom) . "' + WHERE com_id = " . intval($arrCompte['com_id']) + ); + $arrCompte['com_prenom'] = $strPrenom; + } + if ($strNom !== '' && trim((string)$arrCompte['com_nom']) === '') { + $objDatabase->fxQuery( + "UPDATE inscriptions_comptes SET com_nom = '" . $objDatabase->fxEscape($strNom) . "' + WHERE com_id = " . intval($arrCompte['com_id']) + ); + $arrCompte['com_nom'] = $strNom; + } + } else { + if ($strPrenom === '' || $strNom === '') { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_nom', $strLangue)); + } + $arrCreate = fxEveAccesPromoteurCreateCompteInvite( + $strCourriel, + $strPrenom, + $strNom, + $strLangue, + $intGrantedBy + ); + if (($arrCreate['state'] ?? '') !== 'success') { + return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue)); + } + $blnCreated = true; + $strResetToken = (string)($arrCreate['reset_token'] ?? ''); + $arrCompte = array( + 'com_id' => intval($arrCreate['com_id']), + 'com_prenom' => $strPrenom, + 'com_nom' => $strNom, + 'com_courriel' => $strCourriel, + 'com_login' => $strCourriel, + ); + } + + $arrGrant = fxEveAccesGrant( + intval($arrCompte['com_id']), + $intEveId, + $intRoleId, + $intJours, + $intGrantedBy, + 'MSIN-4401 invitation promoteur' + ); + + if (($arrGrant['state'] ?? '') !== 'success') { + return array( + 'state' => 'error', + 'message' => $arrGrant['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue), + ); + } + + fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnCreated, $strResetToken); + + return array( + 'state' => 'success', + 'created' => $blnCreated, + 'message' => $blnCreated + ? fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue) + : fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue), + ); +} + +/** + * MSIN-4401 — révocation par le promoteur (team.manage ou auteur du grant / super admin). + */ +function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId) +{ + global $objDatabase; + + $intEaId = intval($intEaId); + $intActorComId = intval($intActorComId); + $intEveId = intval($intEveId); + + $tab = $objDatabase->fxGetRow( + 'SELECT * FROM inscriptions_eve_acces WHERE ea_id = ' . $intEaId . ' LIMIT 1' + ); + if ($tab === null || intval($tab['eve_id']) !== $intEveId) { + return array('state' => 'error'); + } + + $blnOk = fxEveAccesCanTeamManage($intActorComId, $intEveId) + || (intval($tab['ea_granted_by'] ?? 0) === $intActorComId && fxEveAccesCanTeamInvite($intActorComId, $intEveId)); + + if (!$blnOk) { + return array('state' => 'error'); + } + + return fxEveAccesRevoke($intEaId, $intActorComId); +} + +/** MSIN-4401 — rendu page promoteur (invitation + liste). */ +function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr', $strFlash = '', $strError = '') { global $vDomaine; @@ -75,6 +465,11 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') ? fxGetPromoteurBackUrl($intEveId, $strPage) : ($vDomaine . $strPage); + $intActor = intval($_SESSION['com_id'] ?? 0); + if (!empty($_SESSION['usa_id'])) { + $intActor = intval($_SESSION['usa_id']); + } + $arrEve = function_exists('fxGetEvenementsId') ? fxGetEvenementsId($intEveId) : null; $strEveNom = ''; if (is_array($arrEve) && !empty($arrEve['eve_nom_' . $strLangue])) { @@ -82,7 +477,11 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') } $arrRows = fxEveAccesListByEveId($intEveId, true); + $arrRoles = fxEveAccesGetDelegableRoles(); $strRoleLabelKey = ($strLangue === 'en') ? 'role_label_en' : 'role_label_fr'; + $blnCanManage = fxEveAccesCanTeamManage($intActor, $intEveId); + $strFormAction = $vDomaine . $strPage . '/inc_tableau_eve_acces?promoteur_eve_id=' + . urlencode(base64_encode($intEveId)) . '&lng=' . $strLangue; echo '
'; @@ -104,14 +503,71 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') echo '

' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_intro', $strLangue)) . '

'; echo '
'; - // Emplacement invitation (étape suivante) — même format de carte v2 + if ($strFlash !== '') { + echo '
' . fxEveAccesPromoteurEsc($strFlash) . '
'; + } + if ($strError !== '') { + echo '
' . fxEveAccesPromoteurEsc($strError) . '
'; + } + echo '
'; echo '
'; echo ''; echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_invite_title', $strLangue)); echo '
'; echo '
'; - echo '

' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_invite_soon', $strLangue)) . '

'; + + if (count($arrRoles) === 0) { + echo '

' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_no_kits', $strLangue)) . '

'; + } else { + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_jours_help', $strLangue)) . ''; + echo '
'; + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo '

' + . fxEveAccesPromoteurEsc( + $strLangue === 'en' + ? 'If the email already has an MS1 account, the kit is added to that account. Otherwise a new account is created.' + : 'Si le courriel a déjà un compte MS1, le kit est ajouté à ce compte. Sinon un nouveau compte est créé.' + ) + . '

'; + echo '
'; + } + echo '
'; echo '
'; @@ -130,10 +586,11 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_statut', $strLangue)) . ''; echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_depuis', $strLangue)) . ''; echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_expire', $strLangue)) . ''; + echo ''; echo ''; if (count($arrRows) === 0) { - echo '' + echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_empty', $strLangue)) . ''; } else { @@ -151,6 +608,9 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') $strBadge = 'info'; } + $blnCanRevoke = $blnCanManage + || (intval($arrRow['ea_granted_by'] ?? 0) === $intActor); + echo ''; echo ''; echo '
' . fxEveAccesPromoteurEsc($strCompte) . '
'; @@ -163,6 +623,19 @@ function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr') . fxEveAccesPromoteurEsc(fxEveAccesPromoteurFormatDt($arrRow['ea_created_at'] ?? '')) . ''; echo '' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurFormatDt($arrRow['ea_expires_at'] ?? '')) . ''; + echo ''; + if ($blnCanRevoke && intval($arrRow['com_id']) !== $intActor) { + echo '
'; + echo ''; + echo ''; + echo '
'; + } + echo ''; echo ''; } } diff --git a/sql/MSIN-4401-com-invite-team-flag.sql b/sql/MSIN-4401-com-invite-team-flag.sql new file mode 100644 index 0000000..b9a2d73 --- /dev/null +++ b/sql/MSIN-4401-com-invite-team-flag.sql @@ -0,0 +1,15 @@ +-- MSIN-4401 — Flag comptes créés via invitation promoteur +-- Idempotent. Safe pilote. + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'inscriptions_comptes' + AND COLUMN_NAME = 'com_invite_team' +); +SET @ddl := IF(@col_exists = 0, + 'ALTER TABLE `inscriptions_comptes` ADD COLUMN `com_invite_team` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT ''MSIN-4401 compte cree via invitation promoteur acces v2'' AFTER `com_actif`', + 'DO 0'); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;