Add payment gateway configs and lock credentials

This commit is contained in:
Tiago Yamamoto 2026-01-03 20:28:21 -03:00
parent fa17b16b8b
commit b1107864b5
5 changed files with 75 additions and 24 deletions

View file

@ -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

View file

@ -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"),

View file

@ -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

View file

@ -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;

View file

@ -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">