Implement secure Stripe credential management using RSA encryption
This commit is contained in:
parent
d771e2a3a9
commit
b1639dbcd8
10 changed files with 341 additions and 9 deletions
|
|
@ -30,9 +30,10 @@ type CoreHandlers struct {
|
||||||
notificationService *services.NotificationService
|
notificationService *services.NotificationService
|
||||||
ticketService *services.TicketService
|
ticketService *services.TicketService
|
||||||
adminService *services.AdminService
|
adminService *services.AdminService
|
||||||
|
credentialsService *services.CredentialsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase, upd *user.UpdateUserUseCase, lc *tenant.ListCompaniesUseCase, auditService *services.AuditService, notificationService *services.NotificationService, ticketService *services.TicketService, adminService *services.AdminService) *CoreHandlers {
|
func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase, upd *user.UpdateUserUseCase, lc *tenant.ListCompaniesUseCase, auditService *services.AuditService, notificationService *services.NotificationService, ticketService *services.TicketService, adminService *services.AdminService, credentialsService *services.CredentialsService) *CoreHandlers {
|
||||||
return &CoreHandlers{
|
return &CoreHandlers{
|
||||||
loginUC: l,
|
loginUC: l,
|
||||||
registerCandidateUC: reg,
|
registerCandidateUC: reg,
|
||||||
|
|
@ -46,6 +47,7 @@ func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c
|
||||||
notificationService: notificationService,
|
notificationService: notificationService,
|
||||||
ticketService: ticketService,
|
ticketService: ticketService,
|
||||||
adminService: adminService,
|
adminService: adminService,
|
||||||
|
credentialsService: credentialsService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -980,3 +982,51 @@ func (h *CoreHandlers) SaveFCMToken(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{"message": "Token saved successfully"})
|
json.NewEncoder(w).Encode(map[string]string{"message": "Token saved successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveCredentials saves encrypted credentials for external services.
|
||||||
|
// @Summary Save Credentials
|
||||||
|
// @Description Saves encrypted credentials payload (e.g. Stripe key encrypted by Backoffice)
|
||||||
|
// @Tags System
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param request body map[string]string true "Credentials Payload"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {string} string "Invalid Request"
|
||||||
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
|
// @Router /api/v1/system/credentials [post]
|
||||||
|
func (h *CoreHandlers) SaveCredentials(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
userIDVal := ctx.Value(middleware.ContextUserID)
|
||||||
|
userID, ok := userIDVal.(string)
|
||||||
|
if !ok || userID == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double check role is ADMIN or SUPERADMIN just in case middleware missed it (defense in depth)
|
||||||
|
// But middleware should handle it.
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ServiceName string `json:"serviceName"`
|
||||||
|
EncryptedPayload string `json:"encryptedPayload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ServiceName == "" || req.EncryptedPayload == "" {
|
||||||
|
http.Error(w, "serviceName and encryptedPayload are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.credentialsService.SaveCredentials(ctx, req.ServiceName, req.EncryptedPayload, userID); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"message": "Credentials saved successfully"})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -217,5 +217,6 @@ func createTestCoreHandlers(t *testing.T, loginUC *auth.LoginUseCase) *handlers.
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,15 @@ import (
|
||||||
// PaymentHandler handles Stripe payment operations
|
// PaymentHandler handles Stripe payment operations
|
||||||
type PaymentHandler struct {
|
type PaymentHandler struct {
|
||||||
jobService *services.JobService
|
jobService *services.JobService
|
||||||
|
credentialsService *services.CredentialsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPaymentHandler creates a new payment handler
|
// NewPaymentHandler creates a new payment handler
|
||||||
func NewPaymentHandler(jobService *services.JobService) *PaymentHandler {
|
func NewPaymentHandler(jobService *services.JobService, credentialsService *services.CredentialsService) *PaymentHandler {
|
||||||
return &PaymentHandler{jobService: jobService}
|
return &PaymentHandler{
|
||||||
|
jobService: jobService,
|
||||||
|
credentialsService: credentialsService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCheckoutRequest represents a checkout session request
|
// CreateCheckoutRequest represents a checkout session request
|
||||||
|
|
@ -64,12 +68,16 @@ func (h *PaymentHandler) CreateCheckout(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Stripe secret key
|
// Get Stripe secret key from encrypted vault
|
||||||
stripeSecretKey := os.Getenv("STRIPE_SECRET_KEY")
|
stripeSecretKey, err := h.credentialsService.GetDecryptedKey(r.Context(), "stripe")
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to Env if not found (for migration/dev)
|
||||||
|
stripeSecretKey = os.Getenv("STRIPE_SECRET_KEY")
|
||||||
if stripeSecretKey == "" {
|
if stripeSecretKey == "" {
|
||||||
http.Error(w, "Payment service not configured", http.StatusInternalServerError)
|
http.Error(w, "Payment service not configured", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create Stripe checkout session via API
|
// Create Stripe checkout session via API
|
||||||
sessionID, checkoutURL, err := createStripeCheckoutSession(stripeSecretKey, req)
|
sessionID, checkoutURL, err := createStripeCheckoutSession(stripeSecretKey, req)
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ func NewRouter() http.Handler {
|
||||||
|
|
||||||
authMiddleware := middleware.NewMiddleware(authService)
|
authMiddleware := middleware.NewMiddleware(authService)
|
||||||
adminService := services.NewAdminService(database.DB)
|
adminService := services.NewAdminService(database.DB)
|
||||||
|
credentialsService := services.NewCredentialsService(database.DB)
|
||||||
|
|
||||||
coreHandlers := apiHandlers.NewCoreHandlers(
|
coreHandlers := apiHandlers.NewCoreHandlers(
|
||||||
loginUC,
|
loginUC,
|
||||||
|
|
@ -80,6 +81,7 @@ func NewRouter() http.Handler {
|
||||||
notificationService, // Added
|
notificationService, // Added
|
||||||
ticketService, // Added
|
ticketService, // Added
|
||||||
adminService, // Added for RBAC support
|
adminService, // Added for RBAC support
|
||||||
|
credentialsService, // Added for Encrypted Credentials
|
||||||
)
|
)
|
||||||
|
|
||||||
adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService)
|
adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService)
|
||||||
|
|
@ -87,6 +89,7 @@ func NewRouter() http.Handler {
|
||||||
// Initialize Legacy Handlers
|
// Initialize Legacy Handlers
|
||||||
jobHandler := handlers.NewJobHandler(jobService)
|
jobHandler := handlers.NewJobHandler(jobService)
|
||||||
applicationHandler := handlers.NewApplicationHandler(applicationService)
|
applicationHandler := handlers.NewApplicationHandler(applicationService)
|
||||||
|
paymentHandler := handlers.NewPaymentHandler(jobService, credentialsService)
|
||||||
|
|
||||||
// --- IP HELPER ---
|
// --- IP HELPER ---
|
||||||
GetClientIP := func(r *http.Request) string {
|
GetClientIP := func(r *http.Request) string {
|
||||||
|
|
@ -206,6 +209,9 @@ func NewRouter() http.Handler {
|
||||||
mux.Handle("GET /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.GetTicket)))
|
mux.Handle("GET /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.GetTicket)))
|
||||||
mux.Handle("POST /api/v1/support/tickets/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.AddMessage)))
|
mux.Handle("POST /api/v1/support/tickets/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.AddMessage)))
|
||||||
|
|
||||||
|
// System Credentials Route
|
||||||
|
mux.Handle("POST /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(coreHandlers.SaveCredentials))))
|
||||||
|
|
||||||
// Application Routes
|
// Application Routes
|
||||||
mux.HandleFunc("POST /api/v1/applications", applicationHandler.CreateApplication)
|
mux.HandleFunc("POST /api/v1/applications", applicationHandler.CreateApplication)
|
||||||
mux.HandleFunc("GET /api/v1/applications", applicationHandler.GetApplications)
|
mux.HandleFunc("GET /api/v1/applications", applicationHandler.GetApplications)
|
||||||
|
|
@ -213,6 +219,11 @@ func NewRouter() http.Handler {
|
||||||
mux.HandleFunc("PUT /api/v1/applications/{id}/status", applicationHandler.UpdateApplicationStatus)
|
mux.HandleFunc("PUT /api/v1/applications/{id}/status", applicationHandler.UpdateApplicationStatus)
|
||||||
mux.HandleFunc("DELETE /api/v1/applications/{id}", applicationHandler.DeleteApplication)
|
mux.HandleFunc("DELETE /api/v1/applications/{id}", applicationHandler.DeleteApplication)
|
||||||
|
|
||||||
|
// Payment Routes
|
||||||
|
mux.Handle("POST /api/v1/payments/create-checkout", authMiddleware.HeaderAuthGuard(http.HandlerFunc(paymentHandler.CreateCheckout)))
|
||||||
|
mux.HandleFunc("POST /api/v1/payments/webhook", paymentHandler.HandleWebhook)
|
||||||
|
mux.HandleFunc("GET /api/v1/payments/status/{id}", paymentHandler.GetPaymentStatus)
|
||||||
|
|
||||||
// --- STORAGE ROUTES ---
|
// --- STORAGE ROUTES ---
|
||||||
// Initialize S3 Storage (optional - graceful degradation if not configured)
|
// Initialize S3 Storage (optional - graceful degradation if not configured)
|
||||||
s3Storage, err := storage.NewS3Storage()
|
s3Storage, err := storage.NewS3Storage()
|
||||||
|
|
|
||||||
135
backend/internal/services/credentials_service.go
Normal file
135
backend/internal/services/credentials_service.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CredentialsService struct {
|
||||||
|
DB *sql.DB
|
||||||
|
// Cache for decrypted keys
|
||||||
|
cache map[string]string
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCredentialsService(db *sql.DB) *CredentialsService {
|
||||||
|
return &CredentialsService{
|
||||||
|
DB: db,
|
||||||
|
cache: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCredentials saves the encrypted payload for a service
|
||||||
|
func (s *CredentialsService) SaveCredentials(ctx context.Context, serviceName, encryptedPayload, updatedBy string) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO external_services_credentials (service_name, encrypted_payload, updated_by, updated_at)
|
||||||
|
VALUES ($1, $2, $3, NOW())
|
||||||
|
ON CONFLICT (service_name)
|
||||||
|
DO UPDATE SET
|
||||||
|
encrypted_payload = EXCLUDED.encrypted_payload,
|
||||||
|
updated_by = EXCLUDED.updated_by,
|
||||||
|
updated_at = NOW()
|
||||||
|
`
|
||||||
|
_, err := s.DB.ExecContext(ctx, query, serviceName, encryptedPayload, updatedBy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.cacheMutex.Lock()
|
||||||
|
delete(s.cache, serviceName)
|
||||||
|
s.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDecryptedKey retrieves and decrypts the key for a service
|
||||||
|
func (s *CredentialsService) GetDecryptedKey(ctx context.Context, serviceName string) (string, error) {
|
||||||
|
// Check cache first
|
||||||
|
s.cacheMutex.RLock()
|
||||||
|
if val, ok := s.cache[serviceName]; ok {
|
||||||
|
s.cacheMutex.RUnlock()
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
s.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
// Fetch from DB
|
||||||
|
var encryptedPayload string
|
||||||
|
query := `SELECT encrypted_payload FROM external_services_credentials WHERE service_name = $1`
|
||||||
|
err := s.DB.QueryRowContext(ctx, query, serviceName).Scan(&encryptedPayload)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", fmt.Errorf("credentials for service %s not found", serviceName)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
decrypted, err := s.decryptPayload(encryptedPayload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decrypt credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
s.cacheMutex.Lock()
|
||||||
|
s.cache[serviceName] = decrypted
|
||||||
|
s.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CredentialsService) decryptPayload(encryptedPayload string) (string, error) {
|
||||||
|
// 1. Decode Private Key from Env
|
||||||
|
rawPrivateKey, err := base64.StdEncoding.DecodeString(os.Getenv("RSA_PRIVATE_KEY_BASE64"))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode env RSA private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(rawPrivateKey)
|
||||||
|
if block == nil {
|
||||||
|
return "", fmt.Errorf("failed to parse PEM block containing the private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
// Try generic PKCS8 if PKCS1 fails
|
||||||
|
if key, err2 := x509.ParsePKCS8PrivateKey(block.Bytes); err2 == nil {
|
||||||
|
if rsaKey, ok := key.(*rsa.PrivateKey); ok {
|
||||||
|
privKey = rsaKey
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("key is not RSA")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Decode ciphertext
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPayload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Decrypt using RSA-OAEP
|
||||||
|
plaintext, err := rsa.DecryptOAEP(
|
||||||
|
sha256.New(),
|
||||||
|
rand.Reader,
|
||||||
|
privKey,
|
||||||
|
ciphertext,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- Migration: Create external_services_credentials table
|
||||||
|
-- Description: Stores encrypted credentials for third-party services (Stripe, Cloudflare, etc.)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS external_services_credentials (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||||
|
service_name VARCHAR(50) UNIQUE NOT NULL, -- e.g. 'stripe', 'cloudflare'
|
||||||
|
encrypted_payload TEXT NOT NULL, -- RSA Encrypted Base64
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_by UUID, -- ID of the admin who updated it
|
||||||
|
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for fast lookup by service name
|
||||||
|
CREATE INDEX idx_service_name ON external_services_credentials(service_name);
|
||||||
|
|
||||||
|
COMMENT ON TABLE external_services_credentials IS 'Stores securely encrypted credentials for external services';
|
||||||
|
|
@ -7,6 +7,7 @@ import { PlansModule } from './plans';
|
||||||
import { AdminModule } from './admin';
|
import { AdminModule } from './admin';
|
||||||
import { AuthModule } from './auth';
|
import { AuthModule } from './auth';
|
||||||
import { FcmTokensModule } from './fcm-tokens/fcm-tokens.module';
|
import { FcmTokensModule } from './fcm-tokens/fcm-tokens.module';
|
||||||
|
import { ExternalServicesModule } from './external-services/external-services.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -16,6 +17,7 @@ import { FcmTokensModule } from './fcm-tokens/fcm-tokens.module';
|
||||||
PlansModule,
|
PlansModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
FcmTokensModule,
|
FcmTokensModule,
|
||||||
|
ExternalServicesModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Controller, Post, Body, Headers, UseGuards, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ExternalServicesService } from './external-services.service';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
class SaveStripeKeyDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
secretKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('admin/credentials')
|
||||||
|
export class ExternalServicesController {
|
||||||
|
constructor(private readonly externalServicesService: ExternalServicesService) { }
|
||||||
|
|
||||||
|
@Post('stripe')
|
||||||
|
async saveStripeKey(
|
||||||
|
@Body() dto: SaveStripeKeyDto,
|
||||||
|
@Headers('authorization') authHeader: string
|
||||||
|
) {
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new UnauthorizedException('Missing Authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.externalServicesService.saveStripeKey(dto.secretKey, authHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backoffice/src/external-services/external-services.module.ts
Normal file
12
backoffice/src/external-services/external-services.module.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ExternalServicesService } from './external-services.service';
|
||||||
|
import { ExternalServicesController } from './external-services.controller';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [HttpModule, ConfigModule],
|
||||||
|
controllers: [ExternalServicesController],
|
||||||
|
providers: [ExternalServicesService],
|
||||||
|
})
|
||||||
|
export class ExternalServicesModule { }
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { publicEncrypt, constants } from 'crypto';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExternalServicesService {
|
||||||
|
private readonly logger = new Logger(ExternalServicesService.name);
|
||||||
|
private readonly publicKey: string;
|
||||||
|
private readonly coreApiUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
const b64Key = this.configService.get<string>('RSA_PUBLIC_KEY_BASE64');
|
||||||
|
if (b64Key) {
|
||||||
|
try {
|
||||||
|
this.publicKey = Buffer.from(b64Key, 'base64').toString('utf-8');
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('Failed to decode RSA Public Key', e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn('RSA_PUBLIC_KEY_BASE64 is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.coreApiUrl = this.configService.get<string>('CORE_API_URL') || 'http://localhost:8521';
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveStripeKey(rawSecret: string, authHeader: string) {
|
||||||
|
if (!this.publicKey) {
|
||||||
|
throw new Error('RSA Public Key is not configured in Backoffice');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encrypted = publicEncrypt(
|
||||||
|
{
|
||||||
|
key: this.publicKey,
|
||||||
|
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
||||||
|
oaepHash: 'sha256',
|
||||||
|
},
|
||||||
|
Buffer.from(rawSecret, 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedBase64 = encrypted.toString('base64');
|
||||||
|
|
||||||
|
// Send to Core API
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post(
|
||||||
|
`${this.coreApiUrl}/api/v1/system/credentials`,
|
||||||
|
{
|
||||||
|
serviceName: 'stripe',
|
||||||
|
encryptedPayload: encryptedBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: authHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to save Stripe key', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue