mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
9 files changed
+418
-36
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Settings as SettingsIcon, Bell, Key, Shield } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Settings as SettingsIcon, Bell, Key, Shield, Cloud } from "lucide-react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -7,11 +8,52 @@ import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
|
||||
export default function Settings() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [notifications, setNotifications] = useState(true);
|
||||
const [autoPublish, setAutoPublish] = useState(true);
|
||||
const [cloudName, setCloudName] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiSecret, setApiSecret] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: cloudinaryConfig } = useQuery({
|
||||
queryKey: ['/api/cloudinary/config'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cloudinaryConfig) {
|
||||
setCloudName((cloudinaryConfig as any).cloudName || "");
|
||||
setApiKey((cloudinaryConfig as any).apiKey || "");
|
||||
}
|
||||
}, [cloudinaryConfig]);
|
||||
|
||||
const saveCloudinaryMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiRequest('POST', '/api/cloudinary/config', {
|
||||
cloudName,
|
||||
apiKey,
|
||||
apiSecret,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/cloudinary/config'] });
|
||||
toast({
|
||||
title: "Configuration sauvegardée",
|
||||
description: "Vos paramètres Cloudinary ont été enregistrés",
|
||||
});
|
||||
setApiSecret(""); // Clear the secret after saving
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de sauvegarder la configuration",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
@@ -109,6 +151,67 @@ export default function Settings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Cloud className="w-5 h-5" />
|
||||
Configuration Cloudinary
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configurez Cloudinary pour le stockage d'images dans le cloud
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cloudName">Cloud Name</Label>
|
||||
<Input
|
||||
id="cloudName"
|
||||
type="text"
|
||||
value={cloudName}
|
||||
onChange={(e) => setCloudName(e.target.value)}
|
||||
placeholder="mon-cloud-name"
|
||||
data-testid="input-cloud-name"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Trouvez-le dans votre tableau de bord Cloudinary
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
id="apiKey"
|
||||
type="text"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="123456789012345"
|
||||
data-testid="input-api-key"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiSecret">API Secret</Label>
|
||||
<Input
|
||||
id="apiSecret"
|
||||
type="password"
|
||||
value={apiSecret}
|
||||
onChange={(e) => setApiSecret(e.target.value)}
|
||||
placeholder="••••••••••••••••"
|
||||
data-testid="input-api-secret"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Votre API Secret restera sécurisé et ne sera jamais exposé
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveCloudinaryMutation.mutate()}
|
||||
disabled={saveCloudinaryMutation.isPending || !cloudName || !apiKey || !apiSecret}
|
||||
data-testid="button-save-cloudinary"
|
||||
className="w-full"
|
||||
>
|
||||
{saveCloudinaryMutation.isPending ? "Enregistrement..." : "Enregistrer Cloudinary"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
@@ -125,12 +228,6 @@ export default function Settings() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button data-testid="button-save-settings">
|
||||
Enregistrer les modifications
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -40,8 +40,6 @@ services:
|
||||
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
|
||||
SESSION_SECRET: ${SESSION_SECRET:-your-secret-key-change-me}
|
||||
APP_URL: ${APP_URL:-http://localhost:5000}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
networks:
|
||||
- internal
|
||||
- nginx_default
|
||||
|
||||
Generated
+25
@@ -44,6 +44,7 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/pg": "^8.15.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cloudinary": "^2.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
@@ -4548,6 +4549,19 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/cloudinary": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.7.0.tgz",
|
||||
"integrity": "sha512-qrqDn31+qkMCzKu1GfRpzPNAO86jchcNwEHCUiqvPHNSFqu7FTNF9FuAkBUyvM1CFFgFPu64NT0DyeREwLwK0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"q": "^1.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=9"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -7020,6 +7034,17 @@
|
||||
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/q": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
|
||||
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0",
|
||||
"teleport": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/pg": "^8.15.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cloudinary": "^2.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
|
||||
@@ -55,7 +55,7 @@ Preferred communication style: Simple, everyday language.
|
||||
- `/api/pages/*` - Social page management
|
||||
- `/api/posts/*` - Post and scheduling management
|
||||
|
||||
**File Upload**: Multer middleware for handling multipart/form-data with in-memory storage strategy
|
||||
**File Upload**: Multer middleware for handling multipart/form-data with in-memory storage strategy, integrated with Cloudinary for cloud storage
|
||||
|
||||
**Background Processing**:
|
||||
- Node-cron for scheduled task execution
|
||||
@@ -65,7 +65,7 @@ Preferred communication style: Simple, everyday language.
|
||||
**Session Management**: Stateless authentication with session cookies (connect-pg-simple for PostgreSQL session store)
|
||||
|
||||
**Key Architectural Patterns**:
|
||||
- Service layer pattern (OpenRouterService, ImageProcessor, SchedulerService)
|
||||
- Service layer pattern (OpenRouterService, CloudinaryService, SchedulerService)
|
||||
- Storage abstraction layer for database operations
|
||||
- Middleware-based request processing pipeline
|
||||
- Separation of concerns between routes, services, and storage
|
||||
@@ -80,8 +80,9 @@ Preferred communication style: Simple, everyday language.
|
||||
|
||||
Core Tables:
|
||||
- `users` - User authentication and profiles
|
||||
- `cloudinary_config` - Cloudinary configuration per user (cloud name, API key, API secret)
|
||||
- `social_pages` - Connected Facebook/Instagram pages with access tokens
|
||||
- `media` - Uploaded media files with platform-specific processed versions
|
||||
- `media` - Uploaded media files with Cloudinary public IDs and transformation URLs
|
||||
- `posts` - Post content with AI generation tracking and status
|
||||
- `scheduled_posts` - Scheduled publication queue with page assignments
|
||||
- `ai_generations` - History of AI-generated content
|
||||
@@ -101,20 +102,21 @@ Core Tables:
|
||||
|
||||
### Media Processing Pipeline
|
||||
|
||||
**Image Processing Library**: Sharp for high-performance image manipulation
|
||||
**Cloud Storage**: Cloudinary for image/video storage and transformation
|
||||
|
||||
**Format Optimization Strategy**:
|
||||
- Facebook Feed: 1200x630px (landscape format)
|
||||
- Instagram Feed: 1080x1080px (square format)
|
||||
- Instagram Story: 1080x1920px (vertical format)
|
||||
|
||||
**Storage**: Local filesystem storage in `/uploads` directory with unique UUID-based filenames
|
||||
**Storage**: Cloud-based storage via Cloudinary CDN with automatic transformations
|
||||
|
||||
**Processing Flow**:
|
||||
1. Upload original file to memory buffer
|
||||
2. Generate three optimized versions for different platforms
|
||||
3. Store all versions with references in database
|
||||
4. Serve via `/uploads` static route
|
||||
1. Upload original file to memory buffer via Multer
|
||||
2. Upload to Cloudinary with unique public_id
|
||||
3. Cloudinary automatically generates transformation URLs for all platform formats
|
||||
4. Store Cloudinary public_id and transformation URLs in database
|
||||
5. Serve optimized images via Cloudinary CDN
|
||||
|
||||
## External Dependencies
|
||||
|
||||
@@ -126,6 +128,16 @@ Core Tables:
|
||||
- Configuration: Requires `OPENROUTER_API_KEY` environment variable
|
||||
- Temperature: 0.8 for creative variation
|
||||
|
||||
### Cloud Storage
|
||||
|
||||
**Cloudinary**:
|
||||
- Purpose: Cloud-based image and video storage with automatic transformations
|
||||
- Configuration: User-configurable via Settings page (cloud name, API key, API secret)
|
||||
- Storage: Images stored in Cloudinary cloud with unique public IDs
|
||||
- Transformations: Automatic generation of platform-specific formats (Facebook Feed, Instagram Feed, Instagram Story)
|
||||
- CDN: Global content delivery network for fast image loading
|
||||
- Status: Fully integrated for media uploads
|
||||
|
||||
### Social Media APIs
|
||||
|
||||
**Facebook Graph API**:
|
||||
|
||||
+104
-16
@@ -3,16 +3,12 @@ import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import multer from "multer";
|
||||
import { openRouterService } from "./services/openrouter";
|
||||
import { imageProcessor } from "./services/imageProcessor";
|
||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema } from "@shared/schema";
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { cloudinaryService } from "./services/cloudinary";
|
||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema } from "@shared/schema";
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Serve uploaded files
|
||||
app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
|
||||
|
||||
// Stats endpoint
|
||||
app.get("/api/stats", async (req, res) => {
|
||||
@@ -69,21 +65,32 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
|
||||
// Check if Cloudinary is configured
|
||||
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
||||
|
||||
const originalUrl = await imageProcessor.saveOriginalFile(req.file.buffer, req.file.originalname);
|
||||
|
||||
let processedUrls = null;
|
||||
if (req.file.mimetype.startsWith("image/")) {
|
||||
processedUrls = await imageProcessor.processImage(req.file.buffer, req.file.originalname);
|
||||
if (!cloudinaryConfig) {
|
||||
return res.status(400).json({
|
||||
error: "Cloudinary not configured. Please configure Cloudinary in Settings first."
|
||||
});
|
||||
}
|
||||
|
||||
// Upload to Cloudinary
|
||||
const uploadResult = await cloudinaryService.uploadMedia(
|
||||
req.file.buffer,
|
||||
req.file.originalname,
|
||||
userId,
|
||||
req.file.mimetype
|
||||
);
|
||||
|
||||
const mediaItem = await storage.createMedia({
|
||||
userId,
|
||||
type: req.file.mimetype.startsWith("video/") ? "video" : "image",
|
||||
originalUrl,
|
||||
facebookFeedUrl: processedUrls?.facebookFeed || null,
|
||||
instagramFeedUrl: processedUrls?.instagramFeed || null,
|
||||
instagramStoryUrl: processedUrls?.instagramStory || null,
|
||||
cloudinaryPublicId: uploadResult.publicId,
|
||||
originalUrl: uploadResult.originalUrl,
|
||||
facebookFeedUrl: uploadResult.facebookFeedUrl,
|
||||
instagramFeedUrl: uploadResult.instagramFeedUrl,
|
||||
instagramStoryUrl: uploadResult.instagramStoryUrl,
|
||||
fileName: req.file.originalname,
|
||||
fileSize: req.file.size,
|
||||
});
|
||||
@@ -91,7 +98,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
res.json(mediaItem);
|
||||
} catch (error) {
|
||||
console.error("Error uploading media:", error);
|
||||
res.status(500).json({ error: "Failed to upload media" });
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to upload media";
|
||||
res.status(500).json({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,6 +115,39 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete media
|
||||
app.delete("/api/media/:id", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const mediaId = req.params.id;
|
||||
|
||||
// Get media to find cloudinary public ID
|
||||
const allMedia = await storage.getMedia(userId);
|
||||
const mediaToDelete = allMedia.find(m => m.id === mediaId);
|
||||
|
||||
if (!mediaToDelete) {
|
||||
return res.status(404).json({ error: "Media not found" });
|
||||
}
|
||||
|
||||
// Delete from Cloudinary
|
||||
if (mediaToDelete.cloudinaryPublicId) {
|
||||
await cloudinaryService.deleteMedia(
|
||||
mediaToDelete.cloudinaryPublicId,
|
||||
userId,
|
||||
mediaToDelete.type
|
||||
);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await storage.deleteMedia(mediaId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting media:", error);
|
||||
res.status(500).json({ error: "Failed to delete media" });
|
||||
}
|
||||
});
|
||||
|
||||
// Posts
|
||||
app.get("/api/posts", async (req, res) => {
|
||||
try {
|
||||
@@ -195,6 +236,53 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Cloudinary Config
|
||||
app.get("/api/cloudinary/config", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const config = await storage.getCloudinaryConfig(userId);
|
||||
|
||||
if (!config) {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
// Don't send the API secret to the client
|
||||
const { apiSecret, ...safeConfig } = config;
|
||||
res.json(safeConfig);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Cloudinary config:", error);
|
||||
res.status(500).json({ error: "Failed to fetch Cloudinary config" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/cloudinary/config", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
|
||||
const configData = insertCloudinaryConfigSchema.parse({
|
||||
...req.body,
|
||||
userId,
|
||||
});
|
||||
|
||||
// Check if config already exists
|
||||
const existingConfig = await storage.getCloudinaryConfig(userId);
|
||||
|
||||
let config;
|
||||
if (existingConfig) {
|
||||
config = await storage.updateCloudinaryConfig(userId, configData);
|
||||
} else {
|
||||
config = await storage.createCloudinaryConfig(configData);
|
||||
}
|
||||
|
||||
// Don't send the API secret back
|
||||
const { apiSecret, ...safeConfig } = config;
|
||||
res.json(safeConfig);
|
||||
} catch (error) {
|
||||
console.error("Error saving Cloudinary config:", error);
|
||||
res.status(500).json({ error: "Failed to save Cloudinary config" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
|
||||
return httpServer;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { v2 as cloudinary } from 'cloudinary';
|
||||
import { storage } from '../storage';
|
||||
|
||||
class CloudinaryService {
|
||||
async uploadMedia(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
userId: string,
|
||||
mimeType: string
|
||||
): Promise<{
|
||||
publicId: string;
|
||||
originalUrl: string;
|
||||
facebookFeedUrl: string | null;
|
||||
instagramFeedUrl: string | null;
|
||||
instagramStoryUrl: string | null;
|
||||
}> {
|
||||
// Get user's Cloudinary config
|
||||
const config = await storage.getCloudinaryConfig(userId);
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Cloudinary configuration not found. Please configure Cloudinary in Settings.');
|
||||
}
|
||||
|
||||
// Configure Cloudinary with user's credentials
|
||||
cloudinary.config({
|
||||
cloud_name: config.cloudName,
|
||||
api_key: config.apiKey,
|
||||
api_secret: config.apiSecret,
|
||||
secure: true,
|
||||
});
|
||||
|
||||
const isVideo = mimeType.startsWith('video/');
|
||||
|
||||
// Upload to Cloudinary
|
||||
const uploadResult = await new Promise<any>((resolve, reject) => {
|
||||
const uploadStream = cloudinary.uploader.upload_stream(
|
||||
{
|
||||
folder: 'social-flow',
|
||||
public_id: `${Date.now()}-${fileName.replace(/\.[^/.]+$/, '')}`,
|
||||
resource_type: isVideo ? 'video' : 'image',
|
||||
},
|
||||
(error, result) => {
|
||||
if (error) reject(error);
|
||||
else resolve(result);
|
||||
}
|
||||
);
|
||||
|
||||
uploadStream.end(buffer);
|
||||
});
|
||||
|
||||
const publicId = uploadResult.public_id;
|
||||
const originalUrl = uploadResult.secure_url;
|
||||
|
||||
// Generate transformation URLs only for images
|
||||
let facebookFeedUrl = null;
|
||||
let instagramFeedUrl = null;
|
||||
let instagramStoryUrl = null;
|
||||
|
||||
if (!isVideo) {
|
||||
facebookFeedUrl = cloudinary.url(publicId, {
|
||||
transformation: [
|
||||
{ width: 1200, height: 630, crop: 'fill', gravity: 'auto' }
|
||||
]
|
||||
});
|
||||
instagramFeedUrl = cloudinary.url(publicId, {
|
||||
transformation: [
|
||||
{ width: 1080, height: 1080, crop: 'fill', gravity: 'auto' }
|
||||
]
|
||||
});
|
||||
instagramStoryUrl = cloudinary.url(publicId, {
|
||||
transformation: [
|
||||
{ width: 1080, height: 1920, crop: 'fill', gravity: 'auto' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
publicId,
|
||||
originalUrl,
|
||||
facebookFeedUrl,
|
||||
instagramFeedUrl,
|
||||
instagramStoryUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteMedia(publicId: string, userId: string, mediaType: 'image' | 'video'): Promise<void> {
|
||||
const config = await storage.getCloudinaryConfig(userId);
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Cloudinary configuration not found');
|
||||
}
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: config.cloudName,
|
||||
api_key: config.apiKey,
|
||||
api_secret: config.apiSecret,
|
||||
secure: true,
|
||||
});
|
||||
|
||||
await cloudinary.uploader.destroy(publicId, {
|
||||
resource_type: mediaType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cloudinaryService = new CloudinaryService();
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
posts,
|
||||
scheduledPosts,
|
||||
aiGenerations,
|
||||
cloudinaryConfig,
|
||||
type User,
|
||||
type InsertUser,
|
||||
type SocialPage,
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
type InsertScheduledPost,
|
||||
type AiGeneration,
|
||||
type InsertAiGeneration,
|
||||
type CloudinaryConfig,
|
||||
type InsertCloudinaryConfig,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, and, gte, lte, desc, isNull } from "drizzle-orm";
|
||||
@@ -58,6 +61,11 @@ export interface IStorage {
|
||||
// AI Generations
|
||||
getAiGenerations(userId: string): Promise<AiGeneration[]>;
|
||||
createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration>;
|
||||
|
||||
// Cloudinary Config
|
||||
getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined>;
|
||||
createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig>;
|
||||
updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
@@ -208,6 +216,25 @@ export class DatabaseStorage implements IStorage {
|
||||
const [newGeneration] = await db.insert(aiGenerations).values(generation).returning();
|
||||
return newGeneration;
|
||||
}
|
||||
|
||||
// Cloudinary Config
|
||||
async getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined> {
|
||||
const [config] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, userId));
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig> {
|
||||
const [newConfig] = await db.insert(cloudinaryConfig).values(config).returning();
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
async updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig> {
|
||||
const [updated] = await db.update(cloudinaryConfig)
|
||||
.set({ ...config, updatedAt: new Date() })
|
||||
.where(eq(cloudinaryConfig.userId, userId))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
+29
-1
@@ -27,10 +27,21 @@ export const socialPages = pgTable("social_pages", {
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const cloudinaryConfig = pgTable("cloudinary_config", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||
cloudName: text("cloud_name").notNull(),
|
||||
apiKey: text("api_key").notNull(),
|
||||
apiSecret: text("api_secret").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const media = pgTable("media", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
type: mediaTypeEnum("type").notNull(),
|
||||
cloudinaryPublicId: text("cloudinary_public_id"),
|
||||
originalUrl: text("original_url").notNull(),
|
||||
facebookFeedUrl: text("facebook_feed_url"),
|
||||
instagramFeedUrl: text("instagram_feed_url"),
|
||||
@@ -79,11 +90,19 @@ export const aiGenerations = pgTable("ai_generations", {
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
export const usersRelations = relations(users, ({ many, one }) => ({
|
||||
socialPages: many(socialPages),
|
||||
media: many(media),
|
||||
posts: many(posts),
|
||||
aiGenerations: many(aiGenerations),
|
||||
cloudinaryConfig: one(cloudinaryConfig),
|
||||
}));
|
||||
|
||||
export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [cloudinaryConfig.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const socialPagesRelations = relations(socialPages, ({ one, many }) => ({
|
||||
@@ -174,6 +193,12 @@ export const insertAiGenerationSchema = createInsertSchema(aiGenerations).omit({
|
||||
createdAt: true,
|
||||
});
|
||||
|
||||
export const insertCloudinaryConfigSchema = createInsertSchema(cloudinaryConfig).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
// Types
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
@@ -192,3 +217,6 @@ export type InsertScheduledPost = z.infer<typeof insertScheduledPostSchema>;
|
||||
|
||||
export type AiGeneration = typeof aiGenerations.$inferSelect;
|
||||
export type InsertAiGeneration = z.infer<typeof insertAiGenerationSchema>;
|
||||
|
||||
export type CloudinaryConfig = typeof cloudinaryConfig.$inferSelect;
|
||||
export type InsertCloudinaryConfig = z.infer<typeof insertCloudinaryConfigSchema>;
|
||||
Reference in new issue
Block a user