fase(17): licensing module with RSA validation
This commit is contained in:
@@ -1,8 +1,60 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Shield } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Shield, CheckCircle2, XCircle, Loader2 } from 'lucide-react'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
|
||||
interface LicenseStatus {
|
||||
plan: 'free' | 'pro' | 'enterprise'
|
||||
organizationName: string
|
||||
email: string
|
||||
issuedAt: string
|
||||
expiresAt: string | null
|
||||
isValid: boolean
|
||||
features: string[]
|
||||
}
|
||||
|
||||
const PLAN_LABELS: Record<string, string> = {
|
||||
free: 'Free / OSS',
|
||||
pro: 'Pro',
|
||||
enterprise: 'Enterprise',
|
||||
}
|
||||
|
||||
export function LicenseSection() {
|
||||
const [licenseKey, setLicenseKey] = useState('')
|
||||
const [activateError, setActivateError] = useState<string | null>(null)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: status, isLoading } = useQuery<LicenseStatus>({
|
||||
queryKey: ['license-status'],
|
||||
queryFn: () => apiFetch('/api/license/status'),
|
||||
})
|
||||
|
||||
const { mutate: activate, isPending } = useMutation({
|
||||
mutationFn: (key: string) =>
|
||||
apiFetch<{ message: string; license: LicenseStatus }>('/api/license/activate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ licenseKey: key }),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setLicenseKey('')
|
||||
setActivateError(null)
|
||||
void queryClient.invalidateQueries({ queryKey: ['license-status'] })
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
setActivateError(err.message)
|
||||
},
|
||||
})
|
||||
|
||||
const handleActivate = () => {
|
||||
if (!licenseKey.trim()) return
|
||||
setActivateError(null)
|
||||
activate(licenseKey.trim())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-xl">
|
||||
<div>
|
||||
@@ -17,14 +69,92 @@ export function LicenseSection() {
|
||||
<CardTitle className="text-base">Current Plan</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading license status...
|
||||
</div>
|
||||
) : status ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Plan</span>
|
||||
<Badge variant={status.plan === 'free' ? 'secondary' : 'default'}>
|
||||
{PLAN_LABELS[status.plan] ?? status.plan}
|
||||
</Badge>
|
||||
</div>
|
||||
{status.plan !== 'free' && (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Organization</span>
|
||||
<span className="font-medium">{status.organizationName}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Email</span>
|
||||
<span className="font-medium">{status.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Issued</span>
|
||||
<span>{new Date(status.issuedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Expires</span>
|
||||
<span>{status.expiresAt ? new Date(status.expiresAt).toLocaleDateString() : 'Never'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
{status.isValid ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<CheckCircle2 className="h-4 w-4" /> Valid
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-red-600">
|
||||
<XCircle className="h-4 w-4" /> Expired
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-muted-foreground">Features</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{status.features.map((f) => (
|
||||
<Badge key={f} variant="outline" className="text-xs">
|
||||
{f}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Activate License</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Plan</span>
|
||||
<Badge>Free / OSS</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
License activation will be available in Phase 17 (RSA-signed keys with feature entitlements).
|
||||
</CardDescription>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Paste your license key below to unlock Pro or Enterprise features.
|
||||
</p>
|
||||
<Textarea
|
||||
placeholder="Paste license key..."
|
||||
value={licenseKey}
|
||||
onChange={(e) => setLicenseKey(e.target.value)}
|
||||
className="font-mono text-xs h-28 resize-none"
|
||||
/>
|
||||
{activateError && (
|
||||
<p className="text-sm text-red-600">{activateError}</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleActivate}
|
||||
disabled={!licenseKey.trim() || isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Activate License
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user