mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
5 files changed
+29
-5
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -11,6 +11,8 @@ Preferred communication style: Simple, everyday language.
|
||||
## Recent Changes
|
||||
|
||||
### October 14, 2025
|
||||
- **Publishing Permissions Security Fix**: Fixed critical security vulnerability in POST `/api/posts` endpoint where standard users could publish to ANY page without verification. Added permission check that validates non-admin users can only publish to pages in their `user_page_permissions` list via `getUserAccessiblePages()`. Admins bypass this check and retain full access. Returns 403 Forbidden if user attempts to publish to unauthorized pages. This prevents privilege escalation and ensures proper page-level access control.
|
||||
- **AI Generation Permissions Fix**: Enabled AI text generation for all authenticated users (previously admin-only). Changed `/api/ai/generate` endpoint from `requireAdmin` to `requireAuth` middleware. Added `getAnyOpenrouterConfig()` method in storage layer to retrieve first available OpenRouter config, enabling shared credentials across all users (same pattern as Cloudinary). Updated `OpenRouterService.generatePostText()` to use shared config instead of per-user lookup. Standard users can now generate AI-powered post text using admin's OpenRouter configuration.
|
||||
- **Cloudinary Upload Permissions Fix**: Fixed critical bug preventing non-admin users from uploading media. Problem: System searched for user-specific Cloudinary config (only admins can configure). Solution: Added `getAnyCloudinaryConfig()` method in storage layer to retrieve first available config, enabling shared Cloudinary credentials across all users. Updated `cloudinaryService.uploadMedia()` and `deleteMedia()` to use shared config internally while maintaining userId for media ownership. Upload and image editor endpoints now verify shared config exists before processing. Standard users can now upload media successfully using admin's Cloudinary configuration.
|
||||
- **Android Camera Upload Fix**: Fixed critical Android camera capture bug where uploads failed with 400 error. Android devices send captured files with invalid names (empty, 'blob', etc.) that Multer rejects. Solution: `handleCameraCapture` now detects invalid filenames and creates new File object with generated name `camera-${timestamp}.${extension}` while preserving MIME type. Applied to both new-post and media-upload components. iPhone functionality unaffected.
|
||||
- **Mobile Performance Optimizations**: Implemented comprehensive mobile speed optimizations for "Nouvelle publication" and "Médiathèque" pages. Changes include: (1) Adaptive initial loading - 6 media items on mobile (<768px), 12 on desktop; (2) Optimized thumbnail URLs - replaced `originalUrl` with `facebookFeedUrl` (1080x1080 Cloudinary transformed images) for 70-90% reduction in data transfer while keeping `originalUrl` for zoom/preview quality; (3) Native lazy loading - added `loading="lazy"` attribute to all `<img>` tags for deferred off-screen image loading; (4) Responsive grid layout - 2 columns on mobile, 3 on desktop (sm:grid-cols-3) for better touch targets. Expected impact: Initial load time reduced from 3-5s to <1s on mobile devices.
|
||||
@@ -37,7 +39,7 @@ The platform leverages **Cloudinary** for cloud-based image and video storage an
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
Authentication uses **Passport.js** with local strategy and `bcrypt` for password hashing. User roles (`admin`, `user`) control access, with `admin` having full access and `user` limited to publishing features. The **AI Assistant** is restricted to administrators only. Session management is via `express-session` with HTTP-only cookies and a 7-day duration. Routes are protected on both the backend (middleware `requireAuth`, `requireAdmin`) and frontend (`ProtectedRoute` component).
|
||||
Authentication uses **Passport.js** with local strategy and `bcrypt` for password hashing. User roles (`admin`, `user`) control access, with `admin` having full access to configuration settings (Cloudinary, OpenRouter, user management) and `user` limited to publishing features. **AI text generation is available to all authenticated users** using shared admin OpenRouter configuration. Session management is via `express-session` with HTTP-only cookies and a 7-day duration. Routes are protected on both the backend (middleware `requireAuth`, `requireAdmin`) and frontend (`ProtectedRoute` component).
|
||||
|
||||
### UI/UX Decisions
|
||||
|
||||
|
||||
+17
-1
@@ -435,7 +435,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
});
|
||||
|
||||
// AI text generation
|
||||
app.post("/api/ai/generate", requireAdmin, async (req, res) => {
|
||||
app.post("/api/ai/generate", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const userId = user.id;
|
||||
@@ -793,6 +793,22 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" });
|
||||
}
|
||||
|
||||
// Security: Verify user has access to all specified pages (unless admin)
|
||||
if (user.role !== 'admin' && pageIds && Array.isArray(pageIds) && pageIds.length > 0) {
|
||||
const accessiblePages = await storage.getUserAccessiblePages(userId);
|
||||
const accessiblePageIds = accessiblePages.map(p => p.id);
|
||||
|
||||
const hasAccessToAllPages = pageIds.every(pageId =>
|
||||
accessiblePageIds.includes(pageId)
|
||||
);
|
||||
|
||||
if (!hasAccessToAllPages) {
|
||||
return res.status(403).json({
|
||||
error: "Vous n'avez pas accès à certaines pages sélectionnées"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert scheduledFor string to Date if provided
|
||||
if (postFields.scheduledFor && typeof postFields.scheduledFor === 'string') {
|
||||
postFields.scheduledFor = new Date(postFields.scheduledFor);
|
||||
|
||||
@@ -18,11 +18,11 @@ export class OpenRouterService {
|
||||
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
|
||||
|
||||
async generatePostText(productInfo: ProductInfo, userId: string, modelOverride?: string): Promise<GeneratedText[]> {
|
||||
// Get user's OpenRouter configuration
|
||||
const config = await storage.getOpenrouterConfig(userId);
|
||||
// Get any available OpenRouter configuration (shared across all users)
|
||||
const config = await storage.getAnyOpenrouterConfig();
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Configuration OpenRouter non trouvée. Veuillez configurer OpenRouter dans les paramètres.');
|
||||
throw new Error('Configuration OpenRouter non trouvée. Veuillez demander à un administrateur de configurer OpenRouter dans les Paramètres.');
|
||||
}
|
||||
|
||||
const prompt = this.buildPrompt(productInfo, config.systemPrompt);
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface IStorage {
|
||||
|
||||
// OpenRouter Config
|
||||
getOpenrouterConfig(userId: string): Promise<OpenrouterConfig | undefined>;
|
||||
getAnyOpenrouterConfig(): Promise<OpenrouterConfig | undefined>;
|
||||
createOpenrouterConfig(config: InsertOpenrouterConfig): Promise<OpenrouterConfig>;
|
||||
updateOpenrouterConfig(userId: string, config: Partial<InsertOpenrouterConfig>): Promise<OpenrouterConfig>;
|
||||
|
||||
@@ -336,6 +337,11 @@ export class DatabaseStorage implements IStorage {
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async getAnyOpenrouterConfig(): Promise<OpenrouterConfig | undefined> {
|
||||
const [config] = await db.select().from(openrouterConfig).limit(1);
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async createOpenrouterConfig(config: InsertOpenrouterConfig): Promise<OpenrouterConfig> {
|
||||
const [newConfig] = await db.insert(openrouterConfig).values(config).returning();
|
||||
return newConfig;
|
||||
|
||||
Reference in new issue
Block a user