Dashboard account section, profile API, and CSS account panels

- SSO session auth gating on all protected pages
- dashboard.php: account section (profile form + workspace panel),
  onboarding prompt modal, overview bar extracted to CSS classes,
  dashboard.css linked in page head
- api/profile.php: save/dismiss endpoint for optional profile fields
- assets/css/dashboard.css: account grid, dash-account-panel,
  dash-profile-form, profile-prompt-backdrop modal, overview bar
  classes, dash-section-kicker, dash-tier-badge base styles
- includes/bootstrap.php: dbnToolsMainUserProfile,
  dbnToolsProfileNeedsPrompt, dbnToolsRequirePageAuth
- scripts/sql/004_user_profile_fields.sql: nullable phone, address,
  and profile_prompt_dismissed_at columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:49:34 +02:00
parent 302bb44f70
commit b912ff22bc
16 changed files with 718 additions and 58 deletions
+65
View File
@@ -122,6 +122,33 @@ function dbnToolsIsAuthenticated(): bool
&& (string)($_SESSION['dbn_tools_client_slug'] ?? '') === dbnToolsClientSlug();
}
function dbnToolsSafeReturnPath(mixed $value, string $default = '/dashboard.php'): string
{
$return = trim((string)$value);
if ($return === '' || !str_starts_with($return, '/') || str_starts_with($return, '//')) {
return $default;
}
if (preg_match('/[\r\n]/', $return)) {
return $default;
}
return $return;
}
function dbnToolsMainLoginUrl(?string $returnPath = null): string
{
$return = dbnToolsSafeReturnPath($returnPath ?? ($_SERVER['REQUEST_URI'] ?? '/dashboard.php'), '/dashboard.php');
return 'https://dobetternorge.no/tools-login.php?return=' . urlencode($return);
}
function dbnToolsRequirePageAuth(?string $returnPath = null): void
{
if (dbnToolsIsAuthenticated()) {
return;
}
header('Location: ' . dbnToolsMainLoginUrl($returnPath));
exit;
}
/**
* Validates a signed SSO token from dobetternorge.no.
* Returns the decoded payload array or null on failure.
@@ -148,6 +175,7 @@ function dbnToolsAuthenticatedUser(): ?array
'user_id' => isset($_SESSION['dbn_tools_user_id']) ? (int)$_SESSION['dbn_tools_user_id'] : null,
'client_id' => isset($_SESSION['dbn_tools_client_id']) ? (int)$_SESSION['dbn_tools_client_id'] : null,
'email' => (string)($_SESSION['dbn_tools_user_email'] ?? ''),
'name' => (string)($_SESSION['dbn_tools_user_name'] ?? ''),
'role' => (string)($_SESSION['dbn_tools_user_role'] ?? ''),
];
}
@@ -518,6 +546,43 @@ function dbnmDb(): PDO
return $pdo;
}
function dbnToolsMainUserProfile(int $userId): ?array
{
if ($userId <= 0) {
return null;
}
try {
$stmt = dbnmDb()->prepare(
'SELECT id, username, display_name, email, phone, address_line1, address_line2,
postal_code, city, address_region, country, preferred_language,
profile_prompt_dismissed_at
FROM users
WHERE id = ?
LIMIT 1'
);
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return is_array($row) ? $row : null;
} catch (Throwable $e) {
return null;
}
}
function dbnToolsProfileNeedsPrompt(?array $profile): bool
{
if (!$profile || !empty($profile['profile_prompt_dismissed_at'])) {
return false;
}
foreach (['display_name', 'phone', 'address_line1', 'postal_code', 'city', 'country'] as $field) {
if (trim((string)($profile[$field] ?? '')) === '') {
return true;
}
}
return false;
}
/**
* True when the current session belongs to an SSO user (Google login).
* All SSO sessions go through the credit + tier system (free, plus, pro).
+3
View File
@@ -4,6 +4,9 @@ declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
$layoutIsGuest = !dbnToolsIsAuthenticated();
if ($layoutIsGuest) {
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/dashboard.php');
}
$uiLang = dbnToolsCurrentLanguage();
$navItems = dbnToolsLaunchedTools($uiLang);
+1 -3
View File
@@ -15,9 +15,7 @@ declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
if (!dbnToolsIsAuthenticated()) {
$return = urlencode($_SERVER['REQUEST_URI'] ?? '/dashboard/');
header('Location: /?return=' . $return);
exit;
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/dashboard/');
}
try {
+3 -3
View File
@@ -13,7 +13,7 @@ $_navLang = dbnToolsCurrentLanguage();
$_navTools = dbnToolsLaunchedTools($_navLang);
$_navPath = strtok((string)($_SERVER['REQUEST_URI'] ?? '/'), '?') ?: '/';
$_navOnDash = str_starts_with($_navPath, '/dashboard');
$_navReturnUrl = urlencode($_navPath);
$_navLoginUrl = dbnToolsMainLoginUrl($_navPath);
$_navScriptPath = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? ''));
$_navAssetBase = str_contains($_navScriptPath, '/dashboard/') ? '../assets' : 'assets';
?>
@@ -84,11 +84,11 @@ $_navAssetBase = str_contains($_navScriptPath, '/dashboard/') ? '../assets' : 'a
</nav>
<?php if ($_navGuest): ?>
<a href="/?return=<?= $_navReturnUrl ?>" class="dbn-nav__login">
<a href="<?= htmlspecialchars($_navLoginUrl) ?>" class="dbn-nav__login">
<?= htmlspecialchars(dbnToolsT('nav_login', $_navLang)) ?>
</a>
<?php else: ?>
<a class="dbn-nav__account-link" href="/account.php" title="<?= htmlspecialchars($_navEmail) ?>">
<a class="dbn-nav__account-link" href="/dashboard.php#account" title="<?= htmlspecialchars($_navEmail) ?>">
<?php if ($_navUser !== ''): ?>
<span class="dbn-nav__username"><?= htmlspecialchars($_navUser) ?></span>
<?php endif; ?>