WebOrbiton
v1.0.0.1

StocketBase

592 lines · 30.9 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/database.php';
  7. require_once __DIR__ . '/includes/auth.php';
  8. require_once __DIR__ . '/includes/csrf.php';
  9. require_once __DIR__ . '/includes/avatar.php';
  10. require_once __DIR__ . '/includes/activity-log.php';
  11. require_once __DIR__ . '/includes/two-factor.php';
  12. ​
  13. Auth::boot();
  14. Auth::requireRoleAtLeast(Auth::ROLE_STORE_OWNER);
  15. ​
  16. $currentUser = Auth::user();
  17. $currentRole = Auth::role();
  18. $isSuperAdmin = $currentRole === Auth::ROLE_SUPER_ADMIN;
  19. ​
  20. $assignableRoles = $isSuperAdmin
  21. ? [
  22. Auth::ROLE_SUPER_ADMIN,
  23. Auth::ROLE_STORE_OWNER,
  24. Auth::ROLE_STORE_MANAGER,
  25. Auth::ROLE_CATALOG_EDITOR,
  26. Auth::ROLE_PRODUCT_EDITOR,
  27. Auth::ROLE_CATALOG_ASSISTANT,
  28. ]
  29. : [
  30. Auth::ROLE_STORE_MANAGER,
  31. Auth::ROLE_CATALOG_EDITOR,
  32. Auth::ROLE_PRODUCT_EDITOR,
  33. Auth::ROLE_CATALOG_ASSISTANT,
  34. ];
  35. ​
  36. $flashMessage = null;
  37. $flashType = 'success';
  38. $db = Database::site();
  39. $usersDb = Database::users();
  40. $activeTab = ($_GET['tab'] ?? 'team') === 'customers' ? 'customers' : 'team';
  41. ​
  42. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  43. if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
  44. $flashMessage = 'Your session expired. Please try again.';
  45. $flashType = 'error';
  46. } else {
  47. $action = $_POST['action'] ?? '';
  48. ​
  49. if ($action === 'suspend_customer') {
  50. if (!$isSuperAdmin) {
  51. $flashMessage = 'Only the Super Admin can block customers.';
  52. $flashType = 'error';
  53. } else {
  54. $customerId = (int) ($_POST['customer_id'] ?? 0);
  55. $update = $usersDb->prepare("UPDATE user_accounts SET status = 'suspended' WHERE id = :id");
  56. $update->execute(['id' => $customerId]);
  57. $flashMessage = 'Customer blocked. They can no longer log in.';
  58. }
  59. $activeTab = 'customers';
  60. }
  61. ​
  62. if ($action === 'reactivate_customer') {
  63. if (!$isSuperAdmin) {
  64. $flashMessage = 'Only the Super Admin can unblock customers.';
  65. $flashType = 'error';
  66. } else {
  67. $customerId = (int) ($_POST['customer_id'] ?? 0);
  68. $update = $usersDb->prepare("UPDATE user_accounts SET status = 'active' WHERE id = :id");
  69. $update->execute(['id' => $customerId]);
  70. $flashMessage = 'Customer unblocked.';
  71. }
  72. $activeTab = 'customers';
  73. }
  74. ​
  75. if ($action === 'create_account') {
  76. $username = trim((string) ($_POST['username'] ?? ''));
  77. $email = trim((string) ($_POST['email'] ?? ''));
  78. $displayName = trim((string) ($_POST['display_name'] ?? ''));
  79. $password = (string) ($_POST['password'] ?? '');
  80. $role = (string) ($_POST['role'] ?? '');
  81. ​
  82. if ($username === '' || $email === '' || $password === '' || !in_array($role, $assignableRoles, true)) {
  83. $flashMessage = 'Please fill in every field and pick a role you are allowed to give.';
  84. $flashType = 'error';
  85. } elseif ($role === Auth::ROLE_SUPER_ADMIN) {
  86. $flashMessage = 'There can only be one Super Admin.';
  87. $flashType = 'error';
  88. } elseif (strlen($password) < 10) {
  89. $flashMessage = 'The password needs at least 10 characters.';
  90. $flashType = 'error';
  91. } else {
  92. $newAvatar = null;
  93. try {
  94. if (Avatar::hasUpload($_FILES['avatar'] ?? null)) {
  95. [$newAvatar, $avatarError] = Avatar::store($_FILES['avatar']);
  96. if ($avatarError !== null) {
  97. throw new InvalidArgumentException($avatarError);
  98. }
  99. }
  100. ​
  101. $insert = $db->prepare(
  102. 'INSERT INTO team_accounts (username, email, password_hash, display_name, role, status, avatar_path) VALUES (:username, :email, :hash, :display_name, :role, :status, :avatar)'
  103. );
  104. $insert->execute([
  105. 'username' => $username,
  106. 'email' => $email,
  107. 'hash' => password_hash($password, PASSWORD_DEFAULT),
  108. 'display_name' => $displayName !== '' ? $displayName : $username,
  109. 'role' => $role,
  110. 'status' => 'active',
  111. 'avatar' => $newAvatar,
  112. ]);
  113. $flashMessage = 'Account created. They can log in now.';
  114. } catch (InvalidArgumentException $exception) {
  115. $flashMessage = $exception->getMessage();
  116. $flashType = 'error';
  117. } catch (Throwable $exception) {
  118. Avatar::delete($newAvatar);
  119. $flashMessage = 'That username or email is already taken.';
  120. $flashType = 'error';
  121. }
  122. }
  123. }
  124. ​
  125. if ($action === 'update_account') {
  126. $targetId = (int) ($_POST['account_id'] ?? 0);
  127. $displayName = trim((string) ($_POST['display_name'] ?? ''));
  128. $role = (string) ($_POST['role'] ?? '');
  129. $status = ($_POST['status'] ?? 'active') === 'suspended' ? 'suspended' : 'active';
  130. $newPassword = (string) ($_POST['new_password'] ?? '');
  131. ​
  132. $targetStatement = $db->prepare('SELECT * FROM team_accounts WHERE id = :id LIMIT 1');
  133. $targetStatement->execute(['id' => $targetId]);
  134. $targetAccount = $targetStatement->fetch();
  135. ​
  136. $isSelfEdit = $targetAccount && (int) $targetAccount['id'] === (int) $currentUser['id'];
  137. if ($isSelfEdit) {
  138. $role = (string) $targetAccount['role'];
  139. $status = 'active';
  140. }
  141. ​
  142. if (!$targetAccount) {
  143. $flashMessage = 'Account not found.';
  144. $flashType = 'error';
  145. } elseif (!$isSuperAdmin && !$isSelfEdit && !in_array($targetAccount['role'], $assignableRoles, true)) {
  146. $flashMessage = 'You can only change people with a lower role than yours.';
  147. $flashType = 'error';
  148. } elseif ($targetAccount['role'] === Auth::ROLE_SUPER_ADMIN && (int) $targetAccount['id'] !== (int) $currentUser['id']) {
  149. $flashMessage = 'Only the Super Admin can change their own account.';
  150. $flashType = 'error';
  151. } elseif ($role === Auth::ROLE_SUPER_ADMIN && $targetAccount['role'] !== Auth::ROLE_SUPER_ADMIN) {
  152. $flashMessage = 'You can\'t make someone else Super Admin.';
  153. $flashType = 'error';
  154. } elseif (!$isSelfEdit && !in_array($role, array_merge($assignableRoles, [Auth::ROLE_SUPER_ADMIN]), true)) {
  155. $flashMessage = 'You can\'t give that role.';
  156. $flashType = 'error';
  157. } else {
  158. $fields = ['display_name = :display_name', 'role = :role', 'status = :status'];
  159. $params = [
  160. 'display_name' => $displayName !== '' ? $displayName : $targetAccount['display_name'],
  161. 'role' => $role,
  162. 'status' => $targetAccount['role'] === Auth::ROLE_SUPER_ADMIN ? 'active' : $status,
  163. 'id' => $targetId,
  164. ];
  165. $saveError = null;
  166. $newAvatar = null;
  167. $removeAvatar = isset($_POST['remove_avatar']);
  168. ​
  169. if ($newPassword !== '') {
  170. if (strlen($newPassword) < 10) {
  171. $saveError = 'The new password needs at least 10 characters.';
  172. } else {
  173. $fields[] = 'password_hash = :hash';
  174. $params['hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
  175. }
  176. }
  177. ​
  178. if ($saveError === null && Avatar::hasUpload($_FILES['avatar'] ?? null)) {
  179. [$newAvatar, $saveError] = Avatar::store($_FILES['avatar']);
  180. }
  181. ​
  182. if ($saveError !== null) {
  183. $flashMessage = $saveError;
  184. $flashType = 'error';
  185. } else {
  186. if ($newAvatar !== null) {
  187. $fields[] = 'avatar_path = :avatar';
  188. $params['avatar'] = $newAvatar;
  189. } elseif ($removeAvatar) {
  190. $fields[] = 'avatar_path = NULL';
  191. }
  192. ​
  193. $resetTwoFactor = isset($_POST['reset_2fa']);
  194. if ($resetTwoFactor) {
  195. array_push($fields, 'totp_enabled = 0', 'totp_secret = NULL', 'totp_recovery = NULL', 'totp_last_step = NULL');
  196. }
  197. ​
  198. $update = $db->prepare('UPDATE team_accounts SET ' . implode(', ', $fields) . ' WHERE id = :id');
  199. $update->execute($params);
  200. ​
  201. if ($isSelfEdit && isset($params['hash'])) {
  202. session_regenerate_id(true);
  203. Auth::refreshPasswordFingerprint($targetId);
  204. }
  205. ​
  206. if ($resetTwoFactor) {
  207. ActivityLog::record('security.2fa_reset', 'account', $targetId);
  208. }
  209. ​
  210. if ($newAvatar !== null || $removeAvatar) {
  211. Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
  212. }
  213. ​
  214. $flashMessage = 'Changes saved.';
  215. }
  216. }
  217. }
  218. ​
  219. if ($action === 'delete_account') {
  220. $targetId = (int) ($_POST['account_id'] ?? 0);
  221. ​
  222. $targetStatement = $db->prepare('SELECT * FROM team_accounts WHERE id = :id LIMIT 1');
  223. $targetStatement->execute(['id' => $targetId]);
  224. $targetAccount = $targetStatement->fetch();
  225. ​
  226. if (!$targetAccount) {
  227. $flashMessage = 'Account not found.';
  228. $flashType = 'error';
  229. } elseif ($targetAccount['role'] === Auth::ROLE_SUPER_ADMIN) {
  230. $flashMessage = 'The Super Admin can\'t be deleted.';
  231. $flashType = 'error';
  232. } elseif ((int) $targetAccount['id'] === (int) $currentUser['id']) {
  233. $flashMessage = 'You can\'t delete your own account.';
  234. $flashType = 'error';
  235. } elseif (!$isSuperAdmin && !in_array($targetAccount['role'], $assignableRoles, true)) {
  236. $flashMessage = 'You can only change people with a lower role than yours.';
  237. $flashType = 'error';
  238. } else {
  239. $transferTo = (int) ($_POST['transfer_to'] ?? 0);
  240. $receiver = null;
  241. if ($transferTo > 0 && $transferTo !== $targetId) {
  242. $receiverStatement = $db->prepare('SELECT id, display_name FROM team_accounts WHERE id = :id LIMIT 1');
  243. $receiverStatement->execute(['id' => $transferTo]);
  244. $receiver = $receiverStatement->fetch() ?: null;
  245. }
  246. ​
  247. if ($receiver === null) {
  248. $flashMessage = 'Pick who should take over this person\'s products, pages, ads and discount codes.';
  249. $flashType = 'error';
  250. } else {
  251. try {
  252. $db->beginTransaction();
  253. $transferSql = [
  254. 'UPDATE products SET created_by = :to WHERE created_by = :from',
  255. 'UPDATE pages SET author_id = :to WHERE author_id = :from AND is_home = 0',
  256. 'UPDATE ads SET created_by = :to WHERE created_by = :from',
  257. 'UPDATE discounts SET created_by = :to WHERE created_by = :from',
  258. ];
  259. foreach ($transferSql as $sql) {
  260. $db->prepare($sql)->execute(['to' => (int) $receiver['id'], 'from' => $targetId]);
  261. }
  262. $db->prepare('UPDATE pages SET author_id = NULL WHERE author_id = :from AND is_home = 1')->execute(['from' => $targetId]);
  263. $delete = $db->prepare('DELETE FROM team_accounts WHERE id = :id');
  264. $delete->execute(['id' => $targetId]);
  265. $db->commit();
  266. Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
  267. $flashMessage = 'Account deleted. Everything they made now belongs to ' . $receiver['display_name'] . '.';
  268. } catch (Throwable $exception) {
  269. if ($db->inTransaction()) {
  270. $db->rollBack();
  271. }
  272. error_log('StocketBase: account delete failed: ' . $exception->getMessage());
  273. $flashMessage = 'Something went wrong, so nothing was deleted. Please try again.';
  274. $flashType = 'error';
  275. }
  276. }
  277. }
  278. }
  279. }
  280. }
  281. ​
  282. $accounts = $db->query('SELECT * FROM team_accounts ORDER BY FIELD(role, \'super_admin\',\'store_owner\',\'store_manager\',\'catalog_editor\',\'product_editor\',\'catalog_assistant\'), display_name ASC')->fetchAll();
  283. ​
  284. $ownedCounts = [];
  285. foreach ([
  286. 'products' => 'SELECT created_by AS owner, COUNT(*) AS c FROM products WHERE created_by IS NOT NULL GROUP BY created_by',
  287. 'pages' => 'SELECT author_id AS owner, COUNT(*) AS c FROM pages WHERE author_id IS NOT NULL AND is_home = 0 GROUP BY author_id',
  288. 'ads' => 'SELECT created_by AS owner, COUNT(*) AS c FROM ads WHERE created_by IS NOT NULL GROUP BY created_by',
  289. 'discounts' => 'SELECT created_by AS owner, COUNT(*) AS c FROM discounts WHERE created_by IS NOT NULL GROUP BY created_by',
  290. ] as $ownedKey => $ownedSql) {
  291. try {
  292. foreach ($db->query($ownedSql)->fetchAll() as $ownedRow) {
  293. $ownedCounts[(int) $ownedRow['owner']][$ownedKey] = (int) $ownedRow['c'];
  294. }
  295. } catch (PDOException $exception) {
  296. }
  297. }
  298. ​
  299. $customers = [];
  300. if ($isSuperAdmin) {
  301. $customers = $usersDb->query('SELECT * FROM user_accounts ORDER BY created_at DESC')->fetchAll();
  302. }
  303. ​
  304. $dashActivePage = 'admin';
  305. $dashPageTitle = 'Team';
  306. ​
  307. require __DIR__ . '/includes/dash-header.php';
  308. ​
  309. ?>
  310. ​
  311. <?php if ($flashMessage !== null): ?>
  312. <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
  313. <?php endif; ?>
  314. ​
  315. <h1 class="dash-title"><?= Icons::icon("users", "icon icon-lg") ?>Accounts</h1>
  316. ​
  317. <?php if ($isSuperAdmin): ?>
  318. <div class="settings-tabs">
  319. <a href="admin.php?tab=team" class="<?= $activeTab === 'team' ? 'active' : '' ?>"><?= Icons::icon("team", "icon icon-sm") ?>Your team</a>
  320. <a href="admin.php?tab=customers" class="<?= $activeTab === 'customers' ? 'active' : '' ?>"><?= Icons::icon("book", "icon icon-sm") ?>Customers</a>
  321. </div>
  322. <?php endif; ?>
  323. ​
  324. <?php if ($activeTab === 'customers' && $isSuperAdmin): ?>
  325. ​
  326. <table class="dash-table">
  327. <thead>
  328. <tr>
  329. <th><?= Icons::icon('heading', 'icon icon-sm') ?>Name</th>
  330. <th><?= Icons::icon('user', 'icon icon-sm') ?>Email</th>
  331. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  332. <th><?= Icons::icon('stats', 'icon icon-sm') ?>Registered</th>
  333. <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last seen</th>
  334. <th></th>
  335. </tr>
  336. </thead>
  337. <tbody>
  338. <?php foreach ($customers as $customer): ?>
  339. <tr>
  340. <td><?= htmlspecialchars((string) ($customer['display_name'] ?? 'No name')) ?></td>
  341. <td><?= htmlspecialchars($customer['email']) ?></td>
  342. <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars(StatusLabel::get((string) $customer['status'])) ?></span></td>
  343. <td><?= htmlspecialchars($customer['created_at']) ?></td>
  344. <td><?= htmlspecialchars((string) ($customer['last_login_at'] ?? 'Never')) ?></td>
  345. <td class="dash-table-actions">
  346. <?php if ($customer['status'] === 'active'): ?>
  347. <form method="post" style="display:inline;" onsubmit="return confirm('Block this customer? They will be logged out and won\'t be able to log in.');">
  348. <?= Csrf::field() ?>
  349. <input type="hidden" name="action" value="suspend_customer">
  350. <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
  351. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('lock', 'icon icon-sm') ?>Block</button>
  352. </form>
  353. <?php else: ?>
  354. <form method="post" style="display:inline;">
  355. <?= Csrf::field() ?>
  356. <input type="hidden" name="action" value="reactivate_customer">
  357. <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
  358. <button type="submit" class="dash-btn-small dash-btn-success"><?= Icons::icon('check', 'icon icon-sm') ?>Unblock</button>
  359. </form>
  360. <?php endif; ?>
  361. </td>
  362. </tr>
  363. <?php endforeach; ?>
  364. <?php if (empty($customers)): ?>
  365. <tr>
  366. <td colspan="6">No customers yet.</td>
  367. </tr>
  368. <?php endif; ?>
  369. </tbody>
  370. </table>
  371. ​
  372. <?php else: ?>
  373. ​
  374. <table class="dash-table">
  375. <thead>
  376. <tr>
  377. <th><?= Icons::icon('heading', 'icon icon-sm') ?>Name</th>
  378. <th><?= Icons::icon('user', 'icon icon-sm') ?>Username</th>
  379. <th><?= Icons::icon('hash', 'icon icon-sm') ?>Email</th>
  380. <th><?= Icons::icon('team', 'icon icon-sm') ?>Role</th>
  381. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  382. <th><?= Icons::icon('lock', 'icon icon-sm') ?>2FA</th>
  383. <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last seen</th>
  384. <th></th>
  385. </tr>
  386. </thead>
  387. <tbody>
  388. <?php foreach ($accounts as $account): ?>
  389. <tr>
  390. <td><span class="dash-avatar-inline"><?= Avatar::html($account, 'dash-user-avatar') ?><?= htmlspecialchars($account['display_name']) ?></span></td>
  391. <td><?= htmlspecialchars($account['username']) ?></td>
  392. <td><?= htmlspecialchars((string) $account['email']) ?></td>
  393. <td><?= htmlspecialchars(Auth::roleLabel($account['role'])) ?></td>
  394. <td><?= htmlspecialchars(StatusLabel::get((string) $account['status'])) ?></td>
  395. <td><?= TwoFactor::isEnabled($account) ? 'On' : 'Off' ?></td>
  396. <td><?= htmlspecialchars((string) ($account['last_login_at'] ?? 'Never')) ?></td>
  397. <td class="dash-table-actions">
  398. <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN || (int) $account['id'] === (int) $currentUser['id']): ?>
  399. <button type="button" class="dash-btn-small js-edit-account"
  400. data-id="<?= (int) $account['id'] ?>"
  401. data-display-name="<?= htmlspecialchars($account['display_name'], ENT_QUOTES) ?>"
  402. data-avatar="<?= htmlspecialchars(Avatar::isValidPath((string) ($account['avatar_path'] ?? '')) ? (string) $account['avatar_path'] : '', ENT_QUOTES) ?>"
  403. data-initial="<?= htmlspecialchars(Avatar::initial((string) $account['display_name']), ENT_QUOTES) ?>"
  404. data-role="<?= htmlspecialchars($account['role'], ENT_QUOTES) ?>"
  405. data-status="<?= htmlspecialchars($account['status'], ENT_QUOTES) ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</button>
  406. <?php endif; ?>
  407. <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN && (int) $account['id'] !== (int) $currentUser['id']): ?>
  408. <?php $owned = $ownedCounts[(int) $account['id']] ?? []; ?>
  409. <button type="button" class="dash-btn-small dash-btn-danger js-delete-account"
  410. data-id="<?= (int) $account['id'] ?>"
  411. data-name="<?= htmlspecialchars($account['display_name'], ENT_QUOTES) ?>"
  412. data-products="<?= (int) ($owned['products'] ?? 0) ?>"
  413. data-pages="<?= (int) ($owned['pages'] ?? 0) ?>"
  414. data-ads="<?= (int) ($owned['ads'] ?? 0) ?>"
  415. data-discounts="<?= (int) ($owned['discounts'] ?? 0) ?>"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  416. <?php endif; ?>
  417. </td>
  418. </tr>
  419. <?php endforeach; ?>
  420. </tbody>
  421. </table>
  422. ​
  423. <div class="sidebar-box" id="delete-account-box" style="display:none;margin-top:20px;">
  424. <h2 class="dash-subtitle" style="margin-top:0;"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete account: <span id="delete_account_name"></span></h2>
  425. <p id="delete_account_summary" style="color:var(--muted);margin:0 0 14px;"></p>
  426. <form method="post" id="delete-account-form">
  427. <?= Csrf::field() ?>
  428. <input type="hidden" name="action" value="delete_account">
  429. <input type="hidden" name="account_id" id="delete_account_id" value="">
  430. ​
  431. <label><?= Icons::icon('users', 'icon icon-sm') ?>Who takes over their products, pages, ads and discount codes?</label>
  432. <select name="transfer_to" id="delete_transfer_to" required>
  433. <?php foreach ($accounts as $receiverAccount): ?>
  434. <option value="<?= (int) $receiverAccount['id'] ?>" <?= (int) $receiverAccount['id'] === (int) $currentUser['id'] ? 'selected' : '' ?>><?= htmlspecialchars($receiverAccount['display_name']) ?> (<?= htmlspecialchars(Auth::roleLabel($receiverAccount['role'])) ?>)</option>
  435. <?php endforeach; ?>
  436. </select>
  437. ​
  438. <div class="publish-actions">
  439. <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Hand over and delete</button>
  440. <button type="button" class="dash-btn" onclick="document.getElementById('delete-account-box').style.display='none';"><?= Icons::icon('x', 'icon icon-sm') ?>Cancel</button>
  441. </div>
  442. </form>
  443. </div>
  444. ​
  445. <h2 class="dash-subtitle"><?= Icons::icon('plus', 'icon icon-sm') ?>Add a team member</h2>
  446. <form method="post" enctype="multipart/form-data">
  447. <?= Csrf::field() ?>
  448. <input type="hidden" name="action" value="create_account">
  449. ​
  450. <label><?= Icons::icon('user', 'icon icon-sm') ?>Username</label>
  451. <input type="text" name="username" required>
  452. ​
  453. <label><?= Icons::icon('hash', 'icon icon-sm') ?>Email</label>
  454. <input type="text" name="email" required>
  455. ​
  456. <label><?= Icons::icon('heading', 'icon icon-sm') ?>Name shown in the dashboard</label>
  457. <input type="text" name="display_name">
  458. ​
  459. <label><?= Icons::icon('lock', 'icon icon-sm') ?>Password</label>
  460. <input type="password" name="password" minlength="10" required>
  461. ​
  462. <label><?= Icons::icon('user', 'icon icon-sm') ?>Photo (optional, JPG, PNG or WebP up to 2 MB)</label>
  463. <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
  464. ​
  465. <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
  466. <select name="role" required>
  467. <?php foreach ($assignableRoles as $role): ?>
  468. <?php if ($role === Auth::ROLE_SUPER_ADMIN) {
  469. continue;
  470. } ?>
  471. <option value="<?= htmlspecialchars($role) ?>"><?= htmlspecialchars(Auth::roleLabel($role)) ?></option>
  472. <?php endforeach; ?>
  473. </select>
  474. ​
  475. <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Add to team</button>
  476. </form>
  477. ​
  478. <h2 class="dash-subtitle" id="edit-account-title" style="display:none;"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit team member</h2>
  479. <form method="post" id="edit-account-form" style="display:none;" enctype="multipart/form-data">
  480. <?= Csrf::field() ?>
  481. <input type="hidden" name="action" value="update_account">
  482. <input type="hidden" name="account_id" id="edit_account_id">
  483. ​
  484. <label><?= Icons::icon('user', 'icon icon-sm') ?>Photo (JPG, PNG or WebP up to 2 MB)</label>
  485. <div class="avatar-edit-preview">
  486. <div class="dash-user-avatar" id="edit_avatar_preview"></div>
  487. <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
  488. </div>
  489. <label><input type="checkbox" name="remove_avatar" id="edit_remove_avatar"> Remove the photo (we'll show their first letter instead)</label>
  490. ​
  491. <label><?= Icons::icon('heading', 'icon icon-sm') ?>Name shown in the dashboard</label>
  492. <input type="text" name="display_name" id="edit_display_name">
  493. ​
  494. <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
  495. <select name="role" id="edit_role">
  496. <?php $editableRoleOptions = $isSuperAdmin ? array_merge($assignableRoles, [Auth::ROLE_SUPER_ADMIN]) : $assignableRoles; ?>
  497. <?php foreach (array_unique($editableRoleOptions) as $role): ?>
  498. <option value="<?= htmlspecialchars($role) ?>"><?= htmlspecialchars(Auth::roleLabel($role)) ?></option>
  499. <?php endforeach; ?>
  500. </select>
  501. ​
  502. <label><?= Icons::icon('flag', 'icon icon-sm') ?>Status</label>
  503. <select name="status" id="edit_status">
  504. <option value="active">Active</option>
  505. <option value="suspended">Blocked</option>
  506. </select>
  507. ​
  508. <label><?= Icons::icon('lock', 'icon icon-sm') ?>New password (leave empty to keep the old one)</label>
  509. <input type="password" name="new_password" minlength="10">
  510. ​
  511. <label><input type="checkbox" name="reset_2fa" id="edit_reset_2fa"> Turn off their two-step login (if they lost their phone and backup codes)</label>
  512. ​
  513. <div class="publish-actions">
  514. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save changes</button>
  515. <button type="button" class="dash-btn" onclick="document.getElementById('edit-account-form').style.display='none';document.getElementById('edit-account-title').style.display='none';"><?= Icons::icon('x', 'icon icon-sm') ?>Cancel</button>
  516. </div>
  517. </form>
  518. ​
  519. <script>
  520. document.querySelectorAll('.js-delete-account').forEach(function(button) {
  521. button.addEventListener('click', function() {
  522. var box = document.getElementById('delete-account-box');
  523. var select = document.getElementById('delete_transfer_to');
  524. var targetId = this.dataset.id;
  525. document.getElementById('delete_account_id').value = targetId;
  526. document.getElementById('delete_account_name').textContent = this.dataset.name;
  527. var parts = [];
  528. [['products', 'product'], ['pages', 'page'], ['ads', 'ad'], ['discounts', 'discount code']].forEach(function(entry) {
  529. var count = parseInt(button.dataset[entry[0]] || '0', 10);
  530. if (count > 0) {
  531. parts.push(count + ' ' + entry[1] + (count === 1 ? '' : 's'));
  532. }
  533. });
  534. document.getElementById('delete_account_summary').textContent = parts.length
  535. ? 'This person made ' + parts.join(', ') + '. Nothing gets deleted, it all moves to the person you pick below. Orders and customers stay as they are.'
  536. : 'This person hasn\'t made any products, pages, ads or discount codes. Orders and customers stay as they are.';
  537. var firstAllowed = null;
  538. Array.prototype.forEach.call(select.options, function(option) {
  539. option.disabled = option.value === targetId;
  540. option.hidden = option.value === targetId;
  541. if (!option.disabled && firstAllowed === null) {
  542. firstAllowed = option;
  543. }
  544. });
  545. if (select.selectedOptions.length === 0 || select.selectedOptions[0].disabled) {
  546. select.value = firstAllowed ? firstAllowed.value : '';
  547. }
  548. box.style.display = '';
  549. box.scrollIntoView({ behavior: 'smooth' });
  550. });
  551. });
  552. ​
  553. document.getElementById('delete-account-form').addEventListener('submit', function(event) {
  554. var select = document.getElementById('delete_transfer_to');
  555. var receiver = select.selectedOptions.length ? select.selectedOptions[0].textContent : '';
  556. if (!confirm('Delete ' + document.getElementById('delete_account_name').textContent + ' and give everything they made to ' + receiver + '?')) {
  557. event.preventDefault();
  558. }
  559. });
  560. ​
  561. document.querySelectorAll('.js-edit-account').forEach(function(button) {
  562. button.addEventListener('click', function() {
  563. document.getElementById('edit-account-title').style.display = '';
  564. document.getElementById('edit-account-form').style.display = '';
  565. document.getElementById('edit_account_id').value = this.dataset.id;
  566. document.getElementById('edit_display_name').value = this.dataset.displayName;
  567. document.getElementById('edit_role').value = this.dataset.role;
  568. document.getElementById('edit_status').value = this.dataset.status;
  569. document.getElementById('edit_remove_avatar').checked = false;
  570. document.getElementById('edit_reset_2fa').checked = false;
  571. ​
  572. var preview = document.getElementById('edit_avatar_preview');
  573. preview.textContent = '';
  574. preview.classList.toggle('has-image', this.dataset.avatar !== '');
  575. if (this.dataset.avatar !== '') {
  576. var image = document.createElement('img');
  577. image.src = this.dataset.avatar;
  578. image.alt = '';
  579. preview.appendChild(image);
  580. } else {
  581. preview.textContent = this.dataset.initial;
  582. }
  583. document.getElementById('edit-account-form').scrollIntoView({
  584. behavior: 'smooth'
  585. });
  586. });
  587. });
  588. </script>
  589. ​
  590. <?php endif; ?>
  591. ​
  592. <?php require __DIR__ . '/includes/dash-footer.php'; ?>