v0.1.0
This commit is contained in:
@@ -4209,3 +4209,16 @@ dialog::backdrop {
|
||||
.file-code button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.footer-access{font-weight:600;color:var(--muted,#9ca3af)}
|
||||
|
||||
.profile-suggestion {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
#profile-dialog input[readonly] {
|
||||
cursor: default;
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<div id="footer-account-user" class="home-footer__account" hidden>
|
||||
<span id="footer-user-label" class="home-footer__user"></span>
|
||||
<button id="footer-resources" class="footer-action footer-action--primary" type="button">My notes</button>
|
||||
<button id="footer-profile" class="footer-action" type="button">Profile</button>
|
||||
<button id="footer-logout" class="footer-action" type="button">Log out</button>
|
||||
</div>
|
||||
<span class="home-footer__author">Author: <a href="https://www.linuxiarz.pl"
|
||||
@@ -122,6 +123,26 @@
|
||||
<p id="resources-error" class="form-message error" role="alert"></p>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="profile-dialog" class="app-dialog">
|
||||
<form id="profile-form" class="dialog-panel identity-panel" autocomplete="off">
|
||||
<button id="close-profile" class="modal-close" type="button" aria-label="Close dialog">×</button>
|
||||
<header class="identity-panel__header"><h2>Profile</h2><p id="profile-copy" class="dialog-copy">Manage your local RustPad account.</p></header>
|
||||
<div class="identity-fields">
|
||||
<label data-directory-profile-field>Full name<input id="profile-directory-name" readonly></label>
|
||||
<label data-directory-profile-field>Organization<input id="profile-directory-organization" readonly></label>
|
||||
<p id="profile-nickname-suggestion" data-directory-profile-field class="profile-suggestion"></p>
|
||||
<label>Nickname<input id="profile-nickname" maxlength="40" required></label>
|
||||
<label data-local-profile-field>New e-mail<input id="profile-email" type="email" maxlength="320" placeholder="Leave empty to keep current"></label>
|
||||
<label data-local-profile-field>New password<input id="profile-new-password" type="password" minlength="8" maxlength="128" placeholder="Leave empty to keep current"></label>
|
||||
<label data-local-profile-field>Current password<input id="profile-password" type="password" minlength="8" maxlength="128" required></label>
|
||||
</div>
|
||||
<button class="primary-button" type="submit">Save profile</button>
|
||||
<button id="profile-delete" data-local-profile-field class="danger-button" type="button">Delete account</button>
|
||||
<p id="profile-message" class="form-message" role="status"></p>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -121,6 +121,25 @@ export function bindIdentityDialog({ dialog, onIdentity, initialMode = "login" }
|
||||
message.classList.remove("success");
|
||||
message.classList.add("error");
|
||||
message.textContent = error.message;
|
||||
if (/confirm the account/i.test(error.message) && email.value.trim()) {
|
||||
const resend = document.createElement("button");
|
||||
resend.type = "button";
|
||||
resend.className = "text-button resend-confirmation";
|
||||
resend.textContent = "Resend confirmation e-mail";
|
||||
resend.addEventListener("click", async () => {
|
||||
resend.disabled = true;
|
||||
try {
|
||||
const result = await api("/api/auth/resend-confirmation", { method: "POST", body: JSON.stringify({ email: email.value.trim() }) });
|
||||
message.classList.remove("error");
|
||||
message.classList.add("success");
|
||||
message.textContent = result.message;
|
||||
} catch (resendError) {
|
||||
message.textContent = resendError.message;
|
||||
resend.disabled = false;
|
||||
}
|
||||
});
|
||||
message.append(document.createElement("br"), resend);
|
||||
}
|
||||
} finally {
|
||||
submit.disabled = false;
|
||||
}
|
||||
@@ -255,12 +274,14 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
|
||||
}
|
||||
setAuthSession(session);
|
||||
await onIdentity(session.nickname, session);
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
nickname.disabled = false;
|
||||
const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname: name, session_token: getAuthToken() || null }) });
|
||||
setNickname(result.nickname);
|
||||
await onIdentity(result.nickname, null);
|
||||
dialog.close();
|
||||
} catch (error) {
|
||||
message.classList.remove("success");
|
||||
message.classList.add("error");
|
||||
|
||||
+43
-1
@@ -84,6 +84,7 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
|
||||
|
||||
handleAccountConfirmationToken();
|
||||
handleResetToken();
|
||||
{ const params=new URLSearchParams(location.search); const token=params.get("account_action_token"); if(token){ api("/api/auth/account-action/confirm",{method:"POST",body:JSON.stringify({token})}).then(r=>alert(r.message)).catch(e=>alert(e.message)).finally(()=>{params.delete("account_action_token");history.replaceState({},"",`${location.pathname}${params.size?`?${params}`:""}${location.hash}`);}); } }
|
||||
|
||||
const identityDialog = document.querySelector("#identity-dialog");
|
||||
const guestAccount = document.querySelector("#footer-account-guest");
|
||||
@@ -94,6 +95,9 @@ const registrationEnabled = document.body.dataset.registrationEnabled === "true"
|
||||
const resourcesDialog = document.querySelector("#resources-dialog");
|
||||
const resourcesList = document.querySelector("#resources-list");
|
||||
const resourcesError = document.querySelector("#resources-error");
|
||||
const profileDialog = document.querySelector("#profile-dialog");
|
||||
const profileForm = document.querySelector("#profile-form");
|
||||
let currentSession = null;
|
||||
|
||||
function authHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||
function escapeHtml(value) { const node = document.createElement("div"); node.textContent = String(value ?? ""); return node.innerHTML; }
|
||||
@@ -179,7 +183,7 @@ async function loadResources() {
|
||||
linkRow.querySelector("[data-revoke-link]").addEventListener("click", async () => { try { await api("/api/auth/resources/share-links", { method: "DELETE", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, token: linkRow.dataset.linkToken }) }); setDialogMessage("Link revoked.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
});
|
||||
};
|
||||
userForm.addEventListener("submit", async event => { event.preventDefault(); try { await api("/api/auth/resources/sharing", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, emails: userForm.emails.value, permission: userForm.permission.value }) }); userForm.emails.value = ""; setDialogMessage("Access granted.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
userForm.addEventListener("submit", async event => { event.preventDefault(); try { const result = await api("/api/auth/resources/sharing", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, emails: userForm.emails.value, permission: userForm.permission.value }) }); userForm.emails.value = ""; setDialogMessage(result.confirmation_required ? "Invitation sent. Access will appear after the recipient accepts it." : "Access granted.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
linkForm.addEventListener("submit", async event => { event.preventDefault(); try { const expires_at = shareExpiry(linkForm.hours.value, linkForm.forever.checked); const result = await api("/api/auth/resources/share-links", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, permission: linkForm.permission.value, expires_at }) }); const absolute = new URL(result.url, location.origin).href; await copyText(absolute); setDialogMessage("Link created and copied. It remains visible below.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
dialog.showModal();
|
||||
try { await refresh(); } catch (err) { setDialogMessage(err.message, "error"); }
|
||||
@@ -232,6 +236,7 @@ async function loadResources() {
|
||||
|
||||
|
||||
function renderAccount(session) {
|
||||
currentSession = session;
|
||||
guestAccount.hidden = Boolean(session);
|
||||
userAccount.hidden = !session;
|
||||
if (session) userLabel.textContent = `Signed in as ${session.nickname}`;
|
||||
@@ -252,6 +257,43 @@ if (identityDialog) {
|
||||
authDialog.setMode("register");
|
||||
identityDialog.showModal();
|
||||
});
|
||||
|
||||
document.querySelector("#footer-profile")?.addEventListener("click", () => {
|
||||
document.querySelector("#profile-nickname").value = currentSession?.nickname || "";
|
||||
document.querySelector("#profile-email").value = "";
|
||||
document.querySelector("#profile-new-password").value = "";
|
||||
document.querySelector("#profile-password").value = "";
|
||||
const profileMessage = document.querySelector("#profile-message");
|
||||
profileMessage.textContent = "";
|
||||
profileMessage.classList.remove("success", "error");
|
||||
const directoryManaged = Boolean(currentSession?.directory_managed);
|
||||
document.querySelector("#profile-copy").textContent = directoryManaged
|
||||
? "Directory account details are read-only. You can change only the displayed nickname."
|
||||
: "Manage your local RustPad account.";
|
||||
document.querySelectorAll("[data-local-profile-field]").forEach(element => { element.hidden = directoryManaged; });
|
||||
document.querySelectorAll("[data-directory-profile-field]").forEach(element => { element.hidden = !directoryManaged; });
|
||||
document.querySelector("#profile-directory-name").value = currentSession?.directory_display_name || "";
|
||||
document.querySelector("#profile-directory-organization").value = currentSession?.directory_organization || "";
|
||||
const suggestion = currentSession?.suggested_nickname;
|
||||
const suggestionElement = document.querySelector("#profile-nickname-suggestion");
|
||||
suggestionElement.textContent = suggestion ? `Suggested nickname: ${suggestion}` : "No automatic nickname suggestion is available.";
|
||||
document.querySelector("#profile-password").required = !directoryManaged;
|
||||
profileDialog.showModal();
|
||||
});
|
||||
document.querySelector("#close-profile")?.addEventListener("click", () => profileDialog.close());
|
||||
profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); });
|
||||
profileForm?.addEventListener("submit", async event => {
|
||||
event.preventDefault(); const message=document.querySelector("#profile-message"); message.textContent=""; message.classList.remove("success", "error");
|
||||
try { const result=await api("/api/auth/profile",{method:"POST",headers:authHeaders(),body:JSON.stringify({nickname:document.querySelector("#profile-nickname").value.trim(),new_email:currentSession?.directory_managed?null:(document.querySelector("#profile-email").value.trim()||null),new_password:currentSession?.directory_managed?null:(document.querySelector("#profile-new-password").value||null),password:currentSession?.directory_managed?"":document.querySelector("#profile-password").value})}); message.textContent=result.message; message.classList.add("success"); currentSession.nickname=result.nickname; renderAccount(currentSession); } catch(e){ message.textContent=e.message; message.classList.add("error"); }
|
||||
});
|
||||
document.querySelector("#profile-delete")?.addEventListener("click", async () => {
|
||||
const message=document.querySelector("#profile-message"); const password=document.querySelector("#profile-password").value;
|
||||
message.classList.remove("success", "error");
|
||||
if (!password) { message.textContent="Enter the current password first."; message.classList.add("error"); return; }
|
||||
if (!confirm("Send an e-mail link to permanently delete this account?")) return;
|
||||
try { const result=await api("/api/auth/account/delete",{method:"POST",headers:authHeaders(),body:JSON.stringify({password})}); message.textContent=result.message; message.classList.add("success"); } catch(e){ message.textContent=e.message; message.classList.add("error"); }
|
||||
});
|
||||
|
||||
document.querySelector("#footer-resources")?.addEventListener("click", async () => { resourcesDialog.showModal(); await loadResources(); });
|
||||
document.querySelector("#close-resources")?.addEventListener("click", () => resourcesDialog.close());
|
||||
resourcesDialog?.addEventListener("click", (event) => { if (event.target === resourcesDialog) resourcesDialog.close(); });
|
||||
|
||||
@@ -14,7 +14,7 @@ import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url
|
||||
export function startNoteEditor(adapter) {
|
||||
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
|
||||
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||
const roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread");
|
||||
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread");
|
||||
let unreadChat = 0;
|
||||
const compactToggle = document.querySelector("#compact-toggle"), publicTaskUpdates = document.querySelector("#public-task-updates"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker");
|
||||
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
||||
@@ -176,7 +176,7 @@ export function startNoteEditor(adapter) {
|
||||
editor, toast, getAccessToken: () => accessToken, canDelete: () => Boolean(info?.can_delete_files),
|
||||
endpoints: adapter.fileEndpoints,
|
||||
});
|
||||
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: getAuthToken(), guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { if (passwordDialog.open) passwordDialog.close(); applyRemote(m.content, m.owner_map); editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { document.querySelector("#password-error").textContent = m; if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
|
||||
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: getAuthToken(), guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { if (passwordDialog.open) passwordDialog.close(); const readOnly = m.access_level === "read_only"; editor.readOnly = readOnly; accessLevel.textContent = readOnly ? "Access: read only" : "Access: full"; applyRemote(m.content, m.owner_map); if (!readOnly) editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { const friendly = /read-only access/i.test(m) ? "This note is read only. Enter the password or ask the owner to grant write access." : m; document.querySelector("#password-error").textContent = friendly; if (/read-only access/i.test(m)) { toast(friendly); accessLevel.textContent = "Access: read only"; editor.readOnly = true; return; } if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
|
||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
||||
async function initialize() {
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <span id="access-level" class="footer-access">Access: checking…</span> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <span id="access-level" class="footer-access">Access: checking…</span> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user