feat: connect settings live flow to auth api
This commit is contained in:
+20
-51
@@ -211,13 +211,13 @@
|
||||
<div class="section-card-body">
|
||||
<div class="avatar-upload">
|
||||
<div class="avatar-large" id="profile-avatar">AT</div>
|
||||
<div class="avatar-upload-actions">
|
||||
<div class="avatar-name" id="profile-display-name">Operator</div>
|
||||
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
|
||||
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Change photo</button>
|
||||
</div>
|
||||
<div class="avatar-upload-actions">
|
||||
<div class="avatar-name" id="profile-display-name">Operator</div>
|
||||
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
|
||||
<div class="field-hint">Profile photo upload is not available yet.</div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-bottom:16px;">Avatar will appear in the console and team views.</div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-bottom:16px;">Your name and email are stored on the backend and used across the console.</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
@@ -233,11 +233,12 @@
|
||||
<div class="field-group">
|
||||
<label class="field-label">Email address</label>
|
||||
<input class="field-input" id="field-email" type="email" value="operator@acme.com" autocomplete="email">
|
||||
<div class="field-hint">Changing email requires re-verification. You'll receive a confirmation link.</div>
|
||||
<div class="field-hint">Changing email updates the login identity for the current account.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-hint" id="settings-profile-status" style="margin-bottom:12px;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;">
|
||||
<button class="btn-primary" type="button">Save profile</button>
|
||||
<button class="btn-primary" id="settings-profile-save-btn" type="button">Save profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,75 +443,42 @@
|
||||
<div class="section-card-body" style="padding-bottom:8px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Current password</label>
|
||||
<input class="field-input" type="password" placeholder="••••••••" autocomplete="current-password">
|
||||
<input class="field-input" id="security-current-password" type="password" placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
<label class="field-label">New password</label>
|
||||
<input class="field-input" type="password" placeholder="min. 12 characters" autocomplete="new-password">
|
||||
<input class="field-input" id="security-new-password" type="password" placeholder="min. 12 characters" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Confirm new password</label>
|
||||
<input class="field-input" type="password" placeholder="••••••••" autocomplete="new-password">
|
||||
<input class="field-input" id="security-confirm-password" type="password" placeholder="••••••••" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-hint" id="settings-password-status" style="margin-bottom:12px;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;padding-bottom:8px;">
|
||||
<button class="btn-primary" type="button">Change password</button>
|
||||
<button class="btn-primary" id="settings-password-save-btn" type="button">Change password</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 4px 20px 16px;">
|
||||
<div class="settings-section-divider" style="margin-top:0;"></div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">Two-factor authentication</div>
|
||||
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">TOTP authenticator app</div>
|
||||
<div class="toggle-setting-desc">Use Google Authenticator, 1Password, or any TOTP-compatible app.</div>
|
||||
</div>
|
||||
<span class="badge badge-active" style="margin-right:8px;">Enabled</span>
|
||||
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Configure</button>
|
||||
</div>
|
||||
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">Hardware security key</div>
|
||||
<div class="toggle-setting-desc">FIDO2 / WebAuthn passkey or YubiKey.</div>
|
||||
</div>
|
||||
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Add key</button>
|
||||
</div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">Advanced security</div>
|
||||
<div class="field-hint" style="margin-bottom:12px;">Two-factor authentication, passkeys and session management are not implemented yet. The current live security flow is password-based login with HttpOnly session cookies.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title">Active sessions</div>
|
||||
<div class="section-card-title">Current session</div>
|
||||
</div>
|
||||
<div style="padding: 8px 20px 16px;">
|
||||
<div class="member-row">
|
||||
<div class="member-info">
|
||||
<div class="member-name">Chrome · macOS — <span style="color:var(--green);font-size:12px;">current</span></div>
|
||||
<div class="member-email">Last active: now · 93.184.216.34 (US)</div>
|
||||
<div class="member-name">Browser session — <span style="color:var(--green);font-size:12px;">current</span></div>
|
||||
<div class="member-email">Session expires according to server-side auth settings. Use Log out to revoke the current browser session.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-row">
|
||||
<div class="member-info">
|
||||
<div class="member-name">Safari · iPhone</div>
|
||||
<div class="member-email">Last active: 2 days ago · 93.184.216.34 (US)</div>
|
||||
</div>
|
||||
<button class="btn-icon danger" title="Revoke session">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="member-row">
|
||||
<div class="member-info">
|
||||
<div class="member-name">Firefox · Linux</div>
|
||||
<div class="member-email">Last active: 8 days ago · 172.16.0.4 (Internal)</div>
|
||||
</div>
|
||||
<button class="btn-icon danger" title="Revoke session">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -557,6 +525,7 @@
|
||||
</div>
|
||||
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-top:12px;">Notification preferences are local UI placeholders for now and are not synced to the backend yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,6 +145,23 @@
|
||||
getSession: function() {
|
||||
return request(AUTH_BASE + '/session');
|
||||
},
|
||||
getProfile: function() {
|
||||
return request(AUTH_BASE + '/profile');
|
||||
},
|
||||
updateProfile: function(payload) {
|
||||
return request(AUTH_BASE + '/profile', {
|
||||
method: 'PATCH',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
changePassword: function(payload) {
|
||||
return request(AUTH_BASE + '/password', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
listWorkspaces: function() {
|
||||
return get('/workspaces');
|
||||
},
|
||||
|
||||
+9
-5
@@ -56,6 +56,12 @@
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function replaceSession(session) {
|
||||
sessionCache = session;
|
||||
persistUserMirror(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async function fetchSession(force) {
|
||||
if (!force && sessionCache) {
|
||||
return sessionCache;
|
||||
@@ -66,9 +72,7 @@
|
||||
|
||||
sessionPromise = window.CrankApi.getSession()
|
||||
.then(function(session) {
|
||||
sessionCache = session;
|
||||
persistUserMirror(session);
|
||||
return session;
|
||||
return replaceSession(session);
|
||||
})
|
||||
.catch(function(error) {
|
||||
clearUserMirror();
|
||||
@@ -109,8 +113,7 @@
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
sessionCache = session;
|
||||
persistUserMirror(session);
|
||||
replaceSession(session);
|
||||
window.location.href = homeUrl();
|
||||
}
|
||||
|
||||
@@ -134,6 +137,7 @@
|
||||
|
||||
window.CrankAuth = {
|
||||
fetchSession: fetchSession,
|
||||
replaceSession: replaceSession,
|
||||
guardProtectedPage: guardProtectedPage,
|
||||
guardLoginPage: guardLoginPage,
|
||||
login: login,
|
||||
|
||||
+127
-17
@@ -1,24 +1,132 @@
|
||||
function populateProfile() {
|
||||
try {
|
||||
var user = JSON.parse(localStorage.getItem('crank_user'));
|
||||
if (!user) {
|
||||
var settingsSession = null;
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
return source
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map(function(part) { return part.charAt(0).toUpperCase(); })
|
||||
.join('') || 'CR';
|
||||
}
|
||||
|
||||
function setStatus(id, text, isError) {
|
||||
var node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
node.textContent = text || '';
|
||||
node.style.color = text
|
||||
? (isError ? 'var(--red)' : 'var(--green)')
|
||||
: 'var(--text-muted)';
|
||||
}
|
||||
|
||||
function populateProfile(session) {
|
||||
if (!session || !session.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
var user = session.user;
|
||||
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
|
||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Operator';
|
||||
document.getElementById('profile-email').textContent = user.email || '';
|
||||
|
||||
var firstName = document.getElementById('field-firstname');
|
||||
var email = document.getElementById('field-email');
|
||||
if (firstName) {
|
||||
firstName.value = user.display_name || '';
|
||||
}
|
||||
if (email) {
|
||||
email.value = user.email || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
if (!window.CrankAuth || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsSession = await window.CrankAuth.fetchSession(true);
|
||||
populateProfile(settingsSession);
|
||||
}
|
||||
|
||||
function bindProfileSave() {
|
||||
var button = document.getElementById('settings-profile-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
button.dataset.bound = 'true';
|
||||
|
||||
button.addEventListener('click', async function() {
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Saving…';
|
||||
setStatus('settings-profile-status', '', false);
|
||||
|
||||
try {
|
||||
var session = await window.CrankApi.updateProfile({
|
||||
display_name: document.getElementById('field-firstname').value.trim(),
|
||||
email: document.getElementById('field-email').value.trim(),
|
||||
});
|
||||
settingsSession = session;
|
||||
if (window.CrankAuth && typeof window.CrankAuth.replaceSession === 'function') {
|
||||
window.CrankAuth.replaceSession(session);
|
||||
}
|
||||
populateProfile(session);
|
||||
setStatus('settings-profile-status', 'Profile saved.', false);
|
||||
button.textContent = 'Saved ✓';
|
||||
} catch (error) {
|
||||
setStatus('settings-profile-status', error.message || 'Failed to save profile', true);
|
||||
button.textContent = original;
|
||||
} finally {
|
||||
setTimeout(function() {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindPasswordSave() {
|
||||
var button = document.getElementById('settings-password-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
button.dataset.bound = 'true';
|
||||
|
||||
button.addEventListener('click', async function() {
|
||||
var currentPassword = document.getElementById('security-current-password').value;
|
||||
var newPassword = document.getElementById('security-new-password').value;
|
||||
var confirmPassword = document.getElementById('security-confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setStatus('settings-password-status', 'New password and confirmation do not match.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('profile-avatar').textContent = user.initials || 'AT';
|
||||
document.getElementById('profile-display-name').textContent = user.name || 'Operator';
|
||||
document.getElementById('profile-email').textContent = user.email || '';
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Saving…';
|
||||
setStatus('settings-password-status', '', false);
|
||||
|
||||
var firstName = document.getElementById('field-firstname');
|
||||
var email = document.getElementById('field-email');
|
||||
if (firstName && user.name) {
|
||||
firstName.value = user.name;
|
||||
try {
|
||||
await window.CrankApi.changePassword({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
document.getElementById('security-current-password').value = '';
|
||||
document.getElementById('security-new-password').value = '';
|
||||
document.getElementById('security-confirm-password').value = '';
|
||||
setStatus('settings-password-status', 'Password updated.', false);
|
||||
button.textContent = 'Saved ✓';
|
||||
} catch (error) {
|
||||
setStatus('settings-password-status', error.message || 'Failed to change password', true);
|
||||
button.textContent = original;
|
||||
} finally {
|
||||
setTimeout(function() {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}, 1200);
|
||||
}
|
||||
if (email && user.email) {
|
||||
email.value = user.email;
|
||||
}
|
||||
} catch (_error) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function injectLanguageSwitcher() {
|
||||
@@ -161,8 +269,10 @@ async function loadWorkspaceSettings() {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
populateProfile();
|
||||
injectLanguageSwitcher();
|
||||
bindSectionNavigation();
|
||||
loadProfile();
|
||||
bindProfileSave();
|
||||
bindPasswordSave();
|
||||
loadWorkspaceSettings();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user