Improve auth UX and password strength across dashboards.

Remove business self-registration, localize SMS/auth errors, add password strength + confirm on reset/signup, and brand customer role as website user. Also include websites SSL sync UI updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 23:29:55 +03:30
co-authored by Cursor
parent 656fc57494
commit aeaad0f645
22 changed files with 847 additions and 285 deletions
@@ -54,3 +54,10 @@
.deployFail {
color: #b91c1c;
}
.headerActions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
+155 -29
View File
@@ -9,6 +9,7 @@ import {
Rocket,
RotateCcw,
Search,
ShieldPlus,
Unlock,
} from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
@@ -20,6 +21,8 @@ import type { ListDomainsParams } from '../services/domainService'
import { ApiError, isAbortError } from '../lib/api'
import {
deployDomain,
issueDomainSsl,
issueWebsiteSsl,
listDomains,
removeDomain,
setDomainActive,
@@ -105,6 +108,8 @@ export function WebsitesPage() {
const [togglingSslId, setTogglingSslId] = useState<number | null>(null)
const [deployingId, setDeployingId] = useState<DomainListItem['id'] | null>(null)
const [syncingSsl, setSyncingSsl] = useState(false)
const [issuingWebsiteSsl, setIssuingWebsiteSsl] = useState(false)
const [issuingSslId, setIssuingSslId] = useState<DomainListItem['id'] | null>(null)
useEffect(() => {
const controller = new AbortController()
@@ -264,17 +269,17 @@ export function WebsitesPage() {
}
async function handleSyncSsl() {
if (syncingSsl) return
if (syncingSsl || issuingWebsiteSsl) return
flushSync(() => {
setSyncingSsl(true)
})
setError('')
showToast('Starting SSL sync…', 'info')
showToast('Starting dashboard SSL sync…', 'info')
try {
const result = await syncSsl()
showToast(result.message || 'SSL sync started.', 'success')
showToast(result.message || 'Dashboard SSL sync started.', 'success')
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Unable to start SSL sync.'
setError(message)
@@ -284,6 +289,77 @@ export function WebsitesPage() {
}
}
async function handleIssueWebsiteSsl() {
if (issuingWebsiteSsl || syncingSsl) return
flushSync(() => {
setIssuingWebsiteSsl(true)
})
setError('')
showToast('Issuing SSL for storefront domains…', 'info')
try {
const result = await issueWebsiteSsl()
if (result.failed.length > 0) {
const detail = result.failed
.map((f) => `${f.host}: ${f.error}`)
.join(' · ')
setError(detail)
showToast(result.message, result.issued.length > 0 ? 'info' : 'error')
} else {
showToast(result.message || 'Website SSL updated.', 'success')
}
if (result.issued.length > 0) {
setData((prev) => {
if (!prev) return prev
const issued = new Set(result.issued)
return {
...prev,
items: prev.items.map((item) =>
issued.has(item.host) ? { ...item, sslEnabled: true } : item,
),
}
})
}
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Unable to issue website SSL.'
setError(message)
showToast(message, 'error')
} finally {
setIssuingWebsiteSsl(false)
}
}
async function handleIssueDomainSsl(domain: DomainListItem) {
if (!domain.deploySlug || issuingSslId != null) return
flushSync(() => {
setIssuingSslId(domain.id)
})
setError('')
showToast(`Issuing SSL for "${domain.host}"…`, 'info')
try {
const result = await issueDomainSsl(domain.id)
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domain.id ? { ...item, sslEnabled: result.sslEnabled } : item,
),
}
})
showToast(result.message || `SSL issued for "${domain.host}".`, 'success')
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Unable to issue SSL.'
setError(message)
showToast(message, 'error')
} finally {
setIssuingSslId(null)
}
}
async function handleDeploy(domain: DomainListItem) {
if (!domain.deploySlug) return
if (deployingId != null) return
@@ -366,22 +442,42 @@ export function WebsitesPage() {
Monitor and manage all domains across the platform.
</p>
</div>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnPrimary} ${
syncingSsl ? styles.deployBusy : ''
}`}
onClick={() => void handleSyncSsl()}
disabled={syncingSsl}
aria-busy={syncingSsl}
>
{syncingSsl ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<RefreshCw size={16} aria-hidden />
)}
{syncingSsl ? 'Syncing SSL…' : 'Sync SSL now'}
</button>
<div className={styles.headerActions}>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnGhost} ${
syncingSsl ? styles.deployBusy : ''
}`}
onClick={() => void handleSyncSsl()}
disabled={syncingSsl || issuingWebsiteSsl}
aria-busy={syncingSsl}
title="Expand Let's Encrypt cert for manage / business.* / customer.* dashboards"
>
{syncingSsl ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<RefreshCw size={16} aria-hidden />
)}
{syncingSsl ? 'Syncing dashboard SSL…' : 'Sync dashboard SSL'}
</button>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnPrimary} ${
issuingWebsiteSsl ? styles.deployBusy : ''
}`}
onClick={() => void handleIssueWebsiteSsl()}
disabled={issuingWebsiteSsl || syncingSsl}
aria-busy={issuingWebsiteSsl}
title="Issue Let's Encrypt certs for storefront domains missing SSL"
>
{issuingWebsiteSsl ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<ShieldPlus size={16} aria-hidden />
)}
{issuingWebsiteSsl ? 'Issuing website SSL…' : 'Issue website SSL'}
</button>
</div>
</div>
<div className={tableStyles.filtersPanel}>
@@ -569,16 +665,46 @@ export function WebsitesPage() {
)}
</button>
) : null}
<button
type="button"
className={tableStyles.controlBtn}
onClick={() => void handleToggleSsl(domain, !domain.sslEnabled)}
disabled={togglingSslId === domain.id}
title={domain.sslEnabled ? 'Disable SSL' : 'Enable SSL'}
aria-label={domain.sslEnabled ? 'Disable SSL' : 'Enable SSL'}
>
{domain.sslEnabled ? <Lock size={16} /> : <Unlock size={16} />}
</button>
{domain.deploySlug && !domain.sslEnabled ? (
<button
type="button"
className={`${tableStyles.controlBtn} ${
issuingSslId === domain.id ? styles.deployBusy : ''
}`}
onClick={() => void handleIssueDomainSsl(domain)}
disabled={issuingSslId === domain.id || issuingWebsiteSsl}
title={
issuingSslId === domain.id
? 'Issuing SSL…'
: 'Issue SSL certificate'
}
aria-label={
issuingSslId === domain.id
? 'Issuing SSL'
: 'Issue SSL certificate'
}
aria-busy={issuingSslId === domain.id}
>
{issuingSslId === domain.id ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<ShieldPlus size={16} />
)}
</button>
) : (
<button
type="button"
className={tableStyles.controlBtn}
onClick={() => void handleToggleSsl(domain, !domain.sslEnabled)}
disabled={togglingSslId === domain.id}
title={domain.sslEnabled ? 'Mark SSL invalid' : 'Mark SSL valid'}
aria-label={
domain.sslEnabled ? 'Mark SSL invalid' : 'Mark SSL valid'
}
>
{domain.sslEnabled ? <Lock size={16} /> : <Unlock size={16} />}
</button>
)}
<button
type="button"
className={tableStyles.controlBtn}
+23 -1
View File
@@ -1,5 +1,11 @@
import { apiRequest } from '../lib/api'
import type { DeployDomainResponse, DomainsListResponse, SyncSslResponse } from '../types/domain'
import type {
DeployDomainResponse,
DomainsListResponse,
IssueDomainSslResponse,
IssueWebsiteSslResponse,
SyncSslResponse,
} from '../types/domain'
export interface ListDomainsParams {
page?: number
@@ -67,3 +73,19 @@ export async function syncSsl() {
auth: true,
})
}
/** Batch-issue Let's Encrypt for storefronts missing SSL (websites VM). */
export async function issueWebsiteSsl() {
return apiRequest<IssueWebsiteSslResponse>('/domains/website-ssl', {
method: 'POST',
auth: true,
})
}
/** Issue Let's Encrypt for one storefront domain. */
export async function issueDomainSsl(domainId: number | string) {
return apiRequest<IssueDomainSslResponse>(`/domains/${domainId}/issue-ssl`, {
method: 'POST',
auth: true,
})
}
+14
View File
@@ -35,3 +35,17 @@ export interface SyncSslResponse {
status: 'accepted'
message: string
}
export interface IssueWebsiteSslResponse {
status: 'ok'
message: string
issued: string[]
failed: Array<{ host: string; error: string }>
}
export interface IssueDomainSslResponse {
status: 'issued'
host: string
sslEnabled: boolean
message: string
}