diff --git a/docs/design/README.md b/docs/design/README.md index 91273f7..087a5d2 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -149,3 +149,8 @@ The Schema board group 7 (Relations) now has 4 states; the Integration page and - Pagination: articles return {items, total, page, size, counts}; media returns an array with X-Total-Count. - Event names follow the backend: role.create, role.edit, role.delete, role.reassign, user.change_role, user.transfer_ownership, user.remove. - Counts above include the Media (+4) and Roles (+6) states only. States added this round to the Editor, Articles, Sites, Authors, Profile and Schema groups are on the boards and in the screens files, and are not yet recounted here. + +## Pass 7: account self-service (email, export, delete) +Board `source/RenewCMS Screens Account.dc.html` (33 states: change email 14, export 9, delete 10), Auth board groups "Verify new email" (7) and "Account deleted" (1), four email templates in `source/emails/` (previews on the Emails board). Specs: `screens/account.md`, `emails.md`, `routes.md`, `components.md`, `states.md`. Frames: `source/AccountCards.dc.html` (new; copy in `source/account-copy.js`), `AppFrame.dc.html` (Profile hooks, modal list, warning, link and disabled confirm). Every Profile frame on the other boards shows the new cards in their default state. English only. +Decided: authored content is anonymized to "Deleted user"; email-change link 2 hours; export link 24 hours, one export per 15 minutes; direct download is the primary export flow, the emailed link is the documented alternative; the export-ready link needs a logged-in session; "last administrator" counts Owner; a change request for an address already in use answers like success; the account-deleted email goes to the address the account had. +Not done: Foundations and Components boards and the Prototype are not updated. The 15-minute rate-limit state is clock-sensitive (frozen clock 09:41) and is skipped by `task visual:check`. diff --git a/docs/design/components.md b/docs/design/components.md index 13300cc..b97fc1f 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -157,3 +157,14 @@ Accessibility of the tri-state cell: | Announcement | Selected item is aria-checked="true". A polite live region announces changes: "Events, Update, set to Own." Disabled items are aria-disabled and described by the reason ("You can only grant what your role has."). | | Not by color alone | Selected segment: sand fill plus weight 600 plus a 2 px underline in --ui-primary. Locked: lock icon. Changed cell: square marker. Mixed: text badge. | | Targets | 26 px segments on desktop, 44 px on mobile. Focus: 2 px --ui-primary outline, offset 2 px. | + +## Account (change email, export, delete) +| Component | Status | Nuxt UI mapping | Where | +|---|---|---|---| +| EmailChangeForm | New | UFormField + UInput (email, password) + UButton, inside the Account UCard | Profile | +| PendingEmailNotice | New | UAlert (info) + two UButton (outline) | Profile | +| DataExportCard | New | UCard + UButton; notices as UAlert | Profile | +| DangerZoneCard | New | UCard with border var(--ui-error), UButton color error | Profile | +| DeleteAccountDialog | New | UModal (existing destructive ConfirmDialog shell) + UInput x2 + list + UAlert info + ULink | Profile | +| BlockedDialog | New | UModal with dismissible false, no close button; UButton primary for the next step | Profile | +Specs: field heights 40 (44 mobile), buttons 36 (44 mobile), card padding 16, section gap 16. Colors from tokens only. Destructive confirm is disabled at 40% opacity until valid. Implemented in AccountCards.dc.html and the AppFrame modal. diff --git a/docs/design/emails.md b/docs/design/emails.md index 8428446..a2c0d74 100644 --- a/docs/design/emails.md +++ b/docs/design/emails.md @@ -14,3 +14,15 @@ Previews: `source/RenewCMS Emails.dc.html`. Three Go html/template files: emails/verification.html, emails/password-reset.html and emails/invitation.html . They use a table layout with inline CSS and a 600 px maximum width. The button is bulletproof: a colored table cell plus a padded link, so it survives Outlook. Each has a plain-URL fallback, a hidden preheader, and prefers-color-scheme overrides for clients that support them. The mark is live text in a black cell rather than an image, so it shows even when images are blocked. Previews below use example values. Suggested subjects are "Verify your RenewCMS account", "Reset your RenewCMS password" and " invited you to RenewCMS", where .InvitedBy is the inviter's display name or username. html/template escapes the .Email value for the query string in the reset link (the @ becomes %40), and the confirm route reads it back decoded. + +## Account emails (change email, deletion, export) +Go `html/template` files built from `source/emails/verification.html` (same table layout, tokens, dark overrides, bulletproof button, URL fallback). English only. + +| File | To | Subject | Variables | +|---|---|---|---| +| `source/emails/verify-new-email.html` | the new address | Confirm your new RenewCMS email address | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/email/confirm?c={{.VerificationCode}}` (expires in 2 hours, works once) | +| `source/emails/email-change-notice.html` | the old address, at request time | Your RenewCMS email address change was requested | `.Host`, `.Username`, `.NewEmail`, linking to `{{.Host}}/profile` | +| `source/emails/account-deleted.html` | the address the account had, sent once | Your RenewCMS account has been deleted | `.Host`, `.Username`, `.DeletedAt`; no button, generic text, no personal data | +| `source/emails/export-ready.html` | the account address, async flow only | Your RenewCMS data export is ready | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/export/{{.VerificationCode}}` (expires in 24 hours, requires a logged-in session) | + +`export-ready.html` is only needed if the async export alternative is built. Previews are rows on `source/RenewCMS Emails.dc.html`. diff --git a/docs/design/routes.md b/docs/design/routes.md index b080ce7..763d321 100644 --- a/docs/design/routes.md +++ b/docs/design/routes.md @@ -74,3 +74,10 @@ Needs backend: | Enforcement | Server checks every call against role permissions and the no-escalation rules. API keys resolve the creator's current role on each request. | | Version field | role.version for the conflict state (409 role_conflict). | | Activity events | role.create, role.edit (with a diff of added and removed permissions), role.delete, role.reassign, user.change_role, user.transfer_ownership. Present tense, as the backend emits them. | + +## Account: change email, export, delete +| Route | Screen | Auth | API (assumed) | +|---|---|---|---| +| /account/email/confirm?c=CODE | Verify new email (AuthCard). States in screens/account.md | Session required; without one, inline log in (as /register/validate) | POST /v1/auth/email/confirm | +| /profile | Gains change email, Your data, Danger zone | Any verified user | POST /v1/auth/email/change, POST /v1/auth/email/resend, DELETE /v1/auth/email/change, GET /v1/user/me/export, DELETE /v1/user/me | +| /login?reason=account-deleted | One-time message after deletion | Public | none | diff --git a/docs/design/screens/account.md b/docs/design/screens/account.md new file mode 100644 index 0000000..ee2773f --- /dev/null +++ b/docs/design/screens/account.md @@ -0,0 +1,72 @@ + +# Account: change email, export my data, delete account + +Route: `/profile` (extends the Account card, adds "Your data" and "Danger zone" cards). Verification landing: `/account/email/confirm?c=CODE`. API calls are assumed, not specified. + +Placement: Profile already holds the read-only Email in the Account card and the Password card, so the email change extends that card; the two other features are cards at the bottom. No new navigation. Order: Display name, Account, Your role, Author profile, Password, Your data, Danger zone. + +Boards: `RenewCMS Screens Account.dc.html` (Profile states), `RenewCMS Screens Auth.dc.html` (groups "Verify new email" and "Account deleted"). Frames: `AppFrame`, `AccountCards`, `AuthCard`. Copy source: `account-copy.js`. English only. + +## Change email +| State | Copy | +|---|---| +| Default | "Email" field, link "Change email" | +| Form | "New email address" · "Current password" · "We'll send a verification link to the new address. Your email stays the same until you open it." · "Cancel" · "Send verification link" | +| Invalid format | "Enter a full email address, like name@example.com." | +| Same as current | "That's already your email address." | +| Password missing | "Enter your current password." | +| Wrong password (403) | "That password isn't correct. Try again." | +| Sending | "Sending…" | +| Pending | "Waiting for verification" · "We sent a link to marta.alves@atelier-renew.example. Until you open it, you keep logging in with marta@renew.example." · "Resend link" · "Cancel change"; toast "Verification link sent" / "Check marta.alves@atelier-renew.example. The link works once, within 2 hours." | +| Resending | "Sending…" | +| Resent | toast "Link sent again" / "The previous link no longer works." | +| Resend rate limited (429) | "Resend in 4:30" (disabled) · "Too many requests. Try again in a few minutes." | +| Cancelled | toast "Email change cancelled" / "You keep logging in with marta@renew.example." | +| Rate limited (429) | "Too many requests. Try again in a few minutes.", Send disabled | +| Server or network | "Something went wrong on our side. Nothing was changed. Try again in a moment." | + +Rules: the old email stays active for login until the link is opened. The old address gets the notice email at request time. A request for an address already in use answers like success, so the UI and the response never reveal that an account exists. + +## Verification landing (AuthCard) +| State | Copy | +|---|---| +| Confirming | "Confirming your new email…" | +| Success | "Your email address has changed" · "Your account now uses marta.alves@atelier-renew.example. Use it the next time you log in." · "Go to profile"; toast "Email address changed" / "Your account now uses marta.alves@atelier-renew.example." | +| Invalid or expired (403) | "This link doesn't work" · "It may have expired (links last 2 hours), been cancelled or already been used." · "Request a new link from your profile." · "Go to profile" | +| Address unavailable (409) | "We couldn't change your email address" · "This address can't be used for your account. Go back to your profile and try a different one." · "Go to profile" (same copy whether the address is taken or rejected) | +| No session (401) | "Log in to confirm your new email" · "You opened this link somewhere you're not logged in. Log in here and we'll confirm the change right after." · "Log in and confirm" | +| Missing code | "This link is incomplete" · "The confirmation code is missing. Open the link from your email again, or copy the full address into your browser." · "Go to log in" | +| Server error | "Couldn't confirm the change" · "Something went wrong on our side. Nothing was changed. Try again in a moment." · "Try again" | + +## Your data +Contents listed in the card: Profile (Username, display name, email, role and author profile.); Authored content (Your articles and entries.); Uploaded media (References to the files you uploaded.); Account activity (Your log-ins and the actions recorded under your name.). Card copy: "Your data" · "Download a copy of the personal data RenewCMS holds about you, as JSON files in a ZIP.". +| State | Copy | +|---|---| +| Direct (recommended): default | "Download my data" · "Ready in a few seconds." | +| Preparing | "Preparing download…" | +| Started | toast "Download started" / "renewcms-export-marta-2026-10-29.zip is in your downloads." | +| Rate limited (429, one per 15 minutes, clock-sensitive) | "You can export once every 15 minutes" · "Your last export was at 09:41. Try again in 12 minutes." · "Available in 12:00" | +| Error | "Couldn't prepare your data" · "Nothing was changed. Try again in a moment." | +| Alternative, emailed link: default | "Request my data" · "Larger exports are prepared in the background. We'll email a download link to marta@renew.example when it's ready." | +| Requested | "We're preparing your export" · "This can take a few minutes. We'll email a link to marta@renew.example when it's ready." · "Export requested"; toast "Export requested" / "Check marta@renew.example in a few minutes." | +| Already in progress | "An export is already being prepared" · "We'll email a link to marta@renew.example when it's ready." | + +Recommendation: direct download; async is the documented alternative and uses the export-ready email. + +## Delete account +Danger zone: "Danger zone" · "Delete your account and your personal data. This can't be undone." · "Delete my account". +| State | Copy | +|---|---| +| Dialog | "Delete your account?" · "This permanently deletes your account. It can't be undone." · list: "Your username, display name and email address", "Your author profile and API keys", "Your sessions: you'll be logged out everywhere" · "Your articles and media stay" / "They stay on the site, credited to "Deleted user". Your name is removed from them." · "Export my data first" · "Current password" · "Type DELETE to confirm" · "Keep my account" · "Delete my account" | +| Empty, typing | Confirm disabled until password is filled and the word equals "DELETE" (exact, case-sensitive) | +| Wrong password (403) | "That password isn't correct. Try again." | +| Deleting | "Deleting…"; fields and Cancel disabled; then sign out and redirect to /login?reason=account-deleted | +| Rate limited (429) | "Too many requests. Try again in a few minutes." | +| Server error | "Couldn't delete your account. Nothing was deleted. Try again in a moment." | +| Blocked: last Owner or administrator | "You can't delete your account yet" · Owner: "You are the last Owner. RenewCMS needs at least one. Transfer ownership or promote another user first, then come back to delete your account." · Administrator: "You are the last administrator. RenewCMS needs at least one. Promote another user first, then come back to delete your account." · "Back to profile" · "Go to Users" | +| Blocked: cannot be deleted | "This account can't be deleted" · "RenewCMS can't delete this account right now. Ask an administrator to help, then try again." · "Back to profile" | +| Login after deletion | "Your account has been deleted" · "Your articles and media were kept without your name." | + +Blocked dialogs have no close button, do not close on Escape or backdrop click, and always give a next step. + +Authored content: anonymized to "Deleted user", stated in the dialog. Export-ready email: the link needs a logged-in session, stated in the email and the card hint. Account-deleted email: generic, no personal data, sent to the address the account had at deletion. diff --git a/docs/design/source/AccountCards.dc.html b/docs/design/source/AccountCards.dc.html new file mode 100644 index 0000000..bff5dbc --- /dev/null +++ b/docs/design/source/AccountCards.dc.html @@ -0,0 +1,131 @@ + + + + + + + + + + + + +
+ +
+
{{ c.emailLabel }}{{ c.changeEmail }}
+
{{ old }}
+
+
+ + +
+
{{ form.notice }}
+ +
+ +
{{ f.shown }}
+{{ f.err }} +
+
+{{ c.formHelp }} +
+{{ c.cancel }} +{{ form.sendLabel }} +
+
+
+ +
+
{{ c.pendTitle }}{{ pend.text }}
+
+{{ pend.resendLabel }} +{{ c.cancelChange }} +
+{{ pend.note }} +
+
+
+ +
+
+
{{ c.dataTitle }}{{ c.dataDesc }}
+
+ +
{{ it.title }}{{ it.text }}
+
+
+
{{ data.notice.title }}{{ data.notice.text }}
+
+{{ data.label }} +{{ data.hint }} +
+
+
+
+
{{ c.dzTitle }}{{ c.dzDesc }}
+{{ c.dzBtn }} +
+
+
+
+
+ +
+ + + diff --git a/docs/design/source/AppFrame.dc.html b/docs/design/source/AppFrame.dc.html index 7adfd5f..3bd4389 100644 --- a/docs/design/source/AppFrame.dc.html +++ b/docs/design/source/AppFrame.dc.html @@ -263,7 +263,7 @@ -
+
ProfileYour account on this RenewCMS workspace.
@@ -281,7 +281,8 @@
AccountSet when your account was created. You log in with your username, even after adding a display name.
-
Username
marta
Email
marta@renew.example
+
Username
marta
+
Author profileYour public byline on articles. It's separate from your account.
Couldn't create your author profileTry again in a moment.
@@ -294,6 +295,7 @@
Password
••••••••••••
{{ rsBtn.t }}
{{ rsNote.title }}{{ rsNote.text }}
+
@@ -316,9 +318,12 @@
{{ s.modal.title }}{{ s.modal.desc }}
{{ s.modal.notice }}
+
{{ li.t }}
+
{{ mWarn.title }}{{ mWarn.text }}
{{ f.label }}{{ f.count }}
{{ f.shown }}
{{ f.err }}{{ f.help }}
+{{ s.modal.link }}
Open in new tab
Remove link
@@ -362,7 +367,7 @@
{{ s.modal.cancel }} -{{ s.modal.confirm }} +{{ s.modal.confirm }}
@@ -580,7 +585,7 @@ class Component extends DCLogic { saveOp: (dirty && !blocked) ? '1' : '0.4', imgCount: e.imgs != null ? String(e.imgs) : (e.image ? '4' : '3'), apiNote: online ? 'Live. Your website can read this article.' : 'Returns 404 until the article is online.', copyIcon: s.copied ? 'check' : 'copy', copyText: s.copied ? 'Copied' : 'Copy', copyFg: s.copied ? 'var(--ui-success)' : 'var(--ui-text-highlighted)', errPad: m ? '40px 20px' : '64px 48px', - closeSz: m ? '44px' : '28px', modalAlign: m ? 'flex-end' : 'flex-start', modalPad: m ? '0' : (md.crop ? '56px 16px 16px' : '140px 16px 16px'), modalW: m ? '100%' : (md.crop ? '640px' : md.wide ? '560px' : '460px'), mBtnDir: m ? 'column-reverse' : 'row', + closeSz: m ? '44px' : '28px', modalAlign: m ? 'flex-end' : 'flex-start', modalPad: m ? '0' : (md.crop ? '56px 16px 16px' : '140px 16px 16px'), modalW: m ? '100%' : (md.crop ? '640px' : md.wide ? '560px' : '460px'), mBtnDir: m ? 'column-reverse' : 'row', mConfirmOp: md.disabled ? '0.4' : '1', mList: (md.list || []).map(t => ({ t })), mWarn: md.warn ? { ...md.warn, bg: 'var(--rc-' + md.warn.tone + '-soft)', fg: 'var(--ui-' + md.warn.tone + ')' } : null, pfShift: pf.shift ? '-' + (m ? pf.shift[1] : pf.shift[0]) + 'px' : '0px', pfAc: pf.ac || {}, pfC: s.copy || null, pfPanel: !!(pf.ac && (pf.ac.form || pf.ac.pending)), mConfirmBg: md.tone === 'danger' ? 'var(--ui-error)' : 'var(--ui-primary)', mCancelFg: md.loading ? 'var(--ui-text-dimmed)' : 'var(--ui-text-highlighted)', hasToasts: toasts.length > 0, tL: m ? '16px' : 'auto', tR: m ? '16px' : '24px', tB: m ? '16px' : '24px', tW: m ? 'auto' : '356px' } diff --git a/docs/design/source/RenewCMS Emails.dc.html b/docs/design/source/RenewCMS Emails.dc.html index 688ea3b..1b6c43d 100644 --- a/docs/design/source/RenewCMS Emails.dc.html +++ b/docs/design/source/RenewCMS Emails.dc.html @@ -38,9 +38,10 @@ class Component extends DCLogic { fetch('emails/verification.html').then(x => x.text()).then(v => this.setState({ v })) fetch('emails/password-reset.html').then(x => x.text()).then(r => this.setState({ r })) fetch('emails/invitation.html').then(x => x.text()).then(i => this.setState({ i })) + ;['verify-new-email','email-change-notice','account-deleted','export-ready'].forEach(n => fetch('emails/' + n + '.html').then(x => x.text()).then(t => this.setState({ ['x_' + n]: t }))) } fill(t) { - return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example') + return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Username}}').join('marta').split('{{.NewEmail}}').join('marta.alves@atelier-renew.example').split('{{.DeletedAt}}').join('29 Oct 2026').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example') } frames(t) { if (!t) return [] @@ -52,7 +53,11 @@ class Component extends DCLogic { return { rows: [ { title: 'Verification email', vars: '{{.Host}} · {{.VerificationCode}} → /register/validate?c=', frames: this.frames(this.state.v) }, { title: 'Password reset email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} → /register/reset/validate?c=&email=', frames: this.frames(this.state.r) }, - { title: 'Invitation email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} · {{.InvitedBy}} → /invite/accept?c=', frames: this.frames(this.state.i) } + { title: 'Invitation email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} · {{.InvitedBy}} → /invite/accept?c=', frames: this.frames(this.state.i) }, + { title: 'Verify new email', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/email/confirm?c=', frames: this.frames(this.state['x_verify-new-email']) }, + { title: 'Email change notice (to the old address)', vars: '{{.Host}} · {{.Username}} · {{.NewEmail}}', frames: this.frames(this.state['x_email-change-notice']) }, + { title: 'Account deleted', vars: '{{.Host}} · {{.Username}} · {{.DeletedAt}}', frames: this.frames(this.state['x_account-deleted']) }, + { title: 'Export ready (alternative flow)', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/export/', frames: this.frames(this.state['x_export-ready']) } ] } } } diff --git a/docs/design/source/RenewCMS Screens Account.dc.html b/docs/design/source/RenewCMS Screens Account.dc.html new file mode 100644 index 0000000..f1295ee --- /dev/null +++ b/docs/design/source/RenewCMS Screens Account.dc.html @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + +
+ +
+
+

Screens: account (email, data, delete)

+

Extends Profile with change email, export my data and delete account. 1440 px at 50%, 390 px at 75%. The theme switch shows every state in dark mode.

+
+
+ + + +
+
+ + +
+
+

{{ g.title }}

+
{{ g.route }}{{ g.api }}
+

{{ g.note }}

+
+
+ +
+
{{ st.name }}{{ st.trigger }}
+
+
+
+
+
+
+
+
{{ g.tableTitle }}
StatusWhereCopy
{{ r.a }}{{ r.b }}{{ r.c }}
+
+
+ +
+
+ + + diff --git a/docs/design/source/RenewCMS Screens Auth.dc.html b/docs/design/source/RenewCMS Screens Auth.dc.html index 03e8b20..4eba538 100644 --- a/docs/design/source/RenewCMS Screens Auth.dc.html +++ b/docs/design/source/RenewCMS Screens Auth.dc.html @@ -156,6 +156,19 @@ class Component extends DCLogic { { name: 'No session, retrying', trigger: 'Logged in inline. Now verifying.', spinner: 'Verifying your account…' }, { name: 'Missing code', trigger: 'The link has no ?c= parameter. No request is made.', result: { tone: 'error', icon: 'link-2-off', title: 'This link is incomplete', text: 'The verification code is missing. Open the link from your email again, or copy the full address into your browser.', actions: [{ t: 'Go to log in', secondary: true }] } } ] }, + { label: 'Verify new email', title: 'Verify new email', route: '/account/email/confirm?c=CODE', api: 'assumed: POST /v1/auth/email/confirm. Needs a session; without one, inline log in', note: 'Opened from the verification email sent to the new address. Submits on load. The unavailable state says the same thing whether the address is in use or rejected, so it never reveals whether an account exists.', states: [ + { name: 'Confirming', trigger: 'On load, with a code in the URL.', spinner: 'Confirming your new email…' }, + { name: 'Success', trigger: '200 → /profile after 800 ms, with a toast.', result: { tone: 'success', icon: 'mail-check', title: 'Your email address has changed', text: 'Your account now uses marta.alves@atelier-renew.example. Use it the next time you log in.', actions: [{ t: 'Go to profile' }] }, toast: toast('success', 'circle-check', 'Email address changed', 'Your account now uses marta.alves@atelier-renew.example.') }, + { name: 'Invalid or expired (403)', trigger: 'The code is wrong, used, cancelled or older than 2 hours.', result: { tone: 'error', icon: 'link-2-off', title: 'This link doesn\'t work', text: 'It may have expired (links last 2 hours), been cancelled or already been used.', note: 'Request a new link from your profile.', actions: [{ t: 'Go to profile' }] } }, + { name: 'Address unavailable (409)', trigger: 'The address is taken or rejected. One message for both.', result: { tone: 'error', icon: 'mail-x', title: 'We couldn\'t change your email address', text: 'This address can\'t be used for your account. Go back to your profile and try a different one.', actions: [{ t: 'Go to profile' }] } }, + { name: 'No session (401)', trigger: 'Opened in another browser. Log in inline, then the change is confirmed with the same code.', title: 'Log in to confirm your new email', desc: 'You opened this link somewhere you\'re not logged in. Log in here and we\'ll confirm the change right after.', fields: [U(), P()], submit: { t: 'Log in and confirm' } }, + { name: 'No session, retrying', trigger: 'Logged in inline. Now confirming.', spinner: 'Confirming your new email…' }, + { name: 'Missing code', trigger: 'The link has no ?c= parameter. No request is made.', result: { tone: 'error', icon: 'link-2-off', title: 'This link is incomplete', text: 'The confirmation code is missing. Open the link from your email again, or copy the full address into your browser.', actions: [{ t: 'Go to log in', secondary: true }] } }, + { name: 'Server error', trigger: '500 or network failure. Nothing was changed.', result: { tone: 'error', icon: 'circle-x', title: 'Couldn\'t confirm the change', text: 'Something went wrong on our side. Nothing was changed. Try again in a moment.', actions: [{ t: 'Try again' }] } } + ] }, + { label: 'Account deleted', title: 'Log in: after account deletion', route: '/login?reason=account-deleted', api: 'none', note: 'After a successful deletion the user is signed out and sent here. The message shows once and is gone on reload.', states: [ + { ...login, name: 'Account deleted', trigger: 'Redirected from the delete dialog.', notice: { tone: 'success', icon: 'circle-check', title: 'Your account has been deleted', text: 'Your articles and media were kept without your name.' }, fields: loginF({}, {}) } + ] }, { label: 'Accept invite', title: 'Accept invite', route: '/invite/accept?c=CODE', api: 'GET /v1/auth/invite?c= (pending) · POST /v1/auth/invite/accept (pending)', note: 'Opened from the invitation email. The link carries only the code, so the invited email is looked up on load and shown read-only. Username and password follow the Register rules, including PasswordStrengthHint. On success the person is logged in and sent to /home with a toast; they are already verified because the invite went to their email.', states: [ { name: 'Checking the link', trigger: 'On load, while the invite is looked up.', spinner: 'Checking your invite…' }, { ...inv, name: 'Default', trigger: 'The code is valid and unused.', fields: invF({}, {}) }, diff --git a/docs/design/source/account-copy.js b/docs/design/source/account-copy.js new file mode 100644 index 0000000..cdef67c --- /dev/null +++ b/docs/design/source/account-copy.js @@ -0,0 +1,101 @@ +window.ACCOUNT_COPY = { + en: { + emailLabel: 'Email', + changeEmail: 'Change email', + newEmail: 'New email address', + newEmailPh: 'name@example.com', + password: 'Current password', + formHelp: "We'll send a verification link to the new address. Your email stays the same until you open it.", + cancel: 'Cancel', + send: 'Send verification link', + sending: 'Sending…', + errFormat: 'Enter a full email address, like name@example.com.', + errSame: "That's already your email address.", + errPwEmpty: 'Enter your current password.', + errPwWrong: "That password isn't correct. Try again.", + rate: 'Too many requests. Try again in a few minutes.', + server: 'Something went wrong on our side. Nothing was changed. Try again in a moment.', + pendTitle: 'Waiting for verification', + pendText: 'We sent a link to {new}. Until you open it, you keep logging in with {old}.', + resend: 'Resend link', + resendRate: 'Resend in 4:30', + cancelChange: 'Cancel change', + sentT: 'Verification link sent', + sentB: 'Check {new}. The link works once, within 2 hours.', + resentT: 'Link sent again', + resentB: 'The previous link no longer works.', + cancelledT: 'Email change cancelled', + cancelledB: 'You keep logging in with {old}.', + dataTitle: 'Your data', + dataDesc: 'Download a copy of the personal data RenewCMS holds about you, as JSON files in a ZIP.', + dataItems: [ + ['user', 'Profile', 'Username, display name, email, role and author profile.'], + ['file-text', 'Authored content', 'Your articles and entries.'], + ['image', 'Uploaded media', 'References to the files you uploaded.'], + ['history', 'Account activity', 'Your log-ins and the actions recorded under your name.'] + ], + download: 'Download my data', + preparing: 'Preparing download…', + dlHint: 'Ready in a few seconds.', + dlT: 'Download started', + dlB: 'renewcms-export-marta-2026-10-29.zip is in your downloads.', + exRateT: 'You can export once every 15 minutes', + exRateB: 'Your last export was at 09:41. Try again in 12 minutes.', + exRateBtn: 'Available in 12:00', + exErrT: "Couldn't prepare your data", + exErrB: 'Nothing was changed. Try again in a moment.', + request: 'Request my data', + asyncHint: "Larger exports are prepared in the background. We'll email a download link to {old} when it's ready. You'll need to be logged in to download it.", + queuedT: "We're preparing your export", + queuedB: "This can take a few minutes. We'll email a link to {old} when it's ready.", + queuedBtn: 'Export requested', + inprogT: 'An export is already being prepared', + inprogB: "We'll email a link to {old} when it's ready.", + queuedToastT: 'Export requested', + queuedToastB: 'Check {old} in a few minutes.', + dzTitle: 'Danger zone', + dzDesc: "Delete your account and your personal data. This can't be undone.", + dzBtn: 'Delete my account', + delTitle: 'Delete your account?', + delDesc: "This permanently deletes your account. It can't be undone.", + delList: ['Your username, display name and email address', 'Your author profile and API keys', "Your sessions: you'll be logged out everywhere"], + delKeepT: 'Your articles and media stay, without your name', + delKeepB: 'They stay on the site and are credited to "Deleted user". Your name is removed from them and this cannot be reversed.', + delExport: 'Export my data first', + delPw: 'Current password', + delWordLabel: 'Type DELETE to confirm', + delWord: 'DELETE', + delKeep: 'Keep my account', + delConfirm: 'Delete my account', + deleting: 'Deleting…', + delServer: "Couldn't delete your account. Nothing was deleted. Try again in a moment.", + blockT: "You can't delete your account yet", + blockB: "You are the last Owner. RenewCMS needs at least one. Transfer ownership or promote another user first, then come back to delete your account.", + blockBack: 'Back to profile', + blockGo: 'Go to Users', + protT: "This account can't be deleted", + protB: "RenewCMS can't delete this account right now. Ask an administrator to help, then try again.", + vVerifying: 'Confirming your new email…', + vOkT: 'Your email address has changed', + vOkB: 'Your account now uses {new}. Use it the next time you log in.', + vOkBtn: 'Go to profile', + vOkToastT: 'Email address changed', + vOkToastB: 'Your account now uses {new}.', + vBadT: "This link doesn't work", + vBadB: 'It may have expired (links last 2 hours), been cancelled or already been used.', + vBadN: 'Request a new link from your profile.', + vNoT: "We couldn't change your email address", + vNoB: "This address can't be used for your account. Go back to your profile and try a different one.", + vLoginT: 'Log in to confirm your new email', + vLoginB: "You opened this link somewhere you're not logged in. Log in here and we'll confirm the change right after.", + vLoginBtn: 'Log in and confirm', + vMissT: 'This link is incomplete', + vMissB: 'The confirmation code is missing. Open the link from your email again, or copy the full address into your browser.', + vMissBtn: 'Go to log in', + vErrT: "Couldn't confirm the change", + vErrB: 'Something went wrong on our side. Nothing was changed. Try again in a moment.', + vErrBtn: 'Try again', + goneT: 'Your account has been deleted', + goneB: 'Your account has been deleted. Your articles and media were kept without your name.' + } +} diff --git a/docs/design/source/emails/account-deleted.html b/docs/design/source/emails/account-deleted.html new file mode 100644 index 0000000..cc8821c --- /dev/null +++ b/docs/design/source/emails/account-deleted.html @@ -0,0 +1,62 @@ + + + + + + + + Your account has been deleted + + + +
Your RenewCMS account was deleted and your personal data removed.
+ + + + + + + diff --git a/docs/design/source/emails/email-change-notice.html b/docs/design/source/emails/email-change-notice.html new file mode 100644 index 0000000..b79cf9e --- /dev/null +++ b/docs/design/source/emails/email-change-notice.html @@ -0,0 +1,69 @@ + + + + + + + + Your email address change was requested + + + +
A change of email address was requested for your RenewCMS account. Nothing has changed yet.
+ + + + + + + diff --git a/docs/design/source/emails/export-ready.html b/docs/design/source/emails/export-ready.html new file mode 100644 index 0000000..de2387d --- /dev/null +++ b/docs/design/source/emails/export-ready.html @@ -0,0 +1,72 @@ + + + + + + + + Your data export is ready + + + +
Your RenewCMS data export is ready to download. The link expires in 24 hours.
+ + + + + + + diff --git a/docs/design/source/emails/verify-new-email.html b/docs/design/source/emails/verify-new-email.html new file mode 100644 index 0000000..64924df --- /dev/null +++ b/docs/design/source/emails/verify-new-email.html @@ -0,0 +1,72 @@ + + + + + + + + Confirm your new email address + + + +
Confirm this address to finish changing your RenewCMS email. The link expires in 2 hours.
+ + + + + + + diff --git a/docs/design/states.md b/docs/design/states.md index 16f7412..60516f0 100644 --- a/docs/design/states.md +++ b/docs/design/states.md @@ -177,3 +177,13 @@ A role with Read set to Own sees online articles plus its own drafts. For custom ## Wording status Strings that match messages.ts are verbatim. "Only the owner can manage sites…" and other board-only wording are proposals. Moving the app to permission-based wording is a separate code change. + +## Account: change email, export, delete +States and copy in screens/account.md. Board: RenewCMS Screens Account.dc.html (33 states) and the Auth board groups "Verify new email" (7) and "Account deleted" (1). Direct-download rate limit state is clock-sensitive (frozen clock 09:41). +| Status | Copy | +|---|---| +| 403 wrong_password | "That password isn't correct. Try again." (inline under password) | +| 429 | existing 429 copy, inline in the form or the dialog | +| 500, network | existing 500 copy, or "Couldn't delete your account. Nothing was deleted. Try again in a moment." | +| 409 last_admin | "You can't delete your account yet" (non-dismissable dialog) | +| 409 email_unavailable | Landing only: "We couldn't change your email address" (same for in use and rejected) |