refactor: move credentials to settings page and update sidebar
This commit is contained in:
parent
1249c9a499
commit
895974878d
3 changed files with 148 additions and 293 deletions
|
|
@ -1,245 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { credentialsApi, ConfiguredService } from "@/lib/api"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { Check, Loader2, Plus, Shield, Trash2, X } from "lucide-react"
|
|
||||||
|
|
||||||
export default function CredentialsPage() {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [services, setServices] = useState<ConfiguredService[]>([])
|
|
||||||
const [openDialog, setOpenDialog] = useState(false)
|
|
||||||
const [selectedService, setSelectedService] = useState<string>("")
|
|
||||||
const [formData, setFormData] = useState<Record<string, string>>({})
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
|
|
||||||
// Predefined schemas for known services
|
|
||||||
const schemas: Record<string, { label: string, fields: { key: string, label: string, type?: string }[] }> = {
|
|
||||||
stripe: {
|
|
||||||
label: "Stripe",
|
|
||||||
fields: [
|
|
||||||
{ key: "secretKey", label: "Secret Key (sk_...)", type: "password" },
|
|
||||||
{ key: "webhookSecret", label: "Webhook Secret (whsec_...)", type: "password" },
|
|
||||||
{ key: "publishableKey", label: "Publishable Key (pk_...)", type: "text" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
label: "AWS S3 / Compatible",
|
|
||||||
fields: [
|
|
||||||
{ key: "endpoint", label: "Endpoint URL", type: "text" },
|
|
||||||
{ key: "region", label: "Region", type: "text" },
|
|
||||||
{ key: "bucket", label: "Bucket Name", type: "text" },
|
|
||||||
{ key: "accessKey", label: "Access Key ID", type: "text" },
|
|
||||||
{ key: "secretKey", label: "Secret Access Key", type: "password" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
cpanel: {
|
|
||||||
label: "cPanel Integration",
|
|
||||||
fields: [
|
|
||||||
{ key: "host", label: "cPanel URL (https://domain:2083)", type: "text" },
|
|
||||||
{ key: "username", label: "Username", type: "text" },
|
|
||||||
{ key: "apiToken", label: "API Token", type: "password" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
cloudflare_config: {
|
|
||||||
label: "Cloudflare",
|
|
||||||
fields: [
|
|
||||||
{ key: "apiToken", label: "API Token", type: "password" },
|
|
||||||
{ key: "zoneId", label: "Zone ID", type: "text" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
smtp: {
|
|
||||||
label: "SMTP Email",
|
|
||||||
fields: [
|
|
||||||
{ key: "host", label: "Host", type: "text" },
|
|
||||||
{ key: "port", label: "Port", type: "number" },
|
|
||||||
{ key: "username", label: "Username", type: "text" },
|
|
||||||
{ key: "password", label: "Password", type: "password" },
|
|
||||||
{ key: "from_email", label: "From Email", type: "email" },
|
|
||||||
{ key: "from_name", label: "From Name", type: "text" },
|
|
||||||
{ key: "secure", label: "Use TLS", type: "checkbox" } // TODO handle checkbox
|
|
||||||
]
|
|
||||||
},
|
|
||||||
appwrite: {
|
|
||||||
label: "Appwrite",
|
|
||||||
fields: [
|
|
||||||
{ key: "endpoint", label: "Endpoint", type: "text" },
|
|
||||||
{ key: "projectId", label: "Project ID", type: "text" },
|
|
||||||
{ key: "apiKey", label: "API Key", type: "password" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
firebase: {
|
|
||||||
label: "Firebase (JSON)",
|
|
||||||
fields: [
|
|
||||||
{ key: "serviceAccountJson", label: "Service Account JSON Content", type: "textarea" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableServices = Object.keys(schemas)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadServices()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const loadServices = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const res = await credentialsApi.list()
|
|
||||||
// Backend returns { services: [...] }
|
|
||||||
if (res && res.services) {
|
|
||||||
setServices(res.services)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to load credentials")
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (serviceName: string) => {
|
|
||||||
setSelectedService(serviceName)
|
|
||||||
setFormData({}) // Reset form, we don't load existing secrets for security
|
|
||||||
setOpenDialog(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!selectedService) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setSaving(true)
|
|
||||||
await credentialsApi.save(selectedService, formData)
|
|
||||||
toast.success(`${schemas[selectedService]?.label || selectedService} credentials saved!`)
|
|
||||||
setOpenDialog(false)
|
|
||||||
loadServices()
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message || "Failed to save")
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (serviceName: string) => {
|
|
||||||
if (!confirm(`Are you sure you want to delete credentials for ${serviceName}? This will break functionality relying on it.`)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await credentialsApi.delete(serviceName)
|
|
||||||
toast.success("Credentials deleted")
|
|
||||||
loadServices()
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error("Failed to delete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">System Credentials</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">
|
|
||||||
Manage external service connections securely. Keys are encrypted in the database.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{services.map((svc) => (
|
|
||||||
<Card key={svc.service_name} className={svc.is_configured ? "border-green-500/50" : "opacity-70"}>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
{schemas[svc.service_name]?.label || svc.service_name}
|
|
||||||
</CardTitle>
|
|
||||||
{svc.is_configured ? (
|
|
||||||
<Badge variant="default" className="bg-green-500/15 text-green-700 hover:bg-green-500/25 border-green-500/50">
|
|
||||||
<Check className="h-3 w-3 mr-1" /> Active
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="text-muted-foreground">
|
|
||||||
Pending
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold pt-2">
|
|
||||||
<Shield className={svc.is_configured ? "text-green-500" : "text-gray-400"} size={32} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-4">
|
|
||||||
{svc.is_configured
|
|
||||||
? `Last updated ${new Date(svc.updated_at).toLocaleDateString()}`
|
|
||||||
: "Not configured yet"}
|
|
||||||
</p>
|
|
||||||
<div className="mt-4 flex gap-2">
|
|
||||||
<Button variant="outline" size="sm" className="w-full" onClick={() => handleEdit(svc.service_name)}>
|
|
||||||
{svc.is_configured ? "Update" : "Configure"}
|
|
||||||
</Button>
|
|
||||||
{svc.is_configured && (
|
|
||||||
<Button variant="ghost" size="icon" className="text-destructive hover:bg-destructive/10" onClick={() => handleDelete(svc.service_name)}>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Configure {schemas[selectedService]?.label || selectedService}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Enter the credentials for this service. They will be encrypted before storage.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
{schemas[selectedService]?.fields.map((field) => (
|
|
||||||
<div key={field.key} className="space-y-2">
|
|
||||||
<Label htmlFor={field.key}>{field.label}</Label>
|
|
||||||
{field.type === 'textarea' ? (
|
|
||||||
<textarea
|
|
||||||
id={field.key}
|
|
||||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
value={formData[field.key] || ""}
|
|
||||||
onChange={(e) => setFormData({ ...formData, [field.key]: e.target.value })}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={field.key}
|
|
||||||
type={field.type}
|
|
||||||
value={formData[field.key] || ""}
|
|
||||||
onChange={(e) => setFormData({ ...formData, [field.key]: e.target.value })}
|
|
||||||
placeholder={`Enter ${field.label}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setOpenDialog(false)}>Cancel</Button>
|
|
||||||
<Button onClick={handleSave} disabled={saving}>
|
|
||||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Save Credentials
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default function SettingsPage() {
|
||||||
const [credentials, setCredentials] = useState<ConfiguredService[]>([])
|
const [credentials, setCredentials] = useState<ConfiguredService[]>([])
|
||||||
const [loadingCredentials, setLoadingCredentials] = useState(false)
|
const [loadingCredentials, setLoadingCredentials] = useState(false)
|
||||||
const [selectedService, setSelectedService] = useState<string | null>(null)
|
const [selectedService, setSelectedService] = useState<string | null>(null)
|
||||||
const [credentialPayload, setCredentialPayload] = useState("")
|
const [credentialPayload, setCredentialPayload] = useState<any>({})
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
|
@ -133,20 +133,112 @@ export default function SettingsPage() {
|
||||||
return <div className="flex justify-center p-8"><Loader2 className="animate-spin" /></div>
|
return <div className="flex justify-center p-8"><Loader2 className="animate-spin" /></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Predefined schemas for known services (Copied from CredentialsPage)
|
||||||
|
const schemas: Record<string, { label: string, fields: { key: string, label: string, type?: string }[] }> = {
|
||||||
|
stripe: {
|
||||||
|
label: "Stripe",
|
||||||
|
fields: [
|
||||||
|
{ key: "secretKey", label: "Secret Key (sk_...)", type: "password" },
|
||||||
|
{ key: "webhookSecret", label: "Webhook Secret (whsec_...)", type: "password" },
|
||||||
|
{ key: "publishableKey", label: "Publishable Key (pk_...)", type: "text" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
label: "AWS S3 / Compatible",
|
||||||
|
fields: [
|
||||||
|
{ key: "endpoint", label: "Endpoint URL", type: "text" },
|
||||||
|
{ key: "region", label: "Region", type: "text" },
|
||||||
|
{ key: "bucket", label: "Bucket Name", type: "text" },
|
||||||
|
{ key: "accessKey", label: "Access Key ID", type: "text" },
|
||||||
|
{ key: "secretKey", label: "Secret Access Key", type: "password" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
cpanel: {
|
||||||
|
label: "cPanel Integration",
|
||||||
|
fields: [
|
||||||
|
{ key: "host", label: "cPanel URL (https://domain:2083)", type: "text" },
|
||||||
|
{ key: "username", label: "Username", type: "text" },
|
||||||
|
{ key: "apiToken", label: "API Token", type: "password" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
cloudflare_config: {
|
||||||
|
label: "Cloudflare",
|
||||||
|
fields: [
|
||||||
|
{ key: "apiToken", label: "API Token", type: "password" },
|
||||||
|
{ key: "zoneId", label: "Zone ID", type: "text" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
label: "SMTP Email",
|
||||||
|
fields: [
|
||||||
|
{ key: "host", label: "Host", type: "text" },
|
||||||
|
{ key: "port", label: "Port", type: "number" },
|
||||||
|
{ key: "username", label: "Username", type: "text" },
|
||||||
|
{ key: "password", label: "Password", type: "password" },
|
||||||
|
{ key: "from_email", label: "From Email", type: "email" },
|
||||||
|
{ key: "from_name", label: "From Name", type: "text" },
|
||||||
|
{ key: "secure", label: "Use TLS", type: "checkbox" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
appwrite: {
|
||||||
|
label: "Appwrite",
|
||||||
|
fields: [
|
||||||
|
{ key: "endpoint", label: "Endpoint", type: "text" },
|
||||||
|
{ key: "projectId", label: "Project ID", type: "text" },
|
||||||
|
{ key: "apiKey", label: "API Key", type: "password" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
firebase: {
|
||||||
|
label: "Firebase (JSON)",
|
||||||
|
fields: [
|
||||||
|
{ key: "serviceAccountJson", label: "Service Account JSON Content", type: "textarea" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenCredentialDialog = (serviceName: string) => {
|
||||||
|
setSelectedService(serviceName)
|
||||||
|
setCredentialPayload({})
|
||||||
|
setIsDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveCredential = async () => {
|
||||||
|
if (!selectedService) return
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
await credentialsApi.save(selectedService, credentialPayload)
|
||||||
|
toast.success(`Credentials for ${schemas[selectedService]?.label || selectedService} saved`)
|
||||||
|
setIsDialogOpen(false)
|
||||||
|
fetchCredentials()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save credential", error)
|
||||||
|
toast.error("Failed to save credential")
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing imports ...
|
||||||
|
// Note: State definition needs update to object payload
|
||||||
|
// const [credentialPayload, setCredentialPayload] = useState<any>({})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Header ... */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
|
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
|
||||||
<p className="text-muted-foreground">Manage application appearance and integrations.</p>
|
<p className="text-muted-foreground">Manage application appearance and integrations.</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<Tabs defaultValue="theme" className="space-y-4">
|
<Tabs defaultValue="integrations" className="space-y-4"> {/* Default to integrations for visibility */}
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="theme">Branding & Theme</TabsTrigger>
|
<TabsTrigger value="theme">Branding & Theme</TabsTrigger>
|
||||||
<TabsTrigger value="integrations">Integrations & Credentials</TabsTrigger>
|
<TabsTrigger value="integrations">Integrations & Credentials</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Theme Tab Content (unchanged) */}
|
||||||
<TabsContent value="theme" className="space-y-4">
|
<TabsContent value="theme" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -209,36 +301,36 @@ export default function SettingsPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>External Services</CardTitle>
|
<CardTitle>External Services</CardTitle>
|
||||||
<CardDescription>Manage credentials for third-party integrations securely.</CardDescription>
|
<CardDescription>Manage credentials for third-party integrations securely. Keys are encrypted.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{credentials.map((service) => (
|
{credentials.map((svc) => (
|
||||||
<div key={service.service_name} className="flex items-center justify-between border-b pb-4 last:border-0 last:pb-0">
|
<div key={svc.service_name} className="flex flex-col justify-between border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
||||||
<div className="space-y-1">
|
<div className="space-y-2 mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium capitalize">{service.service_name}</p>
|
<p className="font-semibold capitalize">{schemas[svc.service_name]?.label || svc.service_name}</p>
|
||||||
{service.is_configured ? (
|
{svc.is_configured ? (
|
||||||
<Badge variant="default" className="bg-green-600 hover:bg-green-700">
|
<Badge variant="default" className="bg-green-600/20 text-green-700 hover:bg-green-600/30">
|
||||||
<Check className="w-3 h-3 mr-1" /> Configured
|
<Check className="w-3 h-3 mr-1" /> Active
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="secondary">Not Configured</Badge>
|
<Badge variant="outline">Pending</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{service.is_configured
|
{svc.is_configured
|
||||||
? `Updated on ${new Date(service.updated_at).toLocaleDateString()} by ${service.updated_by || 'Unknown'}`
|
? `Updated ${new Date(svc.updated_at).toLocaleDateString()}`
|
||||||
: 'No credentials saved for this service.'}
|
: 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 mt-auto">
|
||||||
<Button variant="outline" size="sm" onClick={() => handleOpenCredentialDialog(service.service_name)}>
|
<Button variant="outline" size="sm" className="w-full" onClick={() => handleOpenCredentialDialog(svc.service_name)}>
|
||||||
<Key className="w-4 h-4 mr-2" />
|
<Key className="w-3 h-3 mr-2" />
|
||||||
{service.is_configured ? "Update" : "Setup"}
|
{svc.is_configured ? "Edit" : "Setup"}
|
||||||
</Button>
|
</Button>
|
||||||
{service.is_configured && (
|
{svc.is_configured && (
|
||||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteCredential(service.service_name)}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:bg-destructive/10" onClick={() => handleDeleteCredential(svc.service_name)}>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -254,32 +346,40 @@ export default function SettingsPage() {
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Configure {selectedService}</DialogTitle>
|
<DialogTitle>Configure {selectedService && (schemas[selectedService]?.label || selectedService)}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Enter the secret credentials for {selectedService}. These will be encrypted and stored securely.
|
Enter credentials. Keys are encrypted before storage.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
{selectedService && schemas[selectedService]?.fields.map((field) => (
|
||||||
<Label htmlFor="payload">Secret Payload (JSON or Token)</Label>
|
<div key={field.key} className="space-y-2">
|
||||||
<div className="relative">
|
<Label htmlFor={field.key}>{field.label}</Label>
|
||||||
<Textarea
|
{field.type === 'textarea' ? (
|
||||||
id="payload"
|
<Textarea
|
||||||
value={credentialPayload}
|
id={field.key}
|
||||||
onChange={(e) => setCredentialPayload(e.target.value)}
|
className="font-mono text-xs min-h-[80px]"
|
||||||
placeholder={selectedService === 'firebase' ? 'Paste service-account.json content here...' : 'Paste API Key or Connection String...'}
|
value={(credentialPayload as any)[field.key] || ""}
|
||||||
className="min-h-[100px] font-mono text-xs pr-10" // Monospace for keys
|
onChange={(e) => setCredentialPayload({ ...credentialPayload, [field.key]: e.target.value })}
|
||||||
// Hack to masking if needed? No real way to mask textarea easily.
|
/>
|
||||||
// But typically service account JSONs are visible when pasting.
|
) : (
|
||||||
/>
|
<Input
|
||||||
|
id={field.key}
|
||||||
|
type={field.type === 'checkbox' ? 'checkbox' : field.type}
|
||||||
|
// TODO: Checkbox handling if needed
|
||||||
|
value={(credentialPayload as any)[field.key] || ""}
|
||||||
|
onChange={(e) => setCredentialPayload({ ...credentialPayload, [field.key]: e.target.value })}
|
||||||
|
placeholder={`Enter ${field.label}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
))}
|
||||||
{selectedService === 'firebase' && "Paste the entire content of your service-account.json"}
|
{/* Fallback for unknown services if any */}
|
||||||
{selectedService === 'stripe' && "Paste your Stripe Secret Key (sk_...)"}
|
{selectedService && !schemas[selectedService] && (
|
||||||
{selectedService === 'appwrite' && "Paste your Appwrite API Key"}
|
<div className="p-4 text-sm text-yellow-600 bg-yellow-50 rounded">
|
||||||
{selectedService === 'lavinmq' && "Paste your AMQP URL"}
|
Usage schema not defined for this service.
|
||||||
</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Link from "next/link"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { LayoutDashboard, Briefcase, Users, MessageSquare, Building2, FileText, HelpCircle, Ticket, Shield } from "lucide-react"
|
import { LayoutDashboard, Briefcase, Users, MessageSquare, Building2, FileText, HelpCircle, Ticket, Settings } from "lucide-react"
|
||||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||||
import { useTranslation } from "@/lib/i18n"
|
import { useTranslation } from "@/lib/i18n"
|
||||||
|
|
||||||
|
|
@ -56,9 +56,9 @@ const Sidebar = () => {
|
||||||
icon: Ticket,
|
icon: Ticket,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Credentials",
|
title: t('sidebar.settings') || "Settings", // Fallback if translation missing
|
||||||
href: "/dashboard/credentials",
|
href: "/dashboard/settings",
|
||||||
icon: Shield,
|
icon: Settings,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue