WebOrbiton
v1.0.0.0

StocketBase

478 lines · 23.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 = 'Security check failed, 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 suspend customer accounts.';
  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 account suspended.';
  58. }
  59. $activeTab = 'customers';
  60. }
  61. ​
  62. if ($action === 'reactivate_customer') {
  63. if (!$isSuperAdmin) {
  64. $flashMessage = 'Only the Super Admin can reactivate customer accounts.';
  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 account reactivated.';
  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 = 'All fields are required and role must be valid for your permission level.';
  84. $flashType = 'error';
  85. } elseif ($role === Auth::ROLE_SUPER_ADMIN) {
  86. $flashMessage = 'Only one Super Admin account may exist; it cannot be created here.';
  87. $flashType = 'error';
  88. } elseif (strlen($password) < 10) {
  89. $flashMessage = 'Password must be 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.';
  114. } catch (InvalidArgumentException $exception) {
  115. $flashMessage = $exception->getMessage();
  116. $flashType = 'error';
  117. } catch (Throwable $exception) {
  118. Avatar::delete($newAvatar);
  119. $flashMessage = 'Could not create account (username or email may already exist).';
  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 manage accounts 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 = 'The Super Admin account can only be edited by itself.';
  150. $flashType = 'error';
  151. } elseif ($role === Auth::ROLE_SUPER_ADMIN && $targetAccount['role'] !== Auth::ROLE_SUPER_ADMIN) {
  152. $flashMessage = 'Super Admin role cannot be assigned to another account.';
  153. $flashType = 'error';
  154. } elseif (!$isSelfEdit && !in_array($role, array_merge($assignableRoles, [Auth::ROLE_SUPER_ADMIN]), true)) {
  155. $flashMessage = 'Invalid role for your permission level.';
  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 = 'New password must be 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 = 'Account updated.';
  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 account cannot be deleted.';
  231. $flashType = 'error';
  232. } elseif ((int) $targetAccount['id'] === (int) $currentUser['id']) {
  233. $flashMessage = 'You cannot delete your own account.';
  234. $flashType = 'error';
  235. } elseif (!$isSuperAdmin && !in_array($targetAccount['role'], $assignableRoles, true)) {
  236. $flashMessage = 'You can only manage accounts with a lower role than yours.';
  237. $flashType = 'error';
  238. } else {
  239. $delete = $db->prepare('DELETE FROM team_accounts WHERE id = :id');
  240. $delete->execute(['id' => $targetId]);
  241. Avatar::delete((string) ($targetAccount['avatar_path'] ?? ''));
  242. $flashMessage = 'Account deleted.';
  243. }
  244. }
  245. }
  246. }
  247. ​
  248. $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();
  249. ​
  250. $customers = [];
  251. if ($isSuperAdmin) {
  252. $customers = $usersDb->query('SELECT * FROM user_accounts ORDER BY created_at DESC')->fetchAll();
  253. }
  254. ​
  255. $dashActivePage = 'admin';
  256. $dashPageTitle = 'Team accounts';
  257. ​
  258. require __DIR__ . '/includes/dash-header.php';
  259. ​
  260. ?>
  261. ​
  262. <?php if ($flashMessage !== null): ?>
  263. <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
  264. <?php endif; ?>
  265. ​
  266. <h1 class="dash-title"><?= Icons::icon("users", "icon icon-lg") ?>Accounts</h1>
  267. ​
  268. <?php if ($isSuperAdmin): ?>
  269. <div class="settings-tabs">
  270. <a href="admin.php?tab=team" class="<?= $activeTab === 'team' ? 'active' : '' ?>"><?= Icons::icon("team", "icon icon-sm") ?>Team accounts</a>
  271. <a href="admin.php?tab=customers" class="<?= $activeTab === 'customers' ? 'active' : '' ?>"><?= Icons::icon("book", "icon icon-sm") ?>Customer accounts</a>
  272. </div>
  273. <?php endif; ?>
  274. ​
  275. <?php if ($activeTab === 'customers' && $isSuperAdmin): ?>
  276. ​
  277. <table class="dash-table">
  278. <thead>
  279. <tr>
  280. <th><?= Icons::icon('heading', 'icon icon-sm') ?>Name</th>
  281. <th><?= Icons::icon('user', 'icon icon-sm') ?>Email</th>
  282. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  283. <th><?= Icons::icon('stats', 'icon icon-sm') ?>Registered</th>
  284. <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last login</th>
  285. <th></th>
  286. </tr>
  287. </thead>
  288. <tbody>
  289. <?php foreach ($customers as $customer): ?>
  290. <tr>
  291. <td><?= htmlspecialchars((string) ($customer['display_name'] ?? '—')) ?></td>
  292. <td><?= htmlspecialchars($customer['email']) ?></td>
  293. <td><span class="status-pill status-<?= $customer['status'] === 'active' ? 'published' : 'rejected' ?>"><?= htmlspecialchars($customer['status']) ?></span></td>
  294. <td><?= htmlspecialchars($customer['created_at']) ?></td>
  295. <td><?= htmlspecialchars((string) ($customer['last_login_at'] ?? '—')) ?></td>
  296. <td class="dash-table-actions">
  297. <?php if ($customer['status'] === 'active'): ?>
  298. <form method="post" style="display:inline;" onsubmit="return confirm('Suspend this customer account? They will be logged out and unable to sign in.');">
  299. <?= Csrf::field() ?>
  300. <input type="hidden" name="action" value="suspend_customer">
  301. <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
  302. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('lock', 'icon icon-sm') ?>Suspend</button>
  303. </form>
  304. <?php else: ?>
  305. <form method="post" style="display:inline;">
  306. <?= Csrf::field() ?>
  307. <input type="hidden" name="action" value="reactivate_customer">
  308. <input type="hidden" name="customer_id" value="<?= (int) $customer['id'] ?>">
  309. <button type="submit" class="dash-btn-small dash-btn-success"><?= Icons::icon('check', 'icon icon-sm') ?>Reactivate</button>
  310. </form>
  311. <?php endif; ?>
  312. </td>
  313. </tr>
  314. <?php endforeach; ?>
  315. <?php if (empty($customers)): ?>
  316. <tr>
  317. <td colspan="6">No customer accounts yet.</td>
  318. </tr>
  319. <?php endif; ?>
  320. </tbody>
  321. </table>
  322. ​
  323. <?php else: ?>
  324. ​
  325. <table class="dash-table">
  326. <thead>
  327. <tr>
  328. <th><?= Icons::icon('heading', 'icon icon-sm') ?>Name</th>
  329. <th><?= Icons::icon('user', 'icon icon-sm') ?>Username</th>
  330. <th><?= Icons::icon('hash', 'icon icon-sm') ?>Email</th>
  331. <th><?= Icons::icon('team', 'icon icon-sm') ?>Role</th>
  332. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  333. <th><?= Icons::icon('lock', 'icon icon-sm') ?>2FA</th>
  334. <th><?= Icons::icon('eye', 'icon icon-sm') ?>Last login</th>
  335. <th></th>
  336. </tr>
  337. </thead>
  338. <tbody>
  339. <?php foreach ($accounts as $account): ?>
  340. <tr>
  341. <td><span class="dash-avatar-inline"><?= Avatar::html($account, 'dash-user-avatar') ?><?= htmlspecialchars($account['display_name']) ?></span></td>
  342. <td><?= htmlspecialchars($account['username']) ?></td>
  343. <td><?= htmlspecialchars((string) $account['email']) ?></td>
  344. <td><?= htmlspecialchars(Auth::roleLabel($account['role'])) ?></td>
  345. <td><?= htmlspecialchars($account['status']) ?></td>
  346. <td><?= TwoFactor::isEnabled($account) ? 'On' : '—' ?></td>
  347. <td><?= htmlspecialchars((string) ($account['last_login_at'] ?? '—')) ?></td>
  348. <td class="dash-table-actions">
  349. <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN || (int) $account['id'] === (int) $currentUser['id']): ?>
  350. <button type="button" class="dash-btn-small js-edit-account"
  351. data-id="<?= (int) $account['id'] ?>"
  352. data-display-name="<?= htmlspecialchars($account['display_name'], ENT_QUOTES) ?>"
  353. data-avatar="<?= htmlspecialchars(Avatar::isValidPath((string) ($account['avatar_path'] ?? '')) ? (string) $account['avatar_path'] : '', ENT_QUOTES) ?>"
  354. data-initial="<?= htmlspecialchars(Avatar::initial((string) $account['display_name']), ENT_QUOTES) ?>"
  355. data-role="<?= htmlspecialchars($account['role'], ENT_QUOTES) ?>"
  356. data-status="<?= htmlspecialchars($account['status'], ENT_QUOTES) ?>"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit</button>
  357. <?php endif; ?>
  358. <?php if ($account['role'] !== Auth::ROLE_SUPER_ADMIN && (int) $account['id'] !== (int) $currentUser['id']): ?>
  359. <form method="post" style="display:inline;" onsubmit="return confirm('Delete this account?');">
  360. <?= Csrf::field() ?>
  361. <input type="hidden" name="action" value="delete_account">
  362. <input type="hidden" name="account_id" value="<?= (int) $account['id'] ?>">
  363. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  364. </form>
  365. <?php endif; ?>
  366. </td>
  367. </tr>
  368. <?php endforeach; ?>
  369. </tbody>
  370. </table>
  371. ​
  372. <h2 class="dash-subtitle"><?= Icons::icon('plus', 'icon icon-sm') ?>Create account</h2>
  373. <form method="post" enctype="multipart/form-data">
  374. <?= Csrf::field() ?>
  375. <input type="hidden" name="action" value="create_account">
  376. ​
  377. <label><?= Icons::icon('user', 'icon icon-sm') ?>Username</label>
  378. <input type="text" name="username" required>
  379. ​
  380. <label><?= Icons::icon('hash', 'icon icon-sm') ?>Email</label>
  381. <input type="text" name="email" required>
  382. ​
  383. <label><?= Icons::icon('heading', 'icon icon-sm') ?>Display name</label>
  384. <input type="text" name="display_name">
  385. ​
  386. <label><?= Icons::icon('lock', 'icon icon-sm') ?>Password</label>
  387. <input type="password" name="password" minlength="10" required>
  388. ​
  389. <label><?= Icons::icon('user', 'icon icon-sm') ?>Profile photo (optional, JPG / PNG / WebP, max 2MB)</label>
  390. <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
  391. ​
  392. <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
  393. <select name="role" required>
  394. <?php foreach ($assignableRoles as $role): ?>
  395. <?php if ($role === Auth::ROLE_SUPER_ADMIN) {
  396. continue;
  397. } ?>
  398. <option value="<?= htmlspecialchars($role) ?>"><?= htmlspecialchars(Auth::roleLabel($role)) ?></option>
  399. <?php endforeach; ?>
  400. </select>
  401. ​
  402. <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:16px;"><?= Icons::icon('plus', 'icon icon-sm') ?>Create account</button>
  403. </form>
  404. ​
  405. <h2 class="dash-subtitle" id="edit-account-title" style="display:none;"><?= Icons::icon('edit', 'icon icon-sm') ?>Edit account</h2>
  406. <form method="post" id="edit-account-form" style="display:none;" enctype="multipart/form-data">
  407. <?= Csrf::field() ?>
  408. <input type="hidden" name="action" value="update_account">
  409. <input type="hidden" name="account_id" id="edit_account_id">
  410. ​
  411. <label><?= Icons::icon('user', 'icon icon-sm') ?>Profile photo (JPG / PNG / WebP, max 2MB)</label>
  412. <div class="avatar-edit-preview">
  413. <div class="dash-user-avatar" id="edit_avatar_preview"></div>
  414. <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
  415. </div>
  416. <label><input type="checkbox" name="remove_avatar" id="edit_remove_avatar"> Remove current photo (the initial letter is shown instead)</label>
  417. ​
  418. <label><?= Icons::icon('heading', 'icon icon-sm') ?>Display name</label>
  419. <input type="text" name="display_name" id="edit_display_name">
  420. ​
  421. <label><?= Icons::icon('team', 'icon icon-sm') ?>Role</label>
  422. <select name="role" id="edit_role">
  423. <?php $editableRoleOptions = $isSuperAdmin ? array_merge($assignableRoles, [Auth::ROLE_SUPER_ADMIN]) : $assignableRoles; ?>
  424. <?php foreach (array_unique($editableRoleOptions) as $role): ?>
  425. <option value="<?= htmlspecialchars($role) ?>"><?= htmlspecialchars(Auth::roleLabel($role)) ?></option>
  426. <?php endforeach; ?>
  427. </select>
  428. ​
  429. <label><?= Icons::icon('flag', 'icon icon-sm') ?>Status</label>
  430. <select name="status" id="edit_status">
  431. <option value="active">Active</option>
  432. <option value="suspended">Suspended</option>
  433. </select>
  434. ​
  435. <label><?= Icons::icon('lock', 'icon icon-sm') ?>New password (leave blank to keep current)</label>
  436. <input type="password" name="new_password" minlength="10">
  437. ​
  438. <label><input type="checkbox" name="reset_2fa" id="edit_reset_2fa"> Reset two-factor authentication (use when the user lost their phone and recovery codes)</label>
  439. ​
  440. <div class="publish-actions">
  441. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save changes</button>
  442. <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>
  443. </div>
  444. </form>
  445. ​
  446. <script>
  447. document.querySelectorAll('.js-edit-account').forEach(function(button) {
  448. button.addEventListener('click', function() {
  449. document.getElementById('edit-account-title').style.display = '';
  450. document.getElementById('edit-account-form').style.display = '';
  451. document.getElementById('edit_account_id').value = this.dataset.id;
  452. document.getElementById('edit_display_name').value = this.dataset.displayName;
  453. document.getElementById('edit_role').value = this.dataset.role;
  454. document.getElementById('edit_status').value = this.dataset.status;
  455. document.getElementById('edit_remove_avatar').checked = false;
  456. document.getElementById('edit_reset_2fa').checked = false;
  457. ​
  458. var preview = document.getElementById('edit_avatar_preview');
  459. preview.textContent = '';
  460. preview.classList.toggle('has-image', this.dataset.avatar !== '');
  461. if (this.dataset.avatar !== '') {
  462. var image = document.createElement('img');
  463. image.src = this.dataset.avatar;
  464. image.alt = '';
  465. preview.appendChild(image);
  466. } else {
  467. preview.textContent = this.dataset.initial;
  468. }
  469. document.getElementById('edit-account-form').scrollIntoView({
  470. behavior: 'smooth'
  471. });
  472. });
  473. });
  474. </script>
  475. ​
  476. <?php endif; ?>
  477. ​
  478. <?php require __DIR__ . '/includes/dash-footer.php'; ?>