Add payment gateway configs and lock credentials
This commit is contained in:
parent
fa17b16b8b
commit
b1107864b5
5 changed files with 75 additions and 24 deletions
|
|
@ -38,6 +38,21 @@ CORS_ORIGINS=http://localhost:3000,http://localhost:8963
|
||||||
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||||
CLOUDFLARE_ZONE_ID=your-zone-id
|
CLOUDFLARE_ZONE_ID=your-zone-id
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stripe
|
||||||
|
# =============================================================================
|
||||||
|
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
|
||||||
|
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Gateway de Pagamento (Fictício)
|
||||||
|
# =============================================================================
|
||||||
|
PAYMENT_GATEWAY_MERCHANT_ID=merchant_demo
|
||||||
|
PAYMENT_GATEWAY_API_KEY=fake_gateway_key
|
||||||
|
PAYMENT_GATEWAY_ENDPOINT=https://payments.example.com/api
|
||||||
|
PAYMENT_GATEWAY_WEBHOOK_SECRET=fake_webhook_secret
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# cPanel API (for email management)
|
# cPanel API (for email management)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -51,3 +66,8 @@ CPANEL_API_TOKEN=your-cpanel-api-token
|
||||||
RESEND_API_KEY=re_xxxx_your_api_key
|
RESEND_API_KEY=re_xxxx_your_api_key
|
||||||
EMAIL_FROM=noreply@gohorsejobs.com
|
EMAIL_FROM=noreply@gohorsejobs.com
|
||||||
APP_URL=https://gohorsejobs.com
|
APP_URL=https://gohorsejobs.com
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LavinMQ (AMQP)
|
||||||
|
# =============================================================================
|
||||||
|
AMQP_URL=amqps://nwigjply:nwEGZdcfz3--H8xc68IKmjiBCVtI09Cq@horse.lmq.cloudamqp.com/nwigjply
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,14 @@ func (s *CredentialsService) BootstrapCredentials(ctx context.Context) error {
|
||||||
"publishableKey": os.Getenv("STRIPE_PUBLISHABLE_KEY"),
|
"publishableKey": os.Getenv("STRIPE_PUBLISHABLE_KEY"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"payment_gateway": func() interface{} {
|
||||||
|
return map[string]string{
|
||||||
|
"merchantId": os.Getenv("PAYMENT_GATEWAY_MERCHANT_ID"),
|
||||||
|
"apiKey": os.Getenv("PAYMENT_GATEWAY_API_KEY"),
|
||||||
|
"endpoint": os.Getenv("PAYMENT_GATEWAY_ENDPOINT"),
|
||||||
|
"webhookSecret": os.Getenv("PAYMENT_GATEWAY_WEBHOOK_SECRET"),
|
||||||
|
}
|
||||||
|
},
|
||||||
"storage": func() interface{} {
|
"storage": func() interface{} {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"endpoint": os.Getenv("AWS_ENDPOINT"),
|
"endpoint": os.Getenv("AWS_ENDPOINT"),
|
||||||
|
|
@ -277,6 +285,11 @@ func (s *CredentialsService) BootstrapCredentials(ctx context.Context) error {
|
||||||
"region": os.Getenv("AWS_REGION"),
|
"region": os.Getenv("AWS_REGION"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lavinmq": func() interface{} {
|
||||||
|
return map[string]string{
|
||||||
|
"amqpUrl": os.Getenv("AMQP_URL"),
|
||||||
|
}
|
||||||
|
},
|
||||||
"cloudflare_config": func() interface{} {
|
"cloudflare_config": func() interface{} {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"apiToken": os.Getenv("CLOUDFLARE_API_TOKEN"),
|
"apiToken": os.Getenv("CLOUDFLARE_API_TOKEN"),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
|
||||||
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
|
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
|
||||||
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
|
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Gateway de Pagamento (Fictício)
|
||||||
|
# =============================================================================
|
||||||
|
PAYMENT_GATEWAY_MERCHANT_ID=merchant_demo
|
||||||
|
PAYMENT_GATEWAY_API_KEY=fake_gateway_key
|
||||||
|
PAYMENT_GATEWAY_ENDPOINT=https://payments.example.com/api
|
||||||
|
PAYMENT_GATEWAY_WEBHOOK_SECRET=fake_webhook_secret
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Database
|
# Database
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -53,3 +61,8 @@ CLOUDFLARE_ZONE_ID=your-zone-id
|
||||||
CPANEL_HOST=https://cpanel.yourdomain.com:2083
|
CPANEL_HOST=https://cpanel.yourdomain.com:2083
|
||||||
CPANEL_USERNAME=your-cpanel-username
|
CPANEL_USERNAME=your-cpanel-username
|
||||||
CPANEL_API_TOKEN=your-cpanel-api-token
|
CPANEL_API_TOKEN=your-cpanel-api-token
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LavinMQ (AMQP)
|
||||||
|
# =============================================================================
|
||||||
|
AMQP_URL=amqps://nwigjply:nwEGZdcfz3--H8xc68IKmjiBCVtI09Cq@horse.lmq.cloudamqp.com/nwigjply
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { ExternalServicesCredentials } from './credentials.entity';
|
import { ExternalServicesCredentials } from './credentials.entity';
|
||||||
|
|
@ -35,6 +35,8 @@ export class CredentialsService {
|
||||||
let cred = await this.credentialsRepo.findOne({ where: { serviceName } });
|
let cred = await this.credentialsRepo.findOne({ where: { serviceName } });
|
||||||
if (!cred) {
|
if (!cred) {
|
||||||
cred = this.credentialsRepo.create({ serviceName });
|
cred = this.credentialsRepo.create({ serviceName });
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException('Credentials already configured for this service.');
|
||||||
}
|
}
|
||||||
cred.encryptedPayload = encrypted;
|
cred.encryptedPayload = encrypted;
|
||||||
cred.updatedBy = updatedBy;
|
cred.updatedBy = updatedBy;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { settingsApi, credentialsApi, ConfiguredService, storageApi } from "@/lib/api"
|
import { settingsApi, credentialsApi, ConfiguredService, storageApi } from "@/lib/api"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Loader2, Check, Key, Trash2 } from "lucide-react"
|
import { Loader2, Check, Key } from "lucide-react"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -107,19 +107,6 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDeleteCredential = async (serviceName: string) => {
|
|
||||||
if (!confirm(`Are you sure you want to remove credentials for ${serviceName}?`)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await credentialsApi.delete(serviceName)
|
|
||||||
toast.success(`Credentials for ${serviceName} removed`)
|
|
||||||
fetchCredentials()
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to delete credential", error)
|
|
||||||
toast.error("Failed to delete credential")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
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>
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +121,15 @@ export default function SettingsPage() {
|
||||||
{ key: "publishableKey", label: "Publishable Key (pk_...)", type: "text" },
|
{ key: "publishableKey", label: "Publishable Key (pk_...)", type: "text" },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
payment_gateway: {
|
||||||
|
label: "Gateway de Pagamento (Fictício)",
|
||||||
|
fields: [
|
||||||
|
{ key: "merchantId", label: "Merchant ID", type: "text" },
|
||||||
|
{ key: "apiKey", label: "API Key", type: "password" },
|
||||||
|
{ key: "endpoint", label: "Endpoint URL", type: "text" },
|
||||||
|
{ key: "webhookSecret", label: "Webhook Secret", type: "password" },
|
||||||
|
]
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
label: "AWS S3 / Compatible",
|
label: "AWS S3 / Compatible",
|
||||||
fields: [
|
fields: [
|
||||||
|
|
@ -152,6 +148,12 @@ export default function SettingsPage() {
|
||||||
{ key: "apiToken", label: "API Token", type: "password" },
|
{ key: "apiToken", label: "API Token", type: "password" },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
lavinmq: {
|
||||||
|
label: "LavinMQ (AMQP)",
|
||||||
|
fields: [
|
||||||
|
{ key: "amqpUrl", label: "AMQP URL", type: "password" },
|
||||||
|
]
|
||||||
|
},
|
||||||
cloudflare_config: {
|
cloudflare_config: {
|
||||||
label: "Cloudflare",
|
label: "Cloudflare",
|
||||||
fields: [
|
fields: [
|
||||||
|
|
@ -304,7 +306,7 @@ export default function SettingsPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>External Services</CardTitle>
|
<CardTitle>External Services</CardTitle>
|
||||||
<CardDescription>Manage credentials for third-party integrations securely. Keys are encrypted.</CardDescription>
|
<CardDescription>Manage credentials for third-party integrations securely. Keys are encrypted and locked after saving.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
|
@ -334,15 +336,16 @@ export default function SettingsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full gap-2">
|
<div className="flex w-full gap-2">
|
||||||
<Button variant="outline" size="sm" className="w-full" onClick={() => handleOpenCredentialDialog(svc.service_name)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleOpenCredentialDialog(svc.service_name)}
|
||||||
|
disabled={svc.is_configured}
|
||||||
|
>
|
||||||
<Key className="w-3 h-3 mr-2" />
|
<Key className="w-3 h-3 mr-2" />
|
||||||
{svc.is_configured ? "Edit" : "Setup"}
|
{svc.is_configured ? "Configured" : "Setup"}
|
||||||
</Button>
|
</Button>
|
||||||
{svc.is_configured && (
|
|
||||||
<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" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -358,7 +361,7 @@ export default function SettingsPage() {
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Configure {selectedService && (schemas[selectedService]?.label || selectedService)}</DialogTitle>
|
<DialogTitle>Configure {selectedService && (schemas[selectedService]?.label || selectedService)}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Enter credentials. Keys are encrypted before storage and hidden after saving.
|
Enter credentials. Keys are encrypted before storage, hidden after saving, and cannot be edited later.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue