Communication entre Services
Vue d'ensemble
Les services communiquent via plusieurs protocoles :
| Source | Destination | Protocole | Usage |
|---|---|---|---|
| Frontend | Supabase | REST/WebSocket | CRUD, Auth, Realtime |
| Frontend | Generator | REST | Génération vidéo |
| Generator | Supabase | REST | Accès Storage |
| Supabase | Frontend | Webhook | Notifications |
Frontend ↔ Supabase
Client SDK
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://xxx.supabase.co',
'eyJ...' // Anon key
);
Authentification
// Inscription
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password'
});
// Connexion
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password'
});
// Session
const { data: { session } } = await supabase.auth.getSession();
Requêtes Base de Données
// SELECT
const { data } = await supabase
.from('product')
.select('*, product_items(*)')
.eq('id', productId);
// INSERT
const { data } = await supabase
.from('product')
.insert({ name: 'Mon événement' })
.select();
// UPDATE
const { data } = await supabase
.from('product')
.update({ close: true })
.eq('id', productId);
// DELETE
await supabase
.from('product_items')
.delete()
.eq('id', itemId);
Storage
// Upload
const { data, error } = await supabase.storage
.from('video_dedicace')
.upload(path, file, {
cacheControl: '30',
contentType: 'video/webm'
});
// Download (URL signée)
const { data } = await supabase.storage
.from('video_dedicace')
.createSignedUrl(path, 3600);
// URL publique
const { data } = supabase.storage
.from('video_dedicace')
.getPublicUrl(path);
Edge Functions
// Appel d'une Edge Function
const { data, error } = await supabase.functions.invoke('invite-participate', {
body: {
productId: 123,
emails: ['user@example.com']
}
});
Realtime (optionnel)
// Subscription aux changements
const channel = supabase
.channel('product-items')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'product_items',
filter: `product_id=eq.${productId}`
},
(payload) => {
console.log('Nouveau message:', payload.new);
}
)
.subscribe();
// Désabonnement
channel.unsubscribe();
Frontend ↔ Video Generator
Création de Présentation
// Service Angular
async createPresentation(productId: number): Promise<string> {
const product = await this.getProductWithItems(productId);
const pages = product.items.map(item => ({
id: item.id,
type: item.message_type.toLowerCase(),
content: item.message_type === 'TEXT'
? item.message_media
: await this.getSignedVideoUrl(item.message_media),
author: item.participant_name
}));
const response = await fetch('https://api.momentscollectifs.fr/create_presentation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: product.name,
description: product.description,
pages,
webhook_url: `${environment.apiUrl}/webhook/presentation`
})
});
const { presentationId } = await response.json();
return presentationId;
}
Téléchargement
// Téléchargement direct
downloadPresentation(presentationId: string, format: 'desktop' | 'mobile') {
const filename = `${format}.mp4`;
const url = `https://api.momentscollectifs.fr/download/${presentationId}/${filename}`;
// Téléchargement via lien
const link = document.createElement('a');
link.href = url;
link.download = `presentation_${format}.mp4`;
link.click();
}
Video Generator ↔ Supabase
Accès aux Vidéos
Le générateur utilise une service key pour accéder aux fichiers privés.
// main.js
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY // Clé service (admin)
);
async function downloadVideo(path) {
// Créer URL signée
const { data, error } = await supabase.storage
.from('video_dedicace')
.createSignedUrl(path, 3600);
if (error) throw error;
// Télécharger
const response = await axios.get(data.signedUrl, {
responseType: 'arraybuffer'
});
return response.data;
}
Webhooks
Stripe → Supabase
Stripe envoie des webhooks lors des paiements.
// Edge Function: stripe-webhook
Deno.serve(async (req) => {
const signature = req.headers.get('stripe-signature');
const body = await req.text();
// Vérifier la signature
const event = stripe.webhooks.constructEvent(
body,
signature,
STRIPE_WEBHOOK_SECRET
);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Mettre à jour le produit
await supabaseAdmin
.from('product')
.update({
plan: 'basic',
session_id: session.id
})
.eq('id', session.metadata.product_id);
}
return new Response('OK');
});
Generator → Frontend
Le générateur notifie via webhook quand la vidéo est prête.
// Generator: notification
async function notifyCompletion(webhookUrl, presentationId, status) {
await axios.post(webhookUrl, {
event: 'presentation.completed',
data: {
presentationId,
status,
downloadUrls: {
desktop: `/download/${presentationId}/desktop.mp4`,
mobile: `/download/${presentationId}/mobile.mp4`
}
}
});
}
CORS Configuration
Video Generator
// main.js
const cors = require('cors');
app.use(cors({
origin: [
'https://app.momentscollectifs.fr',
'http://localhost:4200',
'http://localhost:3000'
],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Supabase Edge Functions
// Edge Function
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
Deno.serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
// ... logique
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
});
Gestion des Erreurs
Frontend
// Intercepteur HTTP Angular
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Déconnexion automatique
this.authService.logout();
}
if (error.status >= 500) {
// Notification erreur serveur
this.toast.error('Erreur serveur, veuillez réessayer');
}
return throwError(() => error);
})
);
}
}
Retry avec Backoff
// Service avec retry
async fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
if (response.status >= 500) {
// Attendre avant retry (exponential backoff)
await this.delay(Math.pow(2, i) * 1000);
continue;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
await this.delay(Math.pow(2, i) * 1000);
}
}
}
Timeouts
| Service | Opération | Timeout |
|---|---|---|
| Frontend | API calls | 30s |
| Generator | Video download | 120s |
| Generator | FFmpeg process | 600s |
| Supabase | Edge Functions | 60s |
| Supabase | Storage upload | 120s |