first commit
This commit is contained in:
commit
efa5080897
127 changed files with 18610 additions and 0 deletions
57
frontend/src/features/auth/AccountSettings.tsx
Normal file
57
frontend/src/features/auth/AccountSettings.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Check, KeyRound, ShieldCheck } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
import { authApi } from '../../lib/api'
|
||||
import { useAuth } from './auth-context'
|
||||
|
||||
export function AccountSettings() {
|
||||
const { user, setUser } = useAuth()
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmation, setConfirmation] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
setError('')
|
||||
setMessage('')
|
||||
if (password !== confirmation) {
|
||||
setError('Passwords do not match')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const updated = await authApi.setPassword(password, currentPassword)
|
||||
setUser(updated)
|
||||
setCurrentPassword('')
|
||||
setPassword('')
|
||||
setConfirmation('')
|
||||
setMessage(updated.has_password ? 'Your password has been saved.' : 'Password configured.')
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'Unable to save password')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="account-page">
|
||||
<div className="account-heading"><span className="eyebrow">Account</span><h1>Sign-in & security</h1><p>Manage how you access your Urmi workspace.</p></div>
|
||||
<div className="account-grid">
|
||||
<section className="panel account-profile">
|
||||
<div className="account-avatar">{user?.avatar_url ? <img src={user.avatar_url} alt="" /> : user?.name.slice(0, 2).toUpperCase()}</div>
|
||||
<div><h2>{user?.name}</h2><p>{user?.email}</p><span><ShieldCheck size={14} /> {user?.email_verified ? 'Verified email' : 'Email verification pending'}</span></div>
|
||||
</section>
|
||||
<section className="panel account-password">
|
||||
<div className="panel-heading"><div><h2>{user?.has_password ? 'Change password' : 'Add a password'}</h2><p>{user?.has_password ? 'Update your first-party Urmi credential.' : 'You signed up with Google. Add a password to use either sign-in method.'}</p></div><KeyRound size={20} /></div>
|
||||
<form onSubmit={submit}>
|
||||
{user?.has_password && <label><span>Current password</span><input required type="password" autoComplete="current-password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} placeholder="Your current password" /></label>}
|
||||
<label><span>New password</span><input required minLength={10} type="password" autoComplete="new-password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="At least 10 characters" /></label>
|
||||
<label><span>Confirm password</span><input required minLength={10} type="password" autoComplete="new-password" value={confirmation} onChange={(e) => setConfirmation(e.target.value)} placeholder="Repeat password" /></label>
|
||||
{error && <p className="auth-error">{error}</p>}
|
||||
{message && <p className="account-success"><Check size={14} /> {message}</p>}
|
||||
<button className="primary-button" type="submit">Save password</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
frontend/src/features/auth/AuthContext.tsx
Normal file
25
frontend/src/features/auth/AuthContext.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { authApi, type AuthUser } from '../../lib/api'
|
||||
import { AuthContext } from './auth-context'
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
authApi.me().then(setUser).catch(() => setUser(null)).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const value = useMemo(() => ({
|
||||
user,
|
||||
loading,
|
||||
setUser,
|
||||
logout: async () => {
|
||||
await authApi.logout()
|
||||
setUser(null)
|
||||
},
|
||||
}), [user, loading])
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
89
frontend/src/features/auth/AuthPage.tsx
Normal file
89
frontend/src/features/auth/AuthPage.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { ArrowLeft, ArrowRight, Check, Eye, EyeOff, LockKeyhole, Mail, User } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { authApi } from '../../lib/api'
|
||||
import { useAuth } from './auth-context'
|
||||
|
||||
export function AuthPage({ initialMode }: { initialMode: 'login' | 'register' }) {
|
||||
const [mode, setMode] = useState(initialMode)
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const { setUser } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
setError('')
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const user = mode === 'login'
|
||||
? await authApi.login(email, password)
|
||||
: await authApi.register(name, email, password)
|
||||
setUser(user)
|
||||
const destination = (location.state as { from?: string } | null)?.from ?? '/console'
|
||||
navigate(destination, { replace: true })
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'Unable to continue')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
function switchMode(nextMode: 'login' | 'register') {
|
||||
setMode(nextMode)
|
||||
setError('')
|
||||
navigate(nextMode === 'login' ? '/login' : '/register', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-visual">
|
||||
<Link to="/" className="auth-brand"><img src="/urmi-logo.png" alt="Urmi" /></Link>
|
||||
<div className="auth-message">
|
||||
<span>SMS FOR AI AGENTS</span>
|
||||
<h1>From agent decision to human action.</h1>
|
||||
<p>Secure messaging infrastructure with delivery, compliance, and observability built in.</p>
|
||||
<div><span><Check size={15} /> Unified agent messaging API</span><span><Check size={15} /> India-ready compliance</span><span><Check size={15} /> Complete event visibility</span></div>
|
||||
</div>
|
||||
<small>Build → Test → Deploy → Observe</small>
|
||||
</div>
|
||||
|
||||
<main className="auth-main">
|
||||
<Link to="/" className="back-home"><ArrowLeft size={15} /> Back to Urmi</Link>
|
||||
<section className="auth-card">
|
||||
<div className="auth-tabs">
|
||||
<button className={mode === 'login' ? 'active' : ''} onClick={() => switchMode('login')}>Log in</button>
|
||||
<button className={mode === 'register' ? 'active' : ''} onClick={() => switchMode('register')}>Create account</button>
|
||||
</div>
|
||||
<div className="auth-heading">
|
||||
<h2>{mode === 'login' ? 'Welcome back' : 'Start building with Urmi'}</h2>
|
||||
<p>{mode === 'login' ? 'Access your agent messaging workspace.' : 'Create your workspace and send your first test message.'}</p>
|
||||
</div>
|
||||
|
||||
<a className="google-button" href={authApi.googleUrl}>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#4285F4" d="M21.6 12.23c0-.71-.06-1.39-.18-2.05H12v3.87h5.38a4.6 4.6 0 0 1-2 3.02v2.51h3.24c1.9-1.75 2.98-4.33 2.98-7.35Z"/><path fill="#34A853" d="M12 22c2.7 0 4.98-.9 6.63-2.42l-3.24-2.51c-.9.6-2.05.96-3.39.96-2.61 0-4.82-1.76-5.61-4.13H3.04v2.59A10 10 0 0 0 12 22Z"/><path fill="#FBBC05" d="M6.39 13.9A6.01 6.01 0 0 1 6.08 12c0-.66.11-1.3.31-1.9V7.51H3.04A10 10 0 0 0 2 12c0 1.61.38 3.14 1.04 4.49l3.35-2.59Z"/><path fill="#EA4335" d="M12 5.97c1.47 0 2.79.51 3.83 1.5l2.87-2.88A9.64 9.64 0 0 0 12 2a10 10 0 0 0-8.96 5.51l3.35 2.59C7.18 7.73 9.39 5.97 12 5.97Z"/></svg>
|
||||
Continue with Google
|
||||
</a>
|
||||
|
||||
<div className="auth-divider"><span>or continue with email</span></div>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
{mode === 'register' && <label><span>Name</span><div><User size={17} /><input required minLength={2} autoComplete="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" /></div></label>}
|
||||
<label><span>Email address</span><div><Mail size={17} /><input required type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@company.com" /></div></label>
|
||||
<label><span>Password</span><div><LockKeyhole size={17} /><input required minLength={mode === 'register' ? 10 : 1} type={showPassword ? 'text' : 'password'} autoComplete={mode === 'login' ? 'current-password' : 'new-password'} value={password} onChange={(e) => setPassword(e.target.value)} placeholder={mode === 'register' ? 'At least 10 characters' : 'Your password'} /><button type="button" aria-label="Toggle password visibility" onClick={() => setShowPassword(!showPassword)}>{showPassword ? <EyeOff size={17} /> : <Eye size={17} />}</button></div></label>
|
||||
{mode === 'login' && <div className="forgot-row"><span>Secure, encrypted sign in</span><button type="button">Forgot password?</button></div>}
|
||||
{error && <p className="auth-error">{error}</p>}
|
||||
<button className="auth-submit" disabled={submitting}>{submitting ? 'Please wait…' : mode === 'login' ? 'Log in to Urmi' : 'Create my account'} {!submitting && <ArrowRight size={17} />}</button>
|
||||
</form>
|
||||
<p className="auth-terms">By continuing, you agree to Urmi’s Terms of Service and Privacy Policy.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
frontend/src/features/auth/ProtectedRoute.tsx
Normal file
11
frontend/src/features/auth/ProtectedRoute.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from './auth-context'
|
||||
|
||||
export function ProtectedRoute() {
|
||||
const { user, loading } = useAuth()
|
||||
const location = useLocation()
|
||||
|
||||
if (loading) return <div className="auth-loading">Loading Urmi…</div>
|
||||
if (!user) return <Navigate to="/login" replace state={{ from: location.pathname }} />
|
||||
return <Outlet />
|
||||
}
|
||||
17
frontend/src/features/auth/auth-context.ts
Normal file
17
frontend/src/features/auth/auth-context.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import type { AuthUser } from '../../lib/api'
|
||||
|
||||
export type AuthContextValue = {
|
||||
user: AuthUser | null
|
||||
loading: boolean
|
||||
setUser: (user: AuthUser | null) => void
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue | null>(null)
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) throw new Error('useAuth must be used inside AuthProvider')
|
||||
return context
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue