urmi-ai/frontend/src/features/agent/UrmiOtpPage.tsx
2026-10-02 11:22:40 +05:30

237 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ArrowRight, CheckCircle2, ExternalLink, KeyRound, Plus, ShieldCheck, Smartphone, X } from 'lucide-react'
import { useEffect, useState, type FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { urmiOtpApi, type UrmiOtpProject } from '../../lib/api'
const PROJECT_NAME_RE = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/
const MESSAGE_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9 -]{0,18}[A-Za-z0-9]$|^[A-Za-z0-9]$/
function ProjectModal({ onClose, onCreated }: {
onClose: () => void
onCreated: (project: UrmiOtpProject) => void
}) {
const [name, setName] = useState('')
const [messageLabel, setMessageLabel] = useState('')
const [appType, setAppType] = useState<'web' | 'mobile'>('web')
const [appUrl, setAppUrl] = useState('')
const [purpose, setPurpose] = useState('')
const [terms, setTerms] = useState(false)
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
async function submit(event: FormEvent) {
event.preventDefault()
setError('')
if (!PROJECT_NAME_RE.test(name) || name.includes('--')) {
setError('Use 6–30 lowercase letters, numbers, or hyphens; start with a letter and end with a letter or number')
return
}
const trimmedLabel = messageLabel.trim()
if (!trimmedLabel || trimmedLabel.length > 20 || !MESSAGE_LABEL_RE.test(trimmedLabel)) {
setError('Message label must be 1–20 characters: letters, numbers, spaces, or hyphens; start and end with a letter or number')
return
}
if (purpose.trim().length < 20) {
setError('Describe your app and OTP use case in at least 20 characters')
return
}
if (!terms) {
setError('You must agree to the terms and conditions')
return
}
setSaving(true)
try {
const project = await urmiOtpApi.createProject({
project_name: name,
message_label: trimmedLabel,
app_type: appType,
app_url: appUrl.trim(),
purpose: purpose.trim(),
terms_accepted: terms,
})
onCreated(project)
onClose()
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Unable to register project')
} finally {
setSaving(false)
}
}
return (
<div className="modal-overlay" onClick={onClose}>
<form className="modal-card otp-project-modal" onSubmit={submit} onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h2>Register an UrmiOTP project</h2>
<button type="button" className="icon-button" onClick={onClose} aria-label="Close"><X size={18} /></button>
</div>
<div className="otp-project-form">
<label>
<span>Project name</span>
<input
value={name}
onChange={(event) => setName(event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
placeholder="acme-login"
minLength={6}
maxLength={30}
required
/>
<small>Globally unique. Lowercase letters, numbers, and hyphens only.</small>
</label>
<label>
<span>Message label</span>
<input
value={messageLabel}
onChange={(event) => setMessageLabel(event.target.value.slice(0, 20))}
placeholder="your product identification in message"
maxLength={20}
required
/>
<small>Your product name in the message body; URMI stays as the platform identifier (max 20 characters).</small>
</label>
<fieldset>
<legend>Application type</legend>
<label><input type="radio" checked={appType === 'web'} onChange={() => setAppType('web')} /> Web application</label>
<label><input type="radio" checked={appType === 'mobile'} onChange={() => setAppType('mobile')} /> Mobile application</label>
</fieldset>
<label>
<span>{appType === 'web' ? 'Website or domain URL' : 'App store URL'}</span>
<input
type="url"
value={appUrl}
onChange={(event) => setAppUrl(event.target.value)}
placeholder={appType === 'web' ? 'https://app.example.com' : 'https://play.google.com/store/apps/details?id=...'}
required
/>
</label>
<label>
<span>Purpose and OTP use case</span>
<textarea
value={purpose}
onChange={(event) => setPurpose(event.target.value)}
placeholder="Describe your app, its users, and how verification codes will be used."
rows={4}
minLength={20}
maxLength={1000}
required
/>
</label>
<label className="otp-terms">
<input type="checkbox" checked={terms} onChange={(event) => setTerms(event.target.checked)} />
<span>I agree to the UrmiOTP terms, acceptable-use policy, and anti-abuse requirements.</span>
</label>
{error && <p className="auth-error">{error}</p>}
</div>
<div className="modal-actions">
<button className="primary-button" disabled={saving}>{saving ? 'Submitting…' : 'Submit for approval'}</button>
<button type="button" className="secondary-button" onClick={onClose}>Cancel</button>
</div>
</form>
</div>
)
}
function OtpMap() {
return (
<section className="panel otp-map">
<div className="otp-map-root"><KeyRound size={20} /><strong>URMI OTP</strong></div>
<div className="otp-map-branches">
<div><strong>Verification</strong><span>Signup · KYC · Identity</span></div>
<div><strong>Login</strong><span>MFA · 2FA · Passwordless</span></div>
<div><strong>Recovery</strong><span>Password reset · Access recovery</span></div>
<div><strong>Account changes</strong><span>Email · Phone · Security</span></div>
<div><strong>Action authorization</strong><span>Sensitive operations</span></div>
<div><strong>Transactions</strong><span>Payments · Transfers</span></div>
<div><strong>Device verification</strong><span>Pairing · IoT · Activation</span></div>
</div>
</section>
)
}
export function UrmiOtpPage() {
const navigate = useNavigate()
const [projects, setProjects] = useState<UrmiOtpProject[]>([])
const [modalOpen, setModalOpen] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
urmiOtpApi.listProjects()
.then(setProjects)
.catch(() => setError('Unable to load UrmiOTP projects'))
.finally(() => setLoading(false))
}, [])
if (loading) return <div className="auth-loading">Loading UrmiOTP…</div>
return (
<div className="urmiotp-page">
<div className="account-heading otp-page-heading">
<div>
<span className="eyebrow">Agent · Unified OTP</span>
<h1>UrmiOTP</h1>
<p>One verification platform and a small set of generic templates for every app and OTP journey.</p>
</div>
<button className="primary-button" onClick={() => setModalOpen(true)}><Plus size={15} /> Add a project</button>
</div>
<OtpMap />
<section className="panel otp-projects-panel">
<div className="panel-heading">
<div><h2>Registered projects</h2><p>Each app must be approved before its API documentation becomes available.</p></div>
<Smartphone size={19} />
</div>
{projects.length === 0 ? (
<div className="otp-empty">
<ShieldCheck size={24} />
<strong>No projects registered</strong>
<span>Add your web or mobile app to begin compliance review.</span>
</div>
) : (
<div className="otp-project-table">
<div className="otp-project-table-head">
<span>Project</span><span>Application</span><span>Status</span><span />
</div>
{projects.map((project) => (
<div className="otp-project-row" key={project.id}>
<div>
<strong>{project.project_name}</strong>
<small>Label: {project.message_label}</small>
<small>{project.purpose}</small>
</div>
<a href={project.app_url} target="_blank" rel="noreferrer">
{project.app_type}<ExternalLink size={12} />
</a>
<div>
<span className={`doc-status ${project.status}`}>{project.status}</span>
{project.review_note && <small>{project.review_note}</small>}
</div>
{project.status === 'approved' ? (
<button
className="secondary-button"
onClick={() => navigate(`/console/agent/urmiotp/projects/${project.id}/docs`)}
>
API documentation <ArrowRight size={14} />
</button>
) : project.status === 'pending' ? (
<span className="otp-awaiting"><CheckCircle2 size={13} /> Awaiting admin review</span>
) : (
<button className="secondary-button" onClick={() => setModalOpen(true)}>Register another</button>
)}
</div>
))}
</div>
)}
{error && <p className="auth-error">{error}</p>}
</section>
{modalOpen && (
<ProjectModal
onClose={() => setModalOpen(false)}
onCreated={(project) => setProjects((current) => [project, ...current])}
/>
)}
</div>
)
}