mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-12 06:40:57 +04:30
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:
co-authored by
Cursor
parent
656fc57494
commit
aeaad0f645
@@ -54,3 +54,10 @@
|
||||
.deployFail {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user