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:
+3
-3
@@ -5,10 +5,10 @@ require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/account.php'));
|
||||
dbnToolsRequirePageAuth('/dashboard.php#account');
|
||||
|
||||
header('Location: /dashboard.php#account');
|
||||
exit;
|
||||
}
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$authUser = dbnToolsAuthenticatedUser();
|
||||
|
||||
+2
-1
@@ -15,5 +15,6 @@ if (ini_get('session.use_cookies')) {
|
||||
]);
|
||||
}
|
||||
session_destroy();
|
||||
header('Location: /');
|
||||
$return = 'https://tools.dobetternorge.no/';
|
||||
header('Location: https://dobetternorge.no/logout.php?redirect=' . urlencode($return));
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
dbnToolsRequireMethod('POST');
|
||||
dbnToolsRequireAuth();
|
||||
|
||||
if (!dbnToolsIsFreeTier()) {
|
||||
dbnToolsError('Main Do Better Norge account profile is available for SSO users.', 403, 'sso_required');
|
||||
}
|
||||
|
||||
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
dbnToolsError('Session user is missing.', 401, 'session_required');
|
||||
}
|
||||
|
||||
$input = dbnToolsJsonInput(20000);
|
||||
$currentProfile = dbnToolsMainUserProfile($userId) ?: [];
|
||||
|
||||
function profileString(array $input, string $key, int $maxChars, array $currentProfile): string
|
||||
{
|
||||
$raw = array_key_exists($key, $input) ? $input[$key] : ($currentProfile[$key] ?? '');
|
||||
$value = trim((string)$raw);
|
||||
if (mb_strlen($value, 'UTF-8') > $maxChars) {
|
||||
dbnToolsError("Field {$key} is too long.", 422, 'field_too_long');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
$displayName = profileString($input, 'display_name', 100, $currentProfile);
|
||||
$phone = profileString($input, 'phone', 40, $currentProfile);
|
||||
$addressLine1 = profileString($input, 'address_line1', 180, $currentProfile);
|
||||
$addressLine2 = profileString($input, 'address_line2', 180, $currentProfile);
|
||||
$postalCode = profileString($input, 'postal_code', 32, $currentProfile);
|
||||
$city = profileString($input, 'city', 100, $currentProfile);
|
||||
$addressRegion = profileString($input, 'address_region', 100, $currentProfile);
|
||||
$country = strtoupper(profileString($input, 'country', 2, $currentProfile));
|
||||
$preferredLanguage = dbnToolsNormalizeUiLanguage($input['preferred_language'] ?? ($_SESSION['lang'] ?? 'en'));
|
||||
$dismissPrompt = !empty($input['dismiss_prompt']);
|
||||
|
||||
try {
|
||||
$db = dbnmDb();
|
||||
$stmt = $db->prepare(
|
||||
'UPDATE users
|
||||
SET display_name = NULLIF(?, ""),
|
||||
phone = NULLIF(?, ""),
|
||||
address_line1 = NULLIF(?, ""),
|
||||
address_line2 = NULLIF(?, ""),
|
||||
postal_code = NULLIF(?, ""),
|
||||
city = NULLIF(?, ""),
|
||||
address_region = NULLIF(?, ""),
|
||||
country = NULLIF(?, ""),
|
||||
preferred_language = ?,
|
||||
profile_prompt_dismissed_at = CASE
|
||||
WHEN ? = 1 THEN COALESCE(profile_prompt_dismissed_at, NOW())
|
||||
ELSE profile_prompt_dismissed_at
|
||||
END
|
||||
WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([
|
||||
$displayName,
|
||||
$phone,
|
||||
$addressLine1,
|
||||
$addressLine2,
|
||||
$postalCode,
|
||||
$city,
|
||||
$addressRegion,
|
||||
$country,
|
||||
$preferredLanguage,
|
||||
$dismissPrompt ? 1 : 0,
|
||||
$userId,
|
||||
]);
|
||||
|
||||
if ($displayName !== '') {
|
||||
$_SESSION['dbn_tools_user_name'] = $displayName;
|
||||
$_SESSION['dbn_tools_sso_name'] = $displayName;
|
||||
}
|
||||
$_SESSION['lang'] = $preferredLanguage;
|
||||
|
||||
dbnToolsRespond([
|
||||
'ok' => true,
|
||||
'profile' => dbnToolsMainUserProfile($userId),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[profile] ' . $e->getMessage());
|
||||
dbnToolsError('Could not save profile details.', 500, 'profile_save_failed');
|
||||
}
|
||||
@@ -171,3 +171,265 @@ body[data-dashboard-page] {
|
||||
/* ── Pagination ──────────────────────────────────────────────────────── */
|
||||
.dash-pager { display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; color: rgba(22, 19, 15, 0.55); font-size: 0.85rem; }
|
||||
.dash-pager__actions { display: flex; gap: 0.5rem; }
|
||||
|
||||
/* ── Section kicker ───────────────────────────────────────────────── */
|
||||
.dash-section-kicker {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: rgba(22, 19, 15, 0.5);
|
||||
margin: 0 0 0.2rem;
|
||||
}
|
||||
|
||||
/* ── Tier badge (bg/color set inline by PHP, base shape here) ─────── */
|
||||
.dash-tier-badge {
|
||||
display: inline-block;
|
||||
padding: 0.18rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Overview bar ─────────────────────────────────────────────────── */
|
||||
.dash-overview-bar {
|
||||
background: #fff;
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: var(--dash-radius);
|
||||
padding: 1.1rem 1.5rem;
|
||||
margin: 0 0 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dash-overview-bar__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.dash-overview-bar__credits {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
color: var(--dbn-blue, #00205b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-overview-bar__meta {
|
||||
color: #9ca3af;
|
||||
font-size: 0.92rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-overview-bar__meta strong { color: #374151; }
|
||||
.dash-overview-bar__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dash-overview-bar__action {
|
||||
font-size: 0.92rem;
|
||||
text-decoration: none;
|
||||
padding: 7px 14px;
|
||||
border-radius: 7px;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dash-overview-bar__action--manage {
|
||||
color: #6b7280;
|
||||
border: 1px solid var(--dbn-line);
|
||||
font-weight: 400;
|
||||
}
|
||||
.dash-overview-bar__action--upgrade {
|
||||
background: var(--dbn-blue, #00205b);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dash-overview-bar__action--topup {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
/* ── Account section grid ─────────────────────────────────────────── */
|
||||
.dashboard-account-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
@media (max-width: 760px) { .dashboard-account-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.dash-account-panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: var(--dash-radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
box-shadow: var(--dash-shadow);
|
||||
}
|
||||
|
||||
.dash-account-panel__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.dash-account-panel__head h2 {
|
||||
font-family: "Crimson Pro", "Georgia", serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--dbn-blue, #00205b);
|
||||
}
|
||||
|
||||
.dash-account-chip {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
background: #f3f4f6;
|
||||
color: rgba(22, 19, 15, 0.6);
|
||||
white-space: nowrap;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.dash-account-list { display: flex; flex-direction: column; }
|
||||
.dash-account-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 0.55rem 0;
|
||||
border-bottom: 1px solid var(--dbn-line);
|
||||
font-size: 0.9rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.dash-account-list > div:last-child { border-bottom: none; }
|
||||
.dash-account-list span { color: rgba(22, 19, 15, 0.55); }
|
||||
.dash-account-list strong { font-weight: 600; color: var(--dbn-ink, #16130f); text-align: right; word-break: break-all; }
|
||||
|
||||
.dash-account-note {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(22, 19, 15, 0.45);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Profile form ─────────────────────────────────────────────────── */
|
||||
.dash-profile-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.dash-profile-form .span-2 { grid-column: 1 / -1; }
|
||||
.dash-profile-form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(22, 19, 15, 0.6);
|
||||
font-weight: 500;
|
||||
}
|
||||
.dash-profile-form input {
|
||||
padding: 0.45rem 0.65rem;
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: var(--dash-radius-sm);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
color: var(--dbn-ink, #16130f);
|
||||
background: var(--dbn-paper, #f6f2ea);
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.dash-profile-form input:focus {
|
||||
outline: none;
|
||||
border-color: var(--dbn-blue, #00205b);
|
||||
background: #fff;
|
||||
}
|
||||
.dash-profile-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.dash-profile-save {
|
||||
padding: 0.45rem 1.1rem;
|
||||
background: var(--dbn-blue, #00205b);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--dash-radius-sm);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
}
|
||||
.dash-profile-save:hover { background: #001543; }
|
||||
.dash-profile-status { font-size: 0.8rem; color: rgba(22, 19, 15, 0.55); }
|
||||
|
||||
/* ── Onboarding prompt modal ──────────────────────────────────────── */
|
||||
.profile-prompt-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 900;
|
||||
}
|
||||
.profile-prompt-backdrop[hidden] { display: none; }
|
||||
.profile-prompt-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 2rem 2.25rem;
|
||||
max-width: 540px;
|
||||
width: 92%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.profile-prompt-card h2 {
|
||||
font-family: "Crimson Pro", "Georgia", serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
color: var(--dbn-blue, #00205b);
|
||||
}
|
||||
.profile-prompt-card > p {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(22, 19, 15, 0.6);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.profile-prompt-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.profile-prompt-actions button[type="button"] {
|
||||
padding: 0.45rem 1rem;
|
||||
background: #f3f4f6;
|
||||
color: var(--dbn-ink, #16130f);
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: var(--dash-radius-sm);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.profile-prompt-actions button[type="submit"] {
|
||||
padding: 0.45rem 1.1rem;
|
||||
background: var(--dbn-blue, #00205b);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--dash-radius-sm);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.profile-prompt-actions button:hover { opacity: 0.88; }
|
||||
|
||||
+1
-4
@@ -5,10 +5,7 @@ require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode('/billing.php'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth('/billing.php');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$isSso = dbnToolsIsFreeTier();
|
||||
|
||||
+1
-4
@@ -6,10 +6,7 @@ require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/CaseStore.php';
|
||||
require_once __DIR__ . '/includes/CaseResults.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
||||
|
||||
+208
-15
@@ -4,10 +4,7 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/dashboard.php'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/dashboard.php');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$tools = dbnToolsLaunchedTools($uiLang);
|
||||
@@ -36,6 +33,17 @@ if ($dashAuthUser !== null) {
|
||||
$dashEmail = strstr($e, '@', true) ?: $e;
|
||||
}
|
||||
|
||||
$dashProfile = $dashIsSso ? dbnToolsMainUserProfile($dashUserId) : null;
|
||||
$dashProfileNeedsPrompt = dbnToolsProfileNeedsPrompt($dashProfile);
|
||||
$dashProfileValue = static function (string $key) use ($dashProfile): string {
|
||||
return (string)($dashProfile[$key] ?? '');
|
||||
};
|
||||
$dashSeatLimit = match ($dashTier) {
|
||||
'pro' => 3,
|
||||
default => 1,
|
||||
};
|
||||
$dashTeamLabel = $dashSeatLimit > 1 ? '1 / ' . $dashSeatLimit . ' seats in use' : 'Single-user workspace';
|
||||
|
||||
// Next refill / billing date
|
||||
$dashNextBilling = '';
|
||||
$dashNextBillingKey = 'next_refill';
|
||||
@@ -218,6 +226,7 @@ $langSuffix = $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '';
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
||||
<link rel="stylesheet" href="assets/css/dashboard.css">
|
||||
<style>
|
||||
/* ── Dashboard enhancements ───────────────────────────── */
|
||||
details.dash-mcp-details summary::-webkit-details-marker { display: none; }
|
||||
@@ -258,29 +267,29 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||||
?>
|
||||
|
||||
<!-- ── Account overview bar ─────────────────────────────────────── -->
|
||||
<div style="background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:1.1rem 1.5rem; margin:0 0 1.25rem; display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap;">
|
||||
<div style="display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; min-width:0;">
|
||||
<div class="dash-overview-bar">
|
||||
<div class="dash-overview-bar__left">
|
||||
<span class="dash-tier-badge" style="background:<?= $tierLabel[1] ?>; color:<?= $tierLabel[2] ?>;"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||||
<?php if (!empty($dashDetail['trial_active'])): ?>
|
||||
<span class="dash-tier-badge" style="background:#fef3c7; color:#92400e;"><?= htmlspecialchars(sprintf($dl['trial_badge'], (int)$dashDetail['trial_days_remaining'])) ?></span>
|
||||
<?php endif; ?>
|
||||
<span style="font-size:1.08rem; font-weight:700; color:#00205B; white-space:nowrap;"><?= number_format($eff) ?> <?= $uiLang === 'no' ? 'kred.' : 'credits' ?></span>
|
||||
<span style="color:#9ca3af; font-size:.92rem; white-space:nowrap;"><?= (int)$dashDetail['balance'] ?> <?= $uiLang === 'no' ? 'månedlige' : 'monthly' ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalt' : 'prepaid' ?></span>
|
||||
<span class="dash-overview-bar__credits"><?= number_format($eff) ?> <?= $uiLang === 'no' ? 'kred.' : 'credits' ?></span>
|
||||
<span class="dash-overview-bar__meta"><?= (int)$dashDetail['balance'] ?> <?= $uiLang === 'no' ? 'månedlige' : 'monthly' ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalt' : 'prepaid' ?></span>
|
||||
<?php if ($dashNextBilling): ?>
|
||||
<span style="color:#6b7280; font-size:.92rem; white-space:nowrap;"><?= htmlspecialchars($dl[$dashNextBillingKey]) ?>: <strong style="color:#374151;"><?= htmlspecialchars($dashNextBilling) ?></strong></span>
|
||||
<span class="dash-overview-bar__meta"><?= htmlspecialchars($dl[$dashNextBillingKey]) ?>: <strong><?= htmlspecialchars($dashNextBilling) ?></strong></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($dashEmail): ?>
|
||||
<span style="color:#d1d5db; font-size:.92rem; white-space:nowrap; display:none;" class="dash-email-sep">·</span>
|
||||
<span style="color:#9ca3af; font-size:.92rem; white-space:nowrap;"><?= htmlspecialchars($dl['signed_in_as']) ?>: <strong style="color:#374151;"><?= htmlspecialchars($dashEmail) ?></strong></span>
|
||||
<span class="dash-overview-bar__meta dash-email-sep" style="display:none;">·</span>
|
||||
<span class="dash-overview-bar__meta"><?= htmlspecialchars($dl['signed_in_as']) ?>: <strong><?= htmlspecialchars($dashEmail) ?></strong></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:.5rem; flex-shrink:0; flex-wrap:wrap;">
|
||||
<div class="dash-overview-bar__right">
|
||||
<?php if ($isPaid): ?>
|
||||
<a href="/billing.php" style="color:#6b7280; font-size:.92rem; text-decoration:none; border:1px solid #e5e7eb; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
||||
<a href="/billing.php" class="dash-overview-bar__action dash-overview-bar__action--manage"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
||||
<?php else: ?>
|
||||
<a href="/pricing.php" style="background:#00205B; color:#fff; font-size:.92rem; font-weight:700; text-decoration:none; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
||||
<a href="/pricing.php" class="dash-overview-bar__action dash-overview-bar__action--upgrade"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
||||
<?php endif; ?>
|
||||
<a href="/pricing.php#topup" style="background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; font-size:.92rem; font-weight:600; text-decoration:none; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['top_up']) ?> →</a>
|
||||
<a href="/pricing.php#topup" class="dash-overview-bar__action dash-overview-bar__action--topup"><?= htmlspecialchars($dl['top_up']) ?> →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,6 +332,86 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section id="account" class="dashboard-account-grid" aria-label="Account and profile">
|
||||
<article class="dash-account-panel dash-account-panel--profile">
|
||||
<div class="dash-account-panel__head">
|
||||
<div>
|
||||
<p class="dash-section-kicker">Account</p>
|
||||
<h2>Profile details</h2>
|
||||
</div>
|
||||
<span class="dash-account-chip">Optional</span>
|
||||
</div>
|
||||
<form id="profileForm" class="dash-profile-form">
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($dashProfileValue('display_name')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Phone</span>
|
||||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($dashProfileValue('phone')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 1</span>
|
||||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($dashProfileValue('address_line1')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 2</span>
|
||||
<input name="address_line2" type="text" maxlength="180" autocomplete="address-line2" value="<?= htmlspecialchars($dashProfileValue('address_line2')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Postal code</span>
|
||||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($dashProfileValue('postal_code')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>City</span>
|
||||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($dashProfileValue('city')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Region</span>
|
||||
<input name="address_region" type="text" maxlength="100" autocomplete="address-level1" value="<?= htmlspecialchars($dashProfileValue('address_region')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Country</span>
|
||||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($dashProfileValue('country')) ?>">
|
||||
</label>
|
||||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
||||
<div class="dash-profile-actions span-2">
|
||||
<button type="submit" class="dash-profile-save">Save profile</button>
|
||||
<p id="profileStatus" class="dash-profile-status" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="dash-account-panel">
|
||||
<div class="dash-account-panel__head">
|
||||
<div>
|
||||
<p class="dash-section-kicker">Users</p>
|
||||
<h2>Workspace access</h2>
|
||||
</div>
|
||||
<span class="dash-account-chip"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||||
</div>
|
||||
<div class="dash-account-list">
|
||||
<div>
|
||||
<span>Email</span>
|
||||
<strong><?= htmlspecialchars((string)($dashAuthUser['email'] ?? '')) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Seats</span>
|
||||
<strong><?= htmlspecialchars($dashTeamLabel) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Role</span>
|
||||
<strong>Owner</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Plan storage</span>
|
||||
<strong><?= $isPaid ? htmlspecialchars(round((int)$dashDetail['storage_quota_bytes'] / 1048576) . ' MB') : 'Upgrade for My Case storage' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dash-account-note">Team invitations and seat management will be added after this consolidation pass.</p>
|
||||
</article>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="disclaimer" role="note"><?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?></div>
|
||||
@@ -500,6 +589,47 @@ foreach ($tools as $slug => $item):
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($dashIsSso && $dashProfileNeedsPrompt): ?>
|
||||
<div id="profilePromptModal" class="profile-prompt-backdrop" role="dialog" aria-modal="true" aria-labelledby="profilePromptTitle">
|
||||
<div class="profile-prompt-card">
|
||||
<p class="dash-section-kicker">Optional profile</p>
|
||||
<h2 id="profilePromptTitle">Add contact details for your tools account</h2>
|
||||
<p>These details help support and billing conversations. They are optional and never block tool access.</p>
|
||||
<form id="profilePromptForm" class="dash-profile-form">
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($dashProfileValue('display_name')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Phone</span>
|
||||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($dashProfileValue('phone')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 1</span>
|
||||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($dashProfileValue('address_line1')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Postal code</span>
|
||||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($dashProfileValue('postal_code')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>City</span>
|
||||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($dashProfileValue('city')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Country</span>
|
||||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($dashProfileValue('country')) ?>">
|
||||
</label>
|
||||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
||||
<div class="profile-prompt-actions span-2">
|
||||
<button type="button" id="profilePromptSkip">Skip for now</button>
|
||||
<button type="submit">Save details</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
<script src="assets/js/tools.js" defer></script>
|
||||
@@ -530,6 +660,69 @@ foreach ($tools as $slug => $item):
|
||||
}
|
||||
}());
|
||||
|
||||
/* Profile details */
|
||||
(function () {
|
||||
function payloadFromForm(form, dismiss) {
|
||||
var data = {};
|
||||
if (form) {
|
||||
Array.prototype.forEach.call(form.elements, function (el) {
|
||||
if (!el.name) return;
|
||||
data[el.name] = el.value || '';
|
||||
});
|
||||
}
|
||||
if (dismiss) data.dismiss_prompt = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
function saveProfile(payload) {
|
||||
return fetch('/api/profile.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function (r) {
|
||||
return r.json().then(function (data) {
|
||||
if (!r.ok || !data.ok) {
|
||||
throw new Error(data.error && data.error.message ? data.error.message : 'Could not save profile');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var profileForm = document.getElementById('profileForm');
|
||||
var profileStatus = document.getElementById('profileStatus');
|
||||
if (profileForm) {
|
||||
profileForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (profileStatus) profileStatus.textContent = 'Saving...';
|
||||
saveProfile(payloadFromForm(profileForm, true))
|
||||
.then(function () { if (profileStatus) profileStatus.textContent = 'Saved'; })
|
||||
.catch(function (err) { if (profileStatus) profileStatus.textContent = err.message; });
|
||||
});
|
||||
}
|
||||
|
||||
var promptModal = document.getElementById('profilePromptModal');
|
||||
var promptForm = document.getElementById('profilePromptForm');
|
||||
var promptSkip = document.getElementById('profilePromptSkip');
|
||||
function closePrompt() {
|
||||
if (promptModal) promptModal.hidden = true;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
if (promptModal) document.body.style.overflow = 'hidden';
|
||||
if (promptForm) {
|
||||
promptForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
saveProfile(payloadFromForm(promptForm, true)).then(closePrompt).catch(function (err) { alert(err.message); });
|
||||
});
|
||||
}
|
||||
if (promptSkip) {
|
||||
promptSkip.addEventListener('click', function () {
|
||||
saveProfile(payloadFromForm(promptForm, true)).then(closePrompt).catch(closePrompt);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
||||
/* ── Corpus summary ─────────────────────────────────────────────────── */
|
||||
<?php if ($dashIsSso): ?>
|
||||
(function () {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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; ?>
|
||||
|
||||
@@ -4,23 +4,8 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/PricingCatalog.php';
|
||||
|
||||
function dbnToolsSafeReturn(mixed $value, string $default = '/'): string
|
||||
{
|
||||
$return = trim((string)$value);
|
||||
if ($return === '') {
|
||||
return $default;
|
||||
}
|
||||
if (!str_starts_with($return, '/') || str_starts_with($return, '//')) {
|
||||
return $default;
|
||||
}
|
||||
if (preg_match('/[\r\n]/', $return)) {
|
||||
return $default;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$returnPath = dbnToolsSafeReturn($_GET['return'] ?? '/', '/');
|
||||
$returnPath = dbnToolsSafeReturnPath($_GET['return'] ?? '/', '/');
|
||||
|
||||
// Handle SSO token from dobetternorge.no
|
||||
if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) {
|
||||
@@ -33,9 +18,19 @@ if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) {
|
||||
$_SESSION['dbn_tools_authenticated_at'] = time();
|
||||
$_SESSION['dbn_tools_sso_uid'] = (int)$tokenData['uid'];
|
||||
$_SESSION['dbn_tools_user_id'] = (int)$tokenData['uid'];
|
||||
$_SESSION['dbn_tools_sso_email'] = (string)$tokenData['email'];
|
||||
$_SESSION['dbn_tools_user_email'] = (string)$tokenData['email'];
|
||||
$_SESSION['dbn_tools_sso_name'] = (string)($tokenData['name'] ?? '');
|
||||
$_SESSION['dbn_tools_user_name'] = (string)($tokenData['name'] ?? '');
|
||||
$_SESSION['dbn_tools_user_role'] = 'sso';
|
||||
$_SESSION['dbn_tools_tier'] = (string)($tokenData['tier'] ?? 'free');
|
||||
$_SESSION['lang'] = dbnToolsNormalizeUiLanguage($tokenData['lang'] ?? $uiLang);
|
||||
try {
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
FreeTier::ensureRow((int)$tokenData['uid']);
|
||||
} catch (Throwable) {
|
||||
// Non-fatal: the token has already been verified; credit checks will fail closed if DB is unavailable.
|
||||
}
|
||||
// Send to workbench directly after SSO; honour a specific return path if set
|
||||
header('Location: ' . ($returnPath === '/' ? '/dashboard.php' : $returnPath));
|
||||
exit;
|
||||
|
||||
+1
-4
@@ -6,10 +6,7 @@ require_once __DIR__ . '/includes/FreeTier.php';
|
||||
require_once __DIR__ . '/includes/CaseStore.php';
|
||||
require_once __DIR__ . '/includes/CaseResults.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode('/min-sak.php'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth('/min-sak.php');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
|
||||
|
||||
@@ -7,6 +7,10 @@ require_once __DIR__ . '/includes/PricingCatalog.php';
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$isAuthed = dbnToolsIsAuthenticated();
|
||||
if (!$isAuthed) {
|
||||
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/pricing.php');
|
||||
}
|
||||
$isAuthed = true;
|
||||
$currentTier = $isAuthed ? dbnToolsCurrentTier() : 'free';
|
||||
$surveyDone = false;
|
||||
if ($isAuthed && dbnToolsIsFreeTier()) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Migration 004: optional DBN Tools profile fields
|
||||
-- Run against dobetternorge_maindb:
|
||||
-- mysql -u root dobetternorge_maindb < scripts/sql/004_user_profile_fields.sql
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @db_name := DATABASE();
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN phone VARCHAR(40) NULL AFTER email',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'phone'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN address_line1 VARCHAR(180) NULL AFTER phone',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'address_line1'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN address_line2 VARCHAR(180) NULL AFTER address_line1',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'address_line2'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN postal_code VARCHAR(32) NULL AFTER address_line2',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'postal_code'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN address_region VARCHAR(100) NULL AFTER city',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'address_region'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN profile_prompt_dismissed_at DATETIME NULL AFTER preferred_language',
|
||||
'SELECT 1')
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'users' AND COLUMN_NAME = 'profile_prompt_dismissed_at'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
COMMIT;
|
||||
+1
-4
@@ -3,10 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/workbench.php'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/workbench.php');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$copy = dbnToolsWorkbenchCopy($uiLang);
|
||||
|
||||
Reference in New Issue
Block a user