diff --git a/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md b/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md index 279256e7d..f7f5ca458 100644 --- a/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md +++ b/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md @@ -665,8 +665,8 @@ veza/ | 2.1 | ~~Unifier `components/views/` et `features/*/pages/`~~ | **L** | **✅ Fait** — pattern unique `features/*/pages/` | | 2.2 | ~~Nettoyer les 137 fichiers .md à la racine~~ | **M** | **✅ Fait** — archivés dans docs/archive/root-md/ | | 2.3 | ~~Nettoyer les 25 fichiers .json à la racine~~ | **S** | **✅ Fait** — archivés dans docs/archive/root-json/ | -| 2.4 | Supprimer le code mort (Education, Studio, Gamification) | **M** | Backend routes + Frontend components | -| 2.5 | Supprimer `cmd/modern-server/` | **S** | Serveur alternatif inutile | +| 2.4 | ~~Supprimer le code mort (Education, Studio, Gamification)~~ | **M** | **✅ Fait** — routes backend, MSW, refs supprimés | +| 2.5 | ~~Supprimer `cmd/modern-server/`~~ | **S** | **✅ Fait** — bascule vers cmd/api/main.go | | 2.6 | ~~Supprimer `pages/` directory legacy~~ | **M** | **✅ Fait** — migré vers `features/*/pages/` | | 2.7 | Découper `config.go` (1 461 LOC) | **M** | Séparer par domaine (DB, JWT, CORS, etc.) | | 2.8 | Gitignorer les fichiers `.out`, test results, `.turbo/` | **S** | Mettre à jour `.gitignore` | diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx index aa4437877..8b12365f0 100644 --- a/apps/web/src/components/layout/Sidebar.tsx +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -3,7 +3,7 @@ import { useLocation, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag, - GraduationCap, BarChart2, Shield, Box, MessageSquare, + BarChart2, Shield, Box, MessageSquare, Layers, Cpu, Heart, ListMusic, CreditCard, DollarSign, Terminal, ChevronLeft, ChevronRight, } from 'lucide-react'; @@ -38,7 +38,6 @@ const iconMap: Record = { marketplace: , live: , chat: , - education: , sell: , wishlist: , purchases: , diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json index 8943e67eb..b8655b178 100644 --- a/apps/web/src/locales/en.json +++ b/apps/web/src/locales/en.json @@ -47,7 +47,10 @@ "never": "Never", "changeTheme": "Change theme", "userMenu": "User menu", - "notifications": "Notifications" + "notifications": "Notifications", + "retry": "Retry", + "retrying": "Retrying...", + "dismiss": "Dismiss" }, "auth": { "login": { @@ -199,7 +202,9 @@ "empty": { "title": "Your library is empty", "subtitle": "Start by uploading your first files", - "uploadButton": "Upload file" + "description": "Upload your first track or create a playlist to get started.", + "uploadButton": "Upload file", + "uploadTrack": "Upload Track" } }, "profile": { @@ -510,7 +515,6 @@ "marketplace": "Marketplace", "live": "Live Sessions", "chat": "Channels", - "education": "Academy", "sell": "Seller Dashboard", "wishlist": "Wishlist", "purchases": "Purchases", diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index 21a5805e7..1e8fb069c 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -47,7 +47,10 @@ "never": "Jamais", "changeTheme": "Changer le thème", "userMenu": "Menu utilisateur", - "notifications": "Notifications" + "notifications": "Notifications", + "retry": "Réessayer", + "retrying": "Nouvelle tentative...", + "dismiss": "Fermer" }, "auth": { "login": { @@ -199,7 +202,9 @@ "empty": { "title": "Votre bibliothèque est vide", "subtitle": "Commencez par téléverser vos premiers fichiers", - "uploadButton": "Téléverser un fichier" + "description": "Téléversez votre premier titre ou créez une playlist pour commencer.", + "uploadButton": "Téléverser un fichier", + "uploadTrack": "Téléverser un titre" } }, "profile": { @@ -510,7 +515,6 @@ "marketplace": "Marketplace", "live": "Sessions Live", "chat": "Canaux", - "education": "Académie", "sell": "Tableau vendeur", "wishlist": "Liste de souhaits", "purchases": "Achats", diff --git a/apps/web/src/mocks/handlers-misc.ts b/apps/web/src/mocks/handlers-misc.ts new file mode 100644 index 000000000..b8c96b36e --- /dev/null +++ b/apps/web/src/mocks/handlers-misc.ts @@ -0,0 +1,253 @@ +/** + * MSW handlers for search, notifications, users, chat, streaming, products, inventory, live + */ + +import { http, HttpResponse } from 'msw'; + +export const handlersMisc = [ + http.get('*/api/v1/search', () => { + return HttpResponse.json({ + success: true, + data: { + tracks: [ + { id: 'track-1', title: 'Neon Signal', artist: 'Void Producer', cover_art_path: 'https://picsum.photos/200', created_at: '2024-01-15T12:00:00Z' }, + { id: 'track-2', title: 'Deep Frequency', artist: 'Echo Artist', created_at: '2024-01-14T10:00:00Z' }, + ], + artists: [{ id: 'artist-1', username: 'ProducerOne', avatar_url: 'https://i.pravatar.cc/150?u=producer1', followers_count: 120 }], + playlists: [{ id: 'playlist-1', title: 'Curated Mix', description: 'Hand-picked tracks', cover_url: 'https://picsum.photos/300' }], + }, + }); + }), + + http.get('*/api/v1/streaming/bitrate-options', () => { + return HttpResponse.json({ + success: true, + data: [ + { id: 'auto', label: 'Auto', bitrate: 0 }, + { id: 'high', label: 'High (320kbps)', bitrate: 320000 }, + { id: 'medium', label: 'Medium (128kbps)', bitrate: 128000 }, + { id: 'low', label: 'Low (64kbps)', bitrate: 64000 }, + ], + }); + }), + + http.get('*/api/v1/streaming/stats', () => { + return HttpResponse.json({ + success: true, + data: { bufferHealth: 0.8, bitrate: 128000, droppedFrames: 0, latency: 25 }, + }); + }), + + http.get('*/api/v1/products*', () => { + return HttpResponse.json({ + data: [{ id: 'prod-1', title: 'Mock Product', price: 29.99, currency: 'USD', author: 'Mock Author', coverUrl: 'https://picsum.photos/300', rating: 4.5, reviewCount: 10, isHot: true }], + }); + }), + + http.get('*/api/v1/notifications', () => { + return HttpResponse.json({ + success: true, + data: { + notifications: [ + { id: 'notif-1', user_id: 'user-1', type: 'new_message', title: 'New message', content: 'Someone sent you a message', read: false, created_at: '2024-01-04T00:00:00Z', link: '/chat/1' }, + { id: 'notif-2', user_id: 'user-1', type: 'track_uploaded', title: 'New track', content: 'A creator you follow uploaded a track', read: true, created_at: '2024-01-03T12:00:00Z' }, + ], + total: 2, + unread_count: 1, + }, + }); + }), + + http.get('*/api/v1/notifications/unread-count', () => { + return HttpResponse.json({ success: true, data: { count: 3 } }); + }), + + http.post('*/api/v1/notifications/:id/read', () => { + return HttpResponse.json({ success: true, data: { read: true } }); + }), + + http.post('*/api/v1/notifications/read-all', () => { + return HttpResponse.json({ success: true, data: { read: true } }); + }), + + http.get('*/api/v1/users/search', () => { + return HttpResponse.json({ + success: true, + data: { items: [{ id: 'user-1', username: 'StorybookUser', avatar_url: 'https://i.pravatar.cc/150?u=1' }], total: 1 }, + }); + }), + + http.get('*/api/v1/users/settings', () => { + return HttpResponse.json({ + notifications: { email_notifications: true, push_notifications: true, marketing_emails: false, new_follower: true, new_comment: true, new_like: true, playlist_update: true }, + privacy: { allow_search_indexing: true, show_activity: true }, + content: { explicit_content: false, autoplay: true }, + preferences: { language: 'en', timezone: 'UTC', theme: 'dark' }, + }); + }), + + http.get('*/api/v1/users/:userId/settings', () => { + return HttpResponse.json({ + success: true, + data: { + notifications: { email_notifications: true, push_notifications: true, marketing_emails: false, new_follower: true, new_comment: true, new_like: true, playlist_update: true }, + privacy: { allow_search_indexing: true, show_activity: true }, + content: { explicit_content: false, autoplay: true }, + preferences: { language: 'en', timezone: 'UTC', theme: 'dark' }, + }, + }); + }), + + http.put('*/api/v1/users/settings', () => { + return HttpResponse.json({ success: true }); + }), + + http.put('*/api/v1/users/:userId/settings', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('*/api/v1/users/by-username/:username', ({ params }) => { + if (params.username === 'notfound') { + return HttpResponse.json({ message: 'User not found' }, { status: 404 }); + } + return HttpResponse.json({ + profile: { + id: '123', + username: params.username, + first_name: 'Story', + last_name: 'User', + avatar_url: `https://i.pravatar.cc/150?u=${params.username}`, + bio: 'Music enthusiast', + location: 'Paris, France', + birthdate: null, + gender: null, + created_at: '2024-01-01T00:00:00Z', + followers_count: 42, + following_count: 10, + }, + }); + }), + + http.get('*/api/v1/users/:id', ({ params }) => { + return HttpResponse.json({ + success: true, + data: { + id: params.id, + username: 'StorybookUser', + email: 'user@example.com', + avatar_url: `https://i.pravatar.cc/150?u=${params.id}`, + created_at: '2024-01-01T00:00:00Z', + role: 'user', + bio: 'Music enthusiast', + location: 'Paris, France', + website: 'https://example.com', + }, + }); + }), + + http.put('*/api/v1/users/:id', ({ params }) => { + return HttpResponse.json({ + success: true, + data: { id: params.id, username: 'UpdatedUser', email: 'user@example.com' }, + }); + }), + + http.delete('*/api/v1/users/:id', () => { + return HttpResponse.json({ success: true }); + }), + + http.get('*/api/v1/users/:id/completion', () => { + return HttpResponse.json({ + success: true, + data: { percentage: 80, missing: ['bio', 'website'] }, + }); + }), + + http.post('*/api/v1/users/:id/follow', () => HttpResponse.json({ success: true })), + http.delete('*/api/v1/users/:id/follow', () => HttpResponse.json({ success: true })), + http.post('*/api/v1/users/:id/block', () => HttpResponse.json({ success: true })), + http.delete('*/api/v1/users/:id/block', () => HttpResponse.json({ success: true })), + http.post('*/api/v1/users/:id/avatar', () => HttpResponse.json({ avatar_url: 'https://i.pravatar.cc/150?u=new' })), + http.delete('*/api/v1/users/:id/avatar', () => HttpResponse.json({ success: true })), + http.get('*/api/v1/users/:id/likes', () => { + return HttpResponse.json({ success: true, data: { tracks: [], total: 0 } }); + }), + + http.get('*/api/v1/users/:userId/upload-quota', () => { + return HttpResponse.json({ + success: true, + data: { + quota: { + tracks_count: 5, + tracks_limit: 10, + storage_used: 52428800, + storage_limit: 104857600, + }, + }, + }); + }), + + http.post('*/api/v1/chat/token', () => { + return HttpResponse.json({ success: true, data: { token: 'mock-chat-token' } }); + }), + + http.get('*/api/v1/chat/stats', () => { + return HttpResponse.json({ + success: true, + data: { online_users: 42, active_rooms: 5 }, + }); + }), + + http.get('*/api/v1/conversations', () => { + return HttpResponse.json({ + success: true, + data: [{ id: 'conv-1', name: 'General Chat', last_message: 'Hello world', updated_at: new Date().toISOString() }], + }); + }), + + http.get('*/api/v1/conversations/:id', ({ params }) => { + return HttpResponse.json({ + success: true, + data: { id: params.id, messages: [] }, + }); + }), + + http.get('*/api/v1/inventory/gear', () => { + return HttpResponse.json({ + success: true, + data: { + items: [ + { id: '1', name: 'Prophet-6', category: 'Synth', brand: 'Sequential', model: 'Prophet-6 Desktop', serialNumber: 'SQ-P6-99281', purchaseDate: '2023-01-15', purchasePrice: 2499, currency: 'USD', status: 'Active', condition: 'Mint', vendor: 'Sweetwater', image: 'https://picsum.photos/id/100/400/400' }, + { id: '2', name: 'Apollo Twin X', category: 'Interface', brand: 'Universal Audio', model: 'Twin X Duo', serialNumber: 'UA-TWX-2210', purchaseDate: '2022-11-20', purchasePrice: 999, currency: 'USD', status: 'Active', condition: 'Good', vendor: 'Thomann', image: 'https://picsum.photos/id/101/400/400' }, + { id: '3', name: 'SM7B', category: 'Microphone', brand: 'Shure', model: 'SM7B Dynamic', serialNumber: 'SH-SM7-004', purchaseDate: '2021-05-10', purchasePrice: 399, currency: 'USD', status: 'Maintenance', condition: 'Fair', vendor: 'Guitar Center', image: 'https://picsum.photos/id/102/400/400' }, + ], + }, + }); + }), + + http.get('*/api/v1/live/streams', ({ request }) => { + const url = new URL(request.url); + const isLive = url.searchParams.get('is_live'); + const streams = [{ id: '1', title: 'Late Night DnB Production 🎧', streamer: 'Neuro_Glitch', viewers: 1240, thumbnailUrl: 'https://picsum.photos/id/140/800/450', tags: ['Production', 'Ableton', 'DnB'], isLive: true, category: 'Production' }]; + const filtered = isLive === 'true' ? streams.filter((s) => s.isLive) : isLive === 'false' ? streams.filter((s) => !s.isLive) : streams; + return HttpResponse.json({ success: true, data: { streams: filtered } }); + }), + + http.get('*/api/v1/live/streams/:id', ({ params }) => { + return HttpResponse.json({ + success: true, + data: { + stream: { + id: params.id, + title: 'Late Night DnB Production 🎧', + streamer: 'Neuro_Glitch', + viewers: 1240, + thumbnailUrl: 'https://picsum.photos/id/140/800/450', + tags: ['Production', 'Ableton', 'DnB'], + isLive: true, + category: 'Production', + }, + }, + }); + }), +]; diff --git a/dev-environment/config/development.yaml b/dev-environment/config/development.yaml index 0e6a0a524..92806e2cb 100644 --- a/dev-environment/config/development.yaml +++ b/dev-environment/config/development.yaml @@ -15,7 +15,7 @@ services: language: "go" version: "1.23" health_endpoint: "/health" - start_command: "go run cmd/modern-server/main.go" + start_command: "go run cmd/api/main.go" working_directory: "veza-backend-api" dependencies: - "postgresql" diff --git a/dev-environment/scripts/setup-dev-environment.sh b/dev-environment/scripts/setup-dev-environment.sh index 2b58466a1..8e488c325 100755 --- a/dev-environment/scripts/setup-dev-environment.sh +++ b/dev-environment/scripts/setup-dev-environment.sh @@ -310,7 +310,7 @@ echo "✅ Redis démarré" # Démarrer Backend API cd veza-backend-api -go run cmd/modern-server/main.go & +go run cmd/api/main.go & BACKEND_PID=$! echo "✅ Backend API démarré (PID: $BACKEND_PID)" diff --git a/make/dev.mk b/make/dev.mk index f5e1d1c2c..bd03a3475 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -25,7 +25,7 @@ dev-full: check-ports infra-up ## [HIGH] Start Everything inc. Chat + Stream (Ru if command -v air >/dev/null; then \ $(ECHO_CMD) "${GREEN}[Go] Hot Reload Active (Air)${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & \ else \ - $(ECHO_CMD) "${YELLOW}[Go] Standard Run${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & \ + $(ECHO_CMD) "${YELLOW}[Go] Standard Run${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/api/main.go & \ fi; \ if command -v cargo-watch >/dev/null; then \ $(ECHO_CMD) "${GREEN}[Chat] Hot Reload Active${NC}" && cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & \ @@ -40,7 +40,7 @@ dev-full: check-ports infra-up ## [HIGH] Start Everything inc. Chat + Stream (Ru dev-backend: check-ports infra-up ## [MID] Start Backends Only (Hot Reload supported) @$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING BACKEND ONLY${NC}" @(trap 'kill 0' SIGINT; \ - if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & fi; \ + if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/api/main.go & fi; \ if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q & fi; \ if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q & fi; \ wait) @@ -51,7 +51,7 @@ dev-web: check-ports infra-up ## [MID] Start Web app only (assumes backend elsew dev-backend-api: check-ports infra-up ## [MID] Start Go backend only @$(ECHO_CMD) "${GREEN}[Backend API] Starting...${NC}" - @if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air; else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go; fi + @if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air; else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/api/main.go; fi dev-chat-server: check-ports infra-up ## [MID] Start Chat server only @$(ECHO_CMD) "${GREEN}[Chat] Starting...${NC}" @@ -62,14 +62,14 @@ dev-stream-server: check-ports infra-up ## [MID] Start Stream server only @if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo watch -x run -q; else cd $(ROOT)/$(SERVICE_DIR_stream-server) && cargo run -q; fi stop-local-services: ## [LOW] Stop all local processes (air, cargo watch, vite) - @pkill -f "air\|cargo watch\|npm run dev\|go run.*modern-server" 2>/dev/null || true + @pkill -f "air\|cargo watch\|npm run dev\|go run.*cmd/api" 2>/dev/null || true start-local-service: ## [LOW] Start a service locally (usage: make start-local-service SERVICE=backend-api) @if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi @$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}" @case "$(SERVICE)" in \ backend-api) \ - if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/modern-server/main.go & fi ;; \ + if command -v air >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & else cd $(ROOT)/$(SERVICE_DIR_backend-api) && go run cmd/api/main.go & fi ;; \ chat-server) \ if command -v cargo-watch >/dev/null; then cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo watch -x run -q & else cd $(ROOT)/$(SERVICE_DIR_chat-server) && cargo run -q & fi ;; \ stream-server) \ @@ -83,7 +83,7 @@ start-local-service: ## [LOW] Start a service locally (usage: make start-local-s stop-local-service: ## [LOW] Stop a local service (usage: make stop-local-service SERVICE=backend-api) @if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi @case "$(SERVICE)" in \ - backend-api) pkill -f "air\|go run.*modern-server" 2>/dev/null || true ;; \ + backend-api) pkill -f "air\|go run.*cmd/api" 2>/dev/null || true ;; \ chat-server|stream-server) pkill -f "cargo.*$(SERVICE)" 2>/dev/null || true ;; \ web) pkill -f "npm run dev\|vite" 2>/dev/null || true ;; \ *) $(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}" ;; \ diff --git a/scripts/start-backend.sh b/scripts/start-backend.sh index e59cc54dc..1c011eea2 100644 --- a/scripts/start-backend.sh +++ b/scripts/start-backend.sh @@ -56,13 +56,13 @@ else echo -e "${BLUE}📍 API sera accessible sur http://127.0.0.1:8080/api/v1${NC}" echo "" - # Chercher le point d'entrée - if [ -f "cmd/modern-server/main.go" ]; then - exec go run cmd/modern-server/main.go + # Point d'entrée canonique + if [ -f "cmd/api/main.go" ]; then + exec go run cmd/api/main.go elif [ -f "cmd/server/main.go" ]; then exec go run cmd/server/main.go else - echo -e "${RED}❌ Point d'entrée non trouvé (cmd/modern-server/main.go ou cmd/server/main.go)${NC}" + echo -e "${RED}❌ Point d'entrée non trouvé (cmd/api/main.go ou cmd/server/main.go)${NC}" exit 1 fi fi diff --git a/scripts/start_boot.sh b/scripts/start_boot.sh index 9c50fb687..f7aa7184c 100755 --- a/scripts/start_boot.sh +++ b/scripts/start_boot.sh @@ -92,7 +92,7 @@ if lsof -i :8080 -t >/dev/null 2>&1; then fi # Start Backend in background -(cd veza-backend-api && go run cmd/modern-server/main.go) > backend_boot.log 2>&1 & +(cd veza-backend-api && go run cmd/api/main.go) > backend_boot.log 2>&1 & BACKEND_PID=$! echo $BACKEND_PID > backend.pid echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend_boot.log${NC}" diff --git a/scripts/start_minimal.sh b/scripts/start_minimal.sh index 67aa72b9f..058014c34 100755 --- a/scripts/start_minimal.sh +++ b/scripts/start_minimal.sh @@ -31,7 +31,7 @@ cd .. echo -e "${BLUE}⚙️ Starting Backend API (port 8080)...${NC}" # Use standard run, not hot reload for stability in this script, or use air if available? # Let's use simple go run to be robust. -(cd veza-backend-api && go run cmd/modern-server/main.go) > backend.log 2>&1 & +(cd veza-backend-api && go run cmd/api/main.go) > backend.log 2>&1 & BACKEND_PID=$! echo $BACKEND_PID > backend.pid echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend.log${NC}" diff --git a/tmt/tests/backend/memory_budget.sh b/tmt/tests/backend/memory_budget.sh index 2de70e8d5..50cbc5564 100755 --- a/tmt/tests/backend/memory_budget.sh +++ b/tmt/tests/backend/memory_budget.sh @@ -13,7 +13,7 @@ echo "📜 Contract: docs/BUDGETS.md" echo "🧠 Budget: ${MAX_RSS_MB}MB RSS (Idle)" cd "$BACKEND_DIR" -go build -o server_mem_check ./cmd/modern-server/main.go +go build -o server_mem_check ./cmd/api/main.go # Start ./server_mem_check > /dev/null 2>&1 & diff --git a/tmt/tests/backend/startup_time.sh b/tmt/tests/backend/startup_time.sh index 2de10bb70..eb29fb2dd 100755 --- a/tmt/tests/backend/startup_time.sh +++ b/tmt/tests/backend/startup_time.sh @@ -15,7 +15,7 @@ echo "⏱️ Budget: ${MAX_STARTUP_SECONDS}s to reach ready state" cd "$BACKEND_DIR" # Build first to not count compilation time -go build -o server_perf_check ./cmd/modern-server/main.go +go build -o server_perf_check ./cmd/api/main.go # Start server in background, measuring time to first log output or port open # This is a naive check: we expect it to NOT crash and to output something quickly. diff --git a/veza-backend-api/Makefile b/veza-backend-api/Makefile index dbc872af4..ed6a06c5b 100644 --- a/veza-backend-api/Makefile +++ b/veza-backend-api/Makefile @@ -32,12 +32,12 @@ help: ## Affiche cette aide # Développement build: ## Compile l'application @echo "$(GREEN)🔨 Compilation de l'application...$(NC)" - @go build -o bin/$(BINARY_NAME) ./cmd/modern-server/main.go + @go build -o bin/$(BINARY_NAME) ./cmd/api/main.go @echo "$(GREEN)✅ Compilation terminée: bin/$(BINARY_NAME)$(NC)" build-linux: ## Compile l'application pour Linux @echo "$(GREEN)🔨 Compilation pour Linux...$(NC)" - @GOOS=linux GOARCH=amd64 go build -o bin/$(BINARY_NAME)-linux ./cmd/modern-server/main.go + @GOOS=linux GOARCH=amd64 go build -o bin/$(BINARY_NAME)-linux ./cmd/api/main.go @echo "$(GREEN)✅ Compilation Linux terminée: bin/$(BINARY_NAME)-linux$(NC)" # Tests @@ -107,7 +107,7 @@ deps: ## Installe les dépendances install: ## Installe l'application @echo "$(GREEN)📦 Installation de l'application...$(NC)" - @go install ./cmd/modern-server/main.go + @go install ./cmd/api/main.go @echo "$(GREEN)✅ Application installée$(NC)" # Nettoyage @@ -125,7 +125,7 @@ run: build ## Compile et exécute l'application dev: ## Exécute l'application en mode développement @echo "$(GREEN)🚀 Mode développement...$(NC)" - @go run ./cmd/modern-server/main.go + @go run ./cmd/api/main.go # Docker docker-build: ## Construit l'image Docker diff --git a/veza-backend-api/cmd/modern-server/main.go b/veza-backend-api/cmd/modern-server/main.go deleted file mode 100644 index 9090fd3f8..000000000 --- a/veza-backend-api/cmd/modern-server/main.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" - "go.uber.org/zap" - - "veza-backend-api/internal/api" - // TODO: Réactiver internal/api/handlers après stabilisation du noyau - // "veza-backend-api/internal/api/handlers" - "veza-backend-api/internal/config" - // TODO: Réactiver services après stabilisation du noyau - // "veza-backend-api/internal/services" -) - -// @title Veza Backend API -// @version 1.2.0 -// @description Backend API for Veza platform. -// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.veza.app/support -// @contact.email support@veza.app -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost:8080 -// @BasePath /api/v1 -// @securityDefinitions.apikey BearerAuth -// @in header -// @name Authorization -// @description Type "Bearer" followed by a space and JWT token. -func main() { - // Charger les variables d'environnement depuis le fichier .env - if err := godotenv.Load(); err != nil { - log.Printf("⚠️ Impossible de charger le fichier .env: %v", err) - } - - // Configuration du logger - logger, err := zap.NewProduction() - if err != nil { - log.Fatalf("Impossible d'initialiser le logger: %v", err) - } - defer logger.Sync() - - logger.Info("🚀 Démarrage du serveur Veza Backend API (Architecture Moderne)") - - // Charger la configuration - cfg, err := config.NewConfig() - if err != nil { - logger.Fatal("❌ Impossible de charger la configuration", zap.Error(err)) - } - - // Valider la configuration - if err := cfg.Validate(); err != nil { - logger.Fatal("❌ Configuration invalide", zap.Error(err)) - } - - logger.Info("✅ Configuration validée avec succès") - - // La base de données est déjà initialisée dans config.NewConfig() - db := cfg.Database - if db == nil { - logger.Fatal("❌ Base de données non initialisée") - } - defer db.Close() - - // Initialiser la base de données (migrations, etc.) - if err := db.Initialize(); err != nil { - logger.Fatal("❌ Impossible d'initialiser la base de données", zap.Error(err)) - } - - // TODO: Réactiver les services après stabilisation du noyau et alignement des signatures - // Initialiser les services - // authService := services.NewAuthService(db, &cfg.JWT, logger) - // oauthService := services.NewOAuthService(db, cfg, logger) - // chatService := services.NewChatService(db, logger) - // twoFactorService := services.NewTwoFactorService(db, logger) - // rbacService := services.NewRBACService(db, logger) - - // TODO: Réactiver les handlers après stabilisation du noyau et alignement des services - // Initialiser les handlers - // handlers.InitHandlers(authService, logger) - // handlers.InitOAuthHandlers(oauthService, authService, logger) - // handlers.InitChatHandlers(chatService, logger) - // handlers.InitTwoFactorHandlers(twoFactorService, authService, logger) - // handlers.InitRBACHandlers(rbacService, logger) - - // Configuration de Gin selon l'environnement - gin.SetMode(gin.DebugMode) // TODO: Utiliser cfg.LogLevel pour déterminer le mode - - // Créer le router Gin - router := gin.New() - - // Configuration des routes avec la nouvelle architecture - apiRouter := api.NewAPIRouter(db, cfg) // Instantiate APIRouter - if err := apiRouter.Setup(router); err != nil { - logger.Error("Failed to setup API routes", zap.Error(err)) - os.Exit(1) - } - - // Configuration du serveur HTTP - port := fmt.Sprintf("%d", cfg.AppPort) - if port == "0" { - port = "8080" - } - server := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: router, - // TODO: Ajouter ReadTimeout et WriteTimeout si nécessaire - } - - // Canal pour écouter les signaux du système - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - - // Démarrer le serveur dans une goroutine - go func() { - logger.Info("🌐 Serveur HTTP démarré", - zap.String("port", port), - ) - - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Fatal("❌ Erreur du serveur HTTP", zap.Error(err)) - } - }() - - logger.Info("✅ Serveur Veza Backend API prêt à recevoir des requêtes") - logger.Info("📋 Endpoints disponibles:") - logger.Info(" - GET /health - Health check global") - logger.Info(" - POST /api/v1/auth/register - Inscription utilisateur") - logger.Info(" - POST /api/v1/auth/login - Connexion utilisateur") - logger.Info(" - POST /api/v1/auth/refresh - Renouvellement de token") - logger.Info(" - POST /api/v1/auth/logout - Déconnexion utilisateur") - logger.Info(" - GET /api/v1/profile - Profil utilisateur") - logger.Info(" - PUT /api/v1/profile - Mise à jour profil") - logger.Info(" - GET /api/v1/health/detailed - Health check détaillé") - - // Attendre un signal d'arrêt - <-quit - logger.Info("🔄 Arrêt du serveur en cours...") - - // Créer un contexte avec timeout pour l'arrêt gracieux - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // TODO: Utiliser config pour timeout - defer cancel() - - // Arrêt gracieux du serveur - if err := server.Shutdown(ctx); err != nil { - logger.Error("❌ Erreur lors de l'arrêt du serveur", zap.Error(err)) - } else { - logger.Info("✅ Serveur arrêté proprement") - } -} diff --git a/veza-backend-api/internal/api/education/handlers.go b/veza-backend-api/internal/api/education/handlers.go deleted file mode 100644 index fbc842d6a..000000000 --- a/veza-backend-api/internal/api/education/handlers.go +++ /dev/null @@ -1,868 +0,0 @@ -package education - -import ( - "net/http" - "strconv" - "time" - - "veza-backend-api/internal/common" - "veza-backend-api/internal/core/education" - "veza-backend-api/internal/response" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -// Handler gère les requêtes HTTP pour l'éducation -type Handler struct { - courseManager *education.CourseManager - tutorialManager *education.TutorialManager - logger *zap.Logger -} - -// NewHandler crée un nouveau handler d'éducation -func NewHandler(courseManager *education.CourseManager, tutorialManager *education.TutorialManager, logger *zap.Logger) *Handler { - return &Handler{ - courseManager: courseManager, - tutorialManager: tutorialManager, - logger: logger, - } -} - -// Request/Response structures -type CreateCourseRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description" binding:"required"` - Instructor string `json:"instructor" binding:"required"` - Category string `json:"category" binding:"required"` - Level education.CourseLevel `json:"level" binding:"required"` - Duration time.Duration `json:"duration" binding:"required"` - Price float64 `json:"price"` - Language string `json:"language" binding:"required"` - Tags []string `json:"tags"` -} - -type UpdateCourseRequest struct { - Title *string `json:"title"` - Description *string `json:"description"` - Instructor *string `json:"instructor"` - Category *string `json:"category"` - Level *education.CourseLevel `json:"level"` - Duration *time.Duration `json:"duration"` - Price *float64 `json:"price"` - Language *string `json:"language"` - IsPublished *bool `json:"is_published"` - Tags []string `json:"tags"` -} - -type CreateTutorialRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description" binding:"required"` - Author string `json:"author" binding:"required"` - Category string `json:"category" binding:"required"` - VideoURL string `json:"video_url" binding:"required"` - Thumbnail string `json:"thumbnail"` - Duration time.Duration `json:"duration" binding:"required"` - Quality education.VideoQuality `json:"quality" binding:"required"` - Language string `json:"language" binding:"required"` - IsFree bool `json:"is_free"` - Tags []string `json:"tags"` -} - -type UpdateTutorialRequest struct { - Title *string `json:"title"` - Description *string `json:"description"` - Author *string `json:"author"` - Category *string `json:"category"` - VideoURL *string `json:"video_url"` - Thumbnail *string `json:"thumbnail"` - Duration *time.Duration `json:"duration"` - Quality *education.VideoQuality `json:"quality"` - IsPublished *bool `json:"is_published"` - Tags []string `json:"tags"` -} - -type AddLessonRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description" binding:"required"` - Content string `json:"content" binding:"required"` - VideoURL string `json:"video_url"` - Duration time.Duration `json:"duration" binding:"required"` - Order int `json:"order" binding:"required"` - IsFree bool `json:"is_free"` -} - -type AddExerciseRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description" binding:"required"` - Content string `json:"content" binding:"required"` - Solution string `json:"solution" binding:"required"` - Type education.ExerciseType `json:"type" binding:"required"` - Points int `json:"points" binding:"required"` - TimeLimit time.Duration `json:"time_limit"` - IsRequired bool `json:"is_required"` -} - -type UpdateProgressRequest struct { - Progress float64 `json:"progress" binding:"required"` - CompletedLessons []string `json:"completed_lessons"` - CurrentLesson string `json:"current_lesson"` - Score float64 `json:"score"` - TimeSpent time.Duration `json:"time_spent"` -} - -type AddTutorialStepRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description" binding:"required"` - Content string `json:"content" binding:"required"` - Order int `json:"order" binding:"required"` - Timestamp time.Duration `json:"timestamp"` - IsFree bool `json:"is_free"` -} - -type AddTutorialCommentRequest struct { - Content string `json:"content" binding:"required"` - Rating int `json:"rating" binding:"min=1,max=5"` -} - -// COURSES HANDLERS - -// CreateCourse crée un nouveau cours -func (h *Handler) CreateCourse(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - var req CreateCourseRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - course, err := h.courseManager.CreateCourse( - c.Request.Context(), - req.Title, - req.Description, - req.Instructor, - req.Category, - req.Level, - req.Duration, - req.Price, - req.Language, - ) - if err != nil { - h.logger.Error("Échec de création du cours", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de création du cours") - return - } - - response.Success(c, course, "Cours créé avec succès") -} - -// GetCourse récupère un cours par son ID -func (h *Handler) GetCourse(c *gin.Context) { - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - course, err := h.courseManager.GetCourse(c.Request.Context(), courseID) - if err != nil { - h.logger.Error("Échec de récupération du cours", zap.Error(err)) - response.Error(c, http.StatusNotFound, "Cours non trouvé") - return - } - - response.Success(c, course, "Cours récupéré avec succès") -} - -// ListCourses liste tous les cours disponibles -func (h *Handler) ListCourses(c *gin.Context) { - filters := make(map[string]interface{}) - - if category := c.Query("category"); category != "" { - filters["category"] = category - } - if level := c.Query("level"); level != "" { - filters["level"] = education.CourseLevel(level) - } - if isPublished := c.Query("is_published"); isPublished != "" { - if published, err := strconv.ParseBool(isPublished); err == nil { - filters["is_published"] = published - } - } - if isFree := c.Query("is_free"); isFree != "" { - if free, err := strconv.ParseBool(isFree); err == nil { - filters["is_free"] = free - } - } - - courses, err := h.courseManager.ListCourses(c.Request.Context(), filters) - if err != nil { - h.logger.Error("Échec de récupération des cours", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de récupération des cours") - return - } - - response.Success(c, courses, "Cours récupérés avec succès") -} - -// UpdateCourse met à jour un cours -func (h *Handler) UpdateCourse(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - var req UpdateCourseRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - updates := make(map[string]interface{}) - if req.Title != nil { - updates["title"] = *req.Title - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Instructor != nil { - updates["instructor"] = *req.Instructor - } - if req.Category != nil { - updates["category"] = *req.Category - } - if req.Level != nil { - updates["level"] = *req.Level - } - if req.Duration != nil { - updates["duration"] = *req.Duration - } - if req.Price != nil { - updates["price"] = *req.Price - } - if req.Language != nil { - updates["language"] = *req.Language - } - if req.IsPublished != nil { - updates["is_published"] = *req.IsPublished - } - if req.Tags != nil { - updates["tags"] = req.Tags - } - - course, err := h.courseManager.UpdateCourse(c.Request.Context(), courseID, updates) - if err != nil { - h.logger.Error("Échec de mise à jour du cours", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du cours") - return - } - - response.Success(c, course, "Cours mis à jour avec succès") -} - -// DeleteCourse supprime un cours -func (h *Handler) DeleteCourse(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - err := h.courseManager.DeleteCourse(c.Request.Context(), courseID) - if err != nil { - h.logger.Error("Échec de suppression du cours", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de suppression du cours") - return - } - - response.Success(c, nil, "Cours supprimé avec succès") -} - -// AddLesson ajoute une leçon à un cours -func (h *Handler) AddLesson(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - var req AddLessonRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - lesson, err := h.courseManager.AddLesson( - c.Request.Context(), - courseID, - req.Title, - req.Description, - req.Content, - req.VideoURL, - req.Duration, - req.Order, - req.IsFree, - ) - if err != nil { - h.logger.Error("Échec d'ajout de leçon", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout de leçon") - return - } - - response.Success(c, lesson, "Leçon ajoutée avec succès") -} - -// AddExercise ajoute un exercice à un cours -func (h *Handler) AddExercise(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - lessonID := c.Param("lesson_id") - if courseID == "" || lessonID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours et de leçon requis") - return - } - - var req AddExerciseRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - exercise, err := h.courseManager.AddExercise( - c.Request.Context(), - courseID, - lessonID, - req.Title, - req.Description, - req.Content, - req.Solution, - req.Type, - req.Points, - req.TimeLimit, - req.IsRequired, - ) - if err != nil { - h.logger.Error("Échec d'ajout d'exercice", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'exercice") - return - } - - response.Success(c, exercise, "Exercice ajouté avec succès") -} - -// GetUserProgress récupère la progression d'un utilisateur -func (h *Handler) GetUserProgress(c *gin.Context) { - userID, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - progress, err := h.courseManager.GetUserProgress(c.Request.Context(), userID, courseID) - if err != nil { - h.logger.Error("Échec de récupération de la progression", zap.Error(err)) - response.Error(c, http.StatusNotFound, "Progression non trouvée") - return - } - - response.Success(c, progress, "Progression récupérée avec succès") -} - -// UpdateUserProgress met à jour la progression d'un utilisateur -func (h *Handler) UpdateUserProgress(c *gin.Context) { - userID, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - var req UpdateProgressRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - progress, err := h.courseManager.UpdateUserProgress( - c.Request.Context(), - userID, - courseID, - req.Progress, - req.CompletedLessons, - req.CurrentLesson, - req.Score, - req.TimeSpent, - ) - if err != nil { - h.logger.Error("Échec de mise à jour de la progression", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de mise à jour de la progression") - return - } - - response.Success(c, progress, "Progression mise à jour avec succès") -} - -// IssueCertificate émet un certificat -func (h *Handler) IssueCertificate(c *gin.Context) { - userID, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - courseID := c.Param("course_id") - if courseID == "" { - response.Error(c, http.StatusBadRequest, "ID de cours requis") - return - } - - // Récupérer les paramètres de la requête - title := c.Query("title") - description := c.Query("description") - scoreStr := c.Query("score") - maxScoreStr := c.Query("max_score") - - if title == "" || description == "" || scoreStr == "" || maxScoreStr == "" { - response.Error(c, http.StatusBadRequest, "Tous les paramètres sont requis") - return - } - - score, err := strconv.ParseFloat(scoreStr, 64) - if err != nil { - response.Error(c, http.StatusBadRequest, "Score invalide") - return - } - - maxScore, err := strconv.ParseFloat(maxScoreStr, 64) - if err != nil { - response.Error(c, http.StatusBadRequest, "Score maximum invalide") - return - } - - certificate, err := h.courseManager.IssueCertificate( - c.Request.Context(), - courseID, - userID, - title, - description, - score, - maxScore, - ) - if err != nil { - h.logger.Error("Échec d'émission du certificat", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'émission du certificat") - return - } - - response.Success(c, certificate, "Certificat émis avec succès") -} - -// TUTORIALS HANDLERS - -// CreateTutorial crée un nouveau tutoriel -func (h *Handler) CreateTutorial(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - var req CreateTutorialRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - tutorial, err := h.tutorialManager.CreateTutorial( - c.Request.Context(), - req.Title, - req.Description, - req.Author, - req.Category, - req.VideoURL, - req.Thumbnail, - req.Language, - req.Duration, - req.Quality, - req.IsFree, - req.Tags, - ) - if err != nil { - h.logger.Error("Échec de création du tutoriel", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de création du tutoriel") - return - } - - response.Success(c, tutorial, "Tutoriel créé avec succès") -} - -// GetTutorial récupère un tutoriel par son ID -func (h *Handler) GetTutorial(c *gin.Context) { - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - tutorial, err := h.tutorialManager.GetTutorial(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec de récupération du tutoriel", zap.Error(err)) - response.Error(c, http.StatusNotFound, "Tutoriel non trouvé") - return - } - - // Incrémenter les vues - go func() { - if err := h.tutorialManager.IncrementViews(c.Request.Context(), tutorialID); err != nil { - h.logger.Error("Échec d'incrémentation des vues", zap.Error(err)) - } - }() - - response.Success(c, tutorial, "Tutoriel récupéré avec succès") -} - -// ListTutorials liste tous les tutoriels disponibles -func (h *Handler) ListTutorials(c *gin.Context) { - filters := make(map[string]interface{}) - - if category := c.Query("category"); category != "" { - filters["category"] = category - } - if isPublished := c.Query("is_published"); isPublished != "" { - if published, err := strconv.ParseBool(isPublished); err == nil { - filters["is_published"] = published - } - } - if isFree := c.Query("is_free"); isFree != "" { - if free, err := strconv.ParseBool(isFree); err == nil { - filters["is_free"] = free - } - } - if language := c.Query("language"); language != "" { - filters["language"] = language - } - if author := c.Query("author"); author != "" { - filters["author"] = author - } - - tutorials, err := h.tutorialManager.ListTutorials(c.Request.Context(), filters) - if err != nil { - h.logger.Error("Échec de récupération des tutoriels", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de récupération des tutoriels") - return - } - - response.Success(c, tutorials, "Tutoriels récupérés avec succès") -} - -// SearchTutorials recherche des tutoriels -func (h *Handler) SearchTutorials(c *gin.Context) { - query := c.Query("q") - if query == "" { - response.Error(c, http.StatusBadRequest, "Terme de recherche requis") - return - } - - filters := make(map[string]interface{}) - if category := c.Query("category"); category != "" { - filters["category"] = category - } - if isPublished := c.Query("is_published"); isPublished != "" { - if published, err := strconv.ParseBool(isPublished); err == nil { - filters["is_published"] = published - } - } - if isFree := c.Query("is_free"); isFree != "" { - if free, err := strconv.ParseBool(isFree); err == nil { - filters["is_free"] = free - } - } - - tutorials, err := h.tutorialManager.SearchTutorials(c.Request.Context(), query, filters) - if err != nil { - h.logger.Error("Échec de recherche des tutoriels", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de recherche des tutoriels") - return - } - - response.Success(c, tutorials, "Recherche de tutoriels terminée") -} - -// UpdateTutorial met à jour un tutoriel -func (h *Handler) UpdateTutorial(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - var req UpdateTutorialRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - updates := make(map[string]interface{}) - if req.Title != nil { - updates["title"] = *req.Title - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Author != nil { - updates["author"] = *req.Author - } - if req.Category != nil { - updates["category"] = *req.Category - } - if req.VideoURL != nil { - updates["video_url"] = *req.VideoURL - } - if req.Thumbnail != nil { - updates["thumbnail"] = *req.Thumbnail - } - if req.Duration != nil { - updates["duration"] = *req.Duration - } - if req.Quality != nil { - updates["quality"] = *req.Quality - } - if req.IsPublished != nil { - updates["is_published"] = *req.IsPublished - } - if req.Tags != nil { - updates["tags"] = req.Tags - } - - tutorial, err := h.tutorialManager.UpdateTutorial(c.Request.Context(), tutorialID, updates) - if err != nil { - h.logger.Error("Échec de mise à jour du tutoriel", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du tutoriel") - return - } - - response.Success(c, tutorial, "Tutoriel mis à jour avec succès") -} - -// DeleteTutorial supprime un tutoriel -func (h *Handler) DeleteTutorial(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - err := h.tutorialManager.DeleteTutorial(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec de suppression du tutoriel", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de suppression du tutoriel") - return - } - - response.Success(c, nil, "Tutoriel supprimé avec succès") -} - -// AddTutorialStep ajoute une étape à un tutoriel -func (h *Handler) AddTutorialStep(c *gin.Context) { - _, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - var req AddTutorialStepRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - step, err := h.tutorialManager.AddTutorialStep( - c.Request.Context(), - tutorialID, - req.Title, - req.Description, - req.Content, - req.Order, - req.Timestamp, - req.IsFree, - ) - if err != nil { - h.logger.Error("Échec d'ajout d'étape de tutoriel", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'étape de tutoriel") - return - } - - response.Success(c, step, "Étape de tutoriel ajoutée avec succès") -} - -// GetTutorialSteps récupère les étapes d'un tutoriel -func (h *Handler) GetTutorialSteps(c *gin.Context) { - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - steps, err := h.tutorialManager.GetTutorialSteps(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec de récupération des étapes", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de récupération des étapes") - return - } - - response.Success(c, steps, "Étapes récupérées avec succès") -} - -// AddTutorialComment ajoute un commentaire à un tutoriel -func (h *Handler) AddTutorialComment(c *gin.Context) { - userID, exists := common.GetUserIDFromContext(c) - if !exists { - response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") - return - } - - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - var req AddTutorialCommentRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.Error(c, http.StatusBadRequest, "Données de requête invalides") - return - } - - username, _ := common.GetUsernameFromContext(c) - if username == "" { - username = "Utilisateur anonyme" - } - - comment, err := h.tutorialManager.AddTutorialComment( - c.Request.Context(), - tutorialID, - userID.String(), - username, - req.Content, - req.Rating, - ) - if err != nil { - h.logger.Error("Échec d'ajout de commentaire", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout de commentaire") - return - } - - response.Success(c, comment, "Commentaire ajouté avec succès") -} - -// GetTutorialComments récupère les commentaires d'un tutoriel -func (h *Handler) GetTutorialComments(c *gin.Context) { - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - comments, err := h.tutorialManager.GetTutorialComments(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec de récupération des commentaires", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec de récupération des commentaires") - return - } - - response.Success(c, comments, "Commentaires récupérés avec succès") -} - -// LikeTutorial ajoute un like à un tutoriel -func (h *Handler) LikeTutorial(c *gin.Context) { - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - err := h.tutorialManager.LikeTutorial(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec d'ajout de like", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout de like") - return - } - - response.Success(c, nil, "Like ajouté avec succès") -} - -// DislikeTutorial ajoute un dislike à un tutoriel -func (h *Handler) DislikeTutorial(c *gin.Context) { - tutorialID := c.Param("tutorial_id") - if tutorialID == "" { - response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") - return - } - - err := h.tutorialManager.DislikeTutorial(c.Request.Context(), tutorialID) - if err != nil { - h.logger.Error("Échec d'ajout de dislike", zap.Error(err)) - response.Error(c, http.StatusInternalServerError, "Échec d'ajout de dislike") - return - } - - response.Success(c, nil, "Dislike ajouté avec succès") -} diff --git a/veza-backend-api/internal/api/education/routes.go b/veza-backend-api/internal/api/education/routes.go deleted file mode 100644 index e1ac2ab7d..000000000 --- a/veza-backend-api/internal/api/education/routes.go +++ /dev/null @@ -1,54 +0,0 @@ -package education - -import ( - "veza-backend-api/internal/middleware" - - "github.com/gin-gonic/gin" -) - -// SetupRoutes configure les routes d'éducation -func SetupRoutes(router *gin.RouterGroup, handler *Handler, jwtSecret string, authMiddleware *middleware.AuthMiddleware) { // Added authMiddleware parameter - // Groupe de routes pour l'éducation - edu := router.Group("/education") - { - // Routes des cours - courses := edu.Group("/courses") - courses.Use(authMiddleware.RequireAuth()) // Changed to authMiddleware.RequireAuth() - { - courses.POST("/create", handler.CreateCourse) - courses.GET("/list", handler.ListCourses) - courses.GET("/:course_id", handler.GetCourse) - courses.PUT("/:course_id", handler.UpdateCourse) - courses.DELETE("/:course_id", handler.DeleteCourse) - courses.POST("/:course_id/lessons", handler.AddLesson) - courses.POST("/:course_id/lessons/:lesson_id/exercises", handler.AddExercise) - courses.GET("/:course_id/progress", handler.GetUserProgress) - courses.PUT("/:course_id/progress", handler.UpdateUserProgress) - courses.POST("/:course_id/certificate", handler.IssueCertificate) - } - - // Routes des tutoriels - tutorials := edu.Group("/tutorials") - { - // Routes publiques (sans authentification) - tutorials.GET("/list", handler.ListTutorials) - tutorials.GET("/search", handler.SearchTutorials) - tutorials.GET("/:tutorial_id", handler.GetTutorial) - tutorials.GET("/:tutorial_id/steps", handler.GetTutorialSteps) - tutorials.GET("/:tutorial_id/comments", handler.GetTutorialComments) - tutorials.POST("/:tutorial_id/like", handler.LikeTutorial) - tutorials.POST("/:tutorial_id/dislike", handler.DislikeTutorial) - - // Routes protégées (avec authentification) - protected := tutorials.Group("") - protected.Use(authMiddleware.RequireAuth()) // Changed to authMiddleware.RequireAuth() - { - protected.POST("/create", handler.CreateTutorial) - protected.PUT("/:tutorial_id", handler.UpdateTutorial) - protected.DELETE("/:tutorial_id", handler.DeleteTutorial) - protected.POST("/:tutorial_id/steps", handler.AddTutorialStep) - protected.POST("/:tutorial_id/comments", handler.AddTutorialComment) - } - } - } -} diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 22405a410..acf4261fb 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -290,9 +290,6 @@ func (r *APIRouter) Setup(router *gin.Engine) error { // Social Routes r.setupSocialRoutes(v1) - // Education Routes (courses, tutorials) - r.setupEducationRoutes(v1) - // Inventory / Gear Routes r.setupGearRoutes(v1) diff --git a/veza-backend-api/internal/api/routes_education.go b/veza-backend-api/internal/api/routes_education.go deleted file mode 100644 index 1a3ac9028..000000000 --- a/veza-backend-api/internal/api/routes_education.go +++ /dev/null @@ -1,19 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" - - eduApi "veza-backend-api/internal/api/education" - eduCore "veza-backend-api/internal/core/education" -) - -// setupEducationRoutes configure les routes d'éducation (cours, tutoriels) -func (r *APIRouter) setupEducationRoutes(router *gin.RouterGroup) { - if r.config == nil || r.config.AuthMiddleware == nil { - return - } - courseManager := eduCore.NewCourseManager(r.logger) - tutorialManager := eduCore.NewTutorialManager(r.logger) - eduHandler := eduApi.NewHandler(courseManager, tutorialManager, r.logger) - eduApi.SetupRoutes(router, eduHandler, r.config.JWTSecret, r.config.AuthMiddleware) -} diff --git a/veza-backend-api/internal/config/config.go b/veza-backend-api/internal/config/config.go index 9d3e59990..8bf99ce20 100644 --- a/veza-backend-api/internal/config/config.go +++ b/veza-backend-api/internal/config/config.go @@ -837,7 +837,7 @@ func (c *Config) initMiddlewares() error { // Les handlers doivent être créés dans main.go ou dans les routes selon les besoins. // // SetupRoutes a été supprimé pour casser le cycle d'import config <-> api. -// Utiliser directement api.SetupRoutes() dans cmd/modern-server/main.go +// Utiliser directement api.SetupRoutes() dans cmd/api/main.go // SetupMiddleware configure les middlewares globaux // DÉPRÉCIÉ : Cette méthode est conservée pour compatibilité mais ne fait plus rien diff --git a/veza-backend-api/internal/core/education/course.go b/veza-backend-api/internal/core/education/course.go deleted file mode 100644 index 1fa4b40a4..000000000 --- a/veza-backend-api/internal/core/education/course.go +++ /dev/null @@ -1,452 +0,0 @@ -package education - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/google/uuid" - "go.uber.org/zap" -) - -// Course représente un cours de formation -type Course struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Instructor string `json:"instructor"` - Category string `json:"category"` - Level CourseLevel `json:"level"` - Duration time.Duration `json:"duration"` - Price float64 `json:"price"` - Currency string `json:"currency"` - Language string `json:"language"` - Thumbnail string `json:"thumbnail"` - VideoURL string `json:"video_url"` - Lessons []*Lesson `json:"lessons"` - Exercises []*Exercise `json:"exercises"` - Certificates []*Certificate `json:"certificates"` - Tags []string `json:"tags"` - IsPublished bool `json:"is_published"` - IsFree bool `json:"is_free"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - mu sync.RWMutex -} - -// CourseLevel définit le niveau de difficulté d'un cours -type CourseLevel string - -const ( - CourseLevelBeginner CourseLevel = "beginner" - CourseLevelIntermediate CourseLevel = "intermediate" - CourseLevelAdvanced CourseLevel = "advanced" - CourseLevelExpert CourseLevel = "expert" -) - -// Lesson représente une leçon dans un cours -type Lesson struct { - ID string `json:"id"` - CourseID string `json:"course_id"` - Title string `json:"title"` - Description string `json:"description"` - Content string `json:"content"` - VideoURL string `json:"video_url"` - Duration time.Duration `json:"duration"` - Order int `json:"order"` - IsFree bool `json:"is_free"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// Exercise représente un exercice pratique -type Exercise struct { - ID string `json:"id"` - CourseID string `json:"course_id"` - LessonID string `json:"lesson_id"` - Title string `json:"title"` - Description string `json:"description"` - Type ExerciseType `json:"type"` - Content string `json:"content"` - Solution string `json:"solution"` - Points int `json:"points"` - TimeLimit time.Duration `json:"time_limit"` - IsRequired bool `json:"is_required"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ExerciseType définit le type d'exercice -type ExerciseType string - -const ( - ExerciseTypeQuiz ExerciseType = "quiz" - ExerciseTypeProject ExerciseType = "project" - ExerciseTypeAudio ExerciseType = "audio" - ExerciseTypeCode ExerciseType = "code" - ExerciseTypeEssay ExerciseType = "essay" -) - -// Certificate représente un certificat de formation -type Certificate struct { - ID string `json:"id"` - CourseID string `json:"course_id"` - UserID uuid.UUID `json:"user_id"` - Title string `json:"title"` - Description string `json:"description"` - Score float64 `json:"score"` - MaxScore float64 `json:"max_score"` - IsPassed bool `json:"is_passed"` - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` -} - -// CourseProgress représente la progression d'un utilisateur dans un cours -type CourseProgress struct { - ID string `json:"id"` - UserID uuid.UUID `json:"user_id"` - CourseID string `json:"course_id"` - Progress float64 `json:"progress"` // 0.0 à 1.0 - CompletedLessons []string `json:"completed_lessons"` - CurrentLesson string `json:"current_lesson"` - Score float64 `json:"score"` - TimeSpent time.Duration `json:"time_spent"` - LastAccessed time.Time `json:"last_accessed"` - IsCompleted bool `json:"is_completed"` - CompletedAt time.Time `json:"completed_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// CourseManager gère les cours et formations -type CourseManager struct { - courses map[string]*Course - progress map[string]*CourseProgress - logger *zap.Logger - mu sync.RWMutex -} - -// NewCourseManager crée un nouveau gestionnaire de cours -func NewCourseManager(logger *zap.Logger) *CourseManager { - return &CourseManager{ - courses: make(map[string]*Course), - progress: make(map[string]*CourseProgress), - logger: logger, - } -} - -// CreateCourse crée un nouveau cours -func (cm *CourseManager) CreateCourse(ctx context.Context, title, description, instructor, category string, level CourseLevel, duration time.Duration, price float64, language string) (*Course, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - courseID := uuid.New().String() - - course := &Course{ - ID: courseID, - Title: title, - Description: description, - Instructor: instructor, - Category: category, - Level: level, - Duration: duration, - Price: price, - Currency: "EUR", - Language: language, - Lessons: []*Lesson{}, - Exercises: []*Exercise{}, - Certificates: []*Certificate{}, - Tags: []string{}, - IsPublished: false, - IsFree: price == 0, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - cm.courses[courseID] = course - - cm.logger.Info("Cours créé", - zap.String("course_id", courseID), - zap.String("title", title), - zap.String("instructor", instructor)) - - return course, nil -} - -// GetCourse récupère un cours par son ID -func (cm *CourseManager) GetCourse(ctx context.Context, courseID string) (*Course, error) { - cm.mu.RLock() - defer cm.mu.RUnlock() - - course, exists := cm.courses[courseID] - if !exists { - return nil, fmt.Errorf("cours non trouvé: %s", courseID) - } - - return course, nil -} - -// ListCourses liste tous les cours disponibles -func (cm *CourseManager) ListCourses(ctx context.Context, filters map[string]interface{}) ([]*Course, error) { - cm.mu.RLock() - defer cm.mu.RUnlock() - - var courses []*Course - for _, course := range cm.courses { - // Appliquer les filtres si fournis - if filters != nil { - if category, ok := filters["category"].(string); ok && course.Category != category { - continue - } - if level, ok := filters["level"].(CourseLevel); ok && course.Level != level { - continue - } - if isPublished, ok := filters["is_published"].(bool); ok && course.IsPublished != isPublished { - continue - } - if isFree, ok := filters["is_free"].(bool); ok && course.IsFree != isFree { - continue - } - } - courses = append(courses, course) - } - - return courses, nil -} - -// UpdateCourse met à jour un cours -func (cm *CourseManager) UpdateCourse(ctx context.Context, courseID string, updates map[string]interface{}) (*Course, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - course, exists := cm.courses[courseID] - if !exists { - return nil, fmt.Errorf("cours non trouvé: %s", courseID) - } - - // Appliquer les mises à jour - if title, ok := updates["title"].(string); ok { - course.Title = title - } - if description, ok := updates["description"].(string); ok { - course.Description = description - } - if instructor, ok := updates["instructor"].(string); ok { - course.Instructor = instructor - } - if category, ok := updates["category"].(string); ok { - course.Category = category - } - if level, ok := updates["level"].(CourseLevel); ok { - course.Level = level - } - if duration, ok := updates["duration"].(time.Duration); ok { - course.Duration = duration - } - if price, ok := updates["price"].(float64); ok { - course.Price = price - course.IsFree = price == 0 - } - if isPublished, ok := updates["is_published"].(bool); ok { - course.IsPublished = isPublished - } - - course.UpdatedAt = time.Now() - - cm.logger.Info("Cours mis à jour", - zap.String("course_id", courseID), - zap.String("title", course.Title)) - - return course, nil -} - -// DeleteCourse supprime un cours -func (cm *CourseManager) DeleteCourse(ctx context.Context, courseID string) error { - cm.mu.Lock() - defer cm.mu.Unlock() - - if _, exists := cm.courses[courseID]; !exists { - return fmt.Errorf("cours non trouvé: %s", courseID) - } - - delete(cm.courses, courseID) - - cm.logger.Info("Cours supprimé", - zap.String("course_id", courseID)) - - return nil -} - -// AddLesson ajoute une leçon à un cours -func (cm *CourseManager) AddLesson(ctx context.Context, courseID, title, description, content, videoURL string, duration time.Duration, order int, isFree bool) (*Lesson, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - course, exists := cm.courses[courseID] - if !exists { - return nil, fmt.Errorf("cours non trouvé: %s", courseID) - } - - lessonID := uuid.New().String() - lesson := &Lesson{ - ID: lessonID, - CourseID: courseID, - Title: title, - Description: description, - Content: content, - VideoURL: videoURL, - Duration: duration, - Order: order, - IsFree: isFree, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - course.Lessons = append(course.Lessons, lesson) - course.UpdatedAt = time.Now() - - cm.logger.Info("Leçon ajoutée", - zap.String("course_id", courseID), - zap.String("lesson_id", lessonID), - zap.String("title", title)) - - return lesson, nil -} - -// AddExercise ajoute un exercice à un cours -func (cm *CourseManager) AddExercise(ctx context.Context, courseID, lessonID, title, description, content, solution string, exerciseType ExerciseType, points int, timeLimit time.Duration, isRequired bool) (*Exercise, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - course, exists := cm.courses[courseID] - if !exists { - return nil, fmt.Errorf("cours non trouvé: %s", courseID) - } - - exerciseID := uuid.New().String() - exercise := &Exercise{ - ID: exerciseID, - CourseID: courseID, - LessonID: lessonID, - Title: title, - Description: description, - Type: exerciseType, - Content: content, - Solution: solution, - Points: points, - TimeLimit: timeLimit, - IsRequired: isRequired, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - course.Exercises = append(course.Exercises, exercise) - course.UpdatedAt = time.Now() - - cm.logger.Info("Exercice ajouté", - zap.String("course_id", courseID), - zap.String("exercise_id", exerciseID), - zap.String("title", title)) - - return exercise, nil -} - -// GetUserProgress récupère la progression d'un utilisateur dans un cours -func (cm *CourseManager) GetUserProgress(ctx context.Context, userID uuid.UUID, courseID string) (*CourseProgress, error) { - cm.mu.RLock() - defer cm.mu.RUnlock() - - progressKey := fmt.Sprintf("%s_%s", userID.String(), courseID) - progress, exists := cm.progress[progressKey] - if !exists { - return nil, fmt.Errorf("progression non trouvée pour l'utilisateur %s dans le cours %s", userID, courseID) - } - - return progress, nil -} - -// UpdateUserProgress met à jour la progression d'un utilisateur -func (cm *CourseManager) UpdateUserProgress(ctx context.Context, userID uuid.UUID, courseID string, progress float64, completedLessons []string, currentLesson string, score float64, timeSpent time.Duration) (*CourseProgress, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - progressKey := fmt.Sprintf("%s_%s", userID.String(), courseID) - - userProgress, exists := cm.progress[progressKey] - if !exists { - userProgress = &CourseProgress{ - ID: uuid.New().String(), - UserID: userID, - CourseID: courseID, - Progress: progress, - CompletedLessons: completedLessons, - CurrentLesson: currentLesson, - Score: score, - TimeSpent: timeSpent, - LastAccessed: time.Now(), - IsCompleted: progress >= 1.0, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - cm.progress[progressKey] = userProgress - } else { - userProgress.Progress = progress - userProgress.CompletedLessons = completedLessons - userProgress.CurrentLesson = currentLesson - userProgress.Score = score - userProgress.TimeSpent = timeSpent - userProgress.LastAccessed = time.Now() - userProgress.IsCompleted = progress >= 1.0 - userProgress.UpdatedAt = time.Now() - - if userProgress.IsCompleted && userProgress.CompletedAt.IsZero() { - userProgress.CompletedAt = time.Now() - } - } - - cm.logger.Info("Progression utilisateur mise à jour", - zap.String("user_id", userID.String()), - zap.String("course_id", courseID), - zap.Float64("progress", progress)) - - return userProgress, nil -} - -// IssueCertificate émet un certificat pour un utilisateur -func (cm *CourseManager) IssueCertificate(ctx context.Context, courseID string, userID uuid.UUID, title, description string, score, maxScore float64) (*Certificate, error) { - cm.mu.Lock() - defer cm.mu.Unlock() - - certificateID := uuid.New().String() - isPassed := score >= maxScore*0.7 // 70% pour réussir - - certificate := &Certificate{ - ID: certificateID, - CourseID: courseID, - UserID: userID, - Title: title, - Description: description, - Score: score, - MaxScore: maxScore, - IsPassed: isPassed, - IssuedAt: time.Now(), - ExpiresAt: time.Now().AddDate(2, 0, 0), // Valide 2 ans - CreatedAt: time.Now(), - } - - // Ajouter le certificat au cours - if course, exists := cm.courses[courseID]; exists { - course.Certificates = append(course.Certificates, certificate) - course.UpdatedAt = time.Now() - } - - cm.logger.Info("Certificat émis", - zap.String("certificate_id", certificateID), - zap.String("course_id", courseID), - zap.String("user_id", userID.String()), - zap.Bool("is_passed", isPassed)) - - return certificate, nil -} diff --git a/veza-backend-api/internal/core/education/tutorial.go b/veza-backend-api/internal/core/education/tutorial.go deleted file mode 100644 index 6e1a456a4..000000000 --- a/veza-backend-api/internal/core/education/tutorial.go +++ /dev/null @@ -1,479 +0,0 @@ -package education - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/google/uuid" - "go.uber.org/zap" -) - -// Tutorial représente un tutoriel vidéo -type Tutorial struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Author string `json:"author"` - Category string `json:"category"` - Tags []string `json:"tags"` - VideoURL string `json:"video_url"` - Thumbnail string `json:"thumbnail"` - Duration time.Duration `json:"duration"` - Quality VideoQuality `json:"quality"` - Language string `json:"language"` - IsFree bool `json:"is_free"` - IsPublished bool `json:"is_published"` - Views int64 `json:"views"` - Likes int64 `json:"likes"` - Dislikes int64 `json:"dislikes"` - Rating float64 `json:"rating"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - mu sync.RWMutex -} - -// VideoQuality définit la qualité de la vidéo -type VideoQuality string - -const ( - VideoQualityHD VideoQuality = "hd" - VideoQuality4K VideoQuality = "4k" - VideoQuality8K VideoQuality = "8k" -) - -// TutorialStep représente une étape dans un tutoriel -type TutorialStep struct { - ID string `json:"id"` - TutorialID string `json:"tutorial_id"` - Title string `json:"title"` - Description string `json:"description"` - Content string `json:"content"` - Order int `json:"order"` - Timestamp time.Duration `json:"timestamp"` - IsFree bool `json:"is_free"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TutorialComment représente un commentaire sur un tutoriel -type TutorialComment struct { - ID string `json:"id"` - TutorialID string `json:"tutorial_id"` - UserID string `json:"user_id"` - Username string `json:"username"` - Content string `json:"content"` - Rating int `json:"rating"` // 1-5 étoiles - IsHelpful bool `json:"is_helpful"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TutorialManager gère les tutoriels vidéo -type TutorialManager struct { - tutorials map[string]*Tutorial - steps map[string][]*TutorialStep - comments map[string][]*TutorialComment - logger *zap.Logger - mu sync.RWMutex -} - -// NewTutorialManager crée un nouveau gestionnaire de tutoriels -func NewTutorialManager(logger *zap.Logger) *TutorialManager { - return &TutorialManager{ - tutorials: make(map[string]*Tutorial), - steps: make(map[string][]*TutorialStep), - comments: make(map[string][]*TutorialComment), - logger: logger, - } -} - -// CreateTutorial crée un nouveau tutoriel -func (tm *TutorialManager) CreateTutorial(ctx context.Context, title, description, author, category, videoURL, thumbnail, language string, duration time.Duration, quality VideoQuality, isFree bool, tags []string) (*Tutorial, error) { - tm.mu.Lock() - defer tm.mu.Unlock() - - tutorialID := uuid.New().String() - - tutorial := &Tutorial{ - ID: tutorialID, - Title: title, - Description: description, - Author: author, - Category: category, - Tags: tags, - VideoURL: videoURL, - Thumbnail: thumbnail, - Duration: duration, - Quality: quality, - Language: language, - IsFree: isFree, - IsPublished: false, - Views: 0, - Likes: 0, - Dislikes: 0, - Rating: 0.0, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - tm.tutorials[tutorialID] = tutorial - - tm.logger.Info("Tutoriel créé", - zap.String("tutorial_id", tutorialID), - zap.String("title", title), - zap.String("author", author)) - - return tutorial, nil -} - -// GetTutorial récupère un tutoriel par son ID -func (tm *TutorialManager) GetTutorial(ctx context.Context, tutorialID string) (*Tutorial, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - tutorial, exists := tm.tutorials[tutorialID] - if !exists { - return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - return tutorial, nil -} - -// ListTutorials liste tous les tutoriels disponibles -func (tm *TutorialManager) ListTutorials(ctx context.Context, filters map[string]interface{}) ([]*Tutorial, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - var tutorials []*Tutorial - for _, tutorial := range tm.tutorials { - // Appliquer les filtres si fournis - if filters != nil { - if category, ok := filters["category"].(string); ok && tutorial.Category != category { - continue - } - if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished { - continue - } - if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree { - continue - } - if language, ok := filters["language"].(string); ok && tutorial.Language != language { - continue - } - if author, ok := filters["author"].(string); ok && tutorial.Author != author { - continue - } - } - tutorials = append(tutorials, tutorial) - } - - return tutorials, nil -} - -// UpdateTutorial met à jour un tutoriel -func (tm *TutorialManager) UpdateTutorial(ctx context.Context, tutorialID string, updates map[string]interface{}) (*Tutorial, error) { - tm.mu.Lock() - defer tm.mu.Unlock() - - tutorial, exists := tm.tutorials[tutorialID] - if !exists { - return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - // Appliquer les mises à jour - if title, ok := updates["title"].(string); ok { - tutorial.Title = title - } - if description, ok := updates["description"].(string); ok { - tutorial.Description = description - } - if author, ok := updates["author"].(string); ok { - tutorial.Author = author - } - if category, ok := updates["category"].(string); ok { - tutorial.Category = category - } - if videoURL, ok := updates["video_url"].(string); ok { - tutorial.VideoURL = videoURL - } - if thumbnail, ok := updates["thumbnail"].(string); ok { - tutorial.Thumbnail = thumbnail - } - if duration, ok := updates["duration"].(time.Duration); ok { - tutorial.Duration = duration - } - if quality, ok := updates["quality"].(VideoQuality); ok { - tutorial.Quality = quality - } - if isPublished, ok := updates["is_published"].(bool); ok { - tutorial.IsPublished = isPublished - } - if tags, ok := updates["tags"].([]string); ok { - tutorial.Tags = tags - } - - tutorial.UpdatedAt = time.Now() - - tm.logger.Info("Tutoriel mis à jour", - zap.String("tutorial_id", tutorialID), - zap.String("title", tutorial.Title)) - - return tutorial, nil -} - -// DeleteTutorial supprime un tutoriel -func (tm *TutorialManager) DeleteTutorial(ctx context.Context, tutorialID string) error { - tm.mu.Lock() - defer tm.mu.Unlock() - - if _, exists := tm.tutorials[tutorialID]; !exists { - return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - delete(tm.tutorials, tutorialID) - delete(tm.steps, tutorialID) - delete(tm.comments, tutorialID) - - tm.logger.Info("Tutoriel supprimé", - zap.String("tutorial_id", tutorialID)) - - return nil -} - -// AddTutorialStep ajoute une étape à un tutoriel -func (tm *TutorialManager) AddTutorialStep(ctx context.Context, tutorialID, title, description, content string, order int, timestamp time.Duration, isFree bool) (*TutorialStep, error) { - tm.mu.Lock() - defer tm.mu.Unlock() - - stepID := uuid.New().String() - step := &TutorialStep{ - ID: stepID, - TutorialID: tutorialID, - Title: title, - Description: description, - Content: content, - Order: order, - Timestamp: timestamp, - IsFree: isFree, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - tm.steps[tutorialID] = append(tm.steps[tutorialID], step) - - tm.logger.Info("Étape de tutoriel ajoutée", - zap.String("tutorial_id", tutorialID), - zap.String("step_id", stepID), - zap.String("title", title)) - - return step, nil -} - -// GetTutorialSteps récupère toutes les étapes d'un tutoriel -func (tm *TutorialManager) GetTutorialSteps(ctx context.Context, tutorialID string) ([]*TutorialStep, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - steps, exists := tm.steps[tutorialID] - if !exists { - return []*TutorialStep{}, nil - } - - return steps, nil -} - -// AddTutorialComment ajoute un commentaire à un tutoriel -func (tm *TutorialManager) AddTutorialComment(ctx context.Context, tutorialID, userID, username, content string, rating int) (*TutorialComment, error) { - tm.mu.Lock() - defer tm.mu.Unlock() - - commentID := uuid.New().String() - comment := &TutorialComment{ - ID: commentID, - TutorialID: tutorialID, - UserID: userID, - Username: username, - Content: content, - Rating: rating, - IsHelpful: false, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - tm.comments[tutorialID] = append(tm.comments[tutorialID], comment) - - // Mettre à jour la note moyenne du tutoriel - tm.updateTutorialRating(tutorialID) - - tm.logger.Info("Commentaire ajouté", - zap.String("tutorial_id", tutorialID), - zap.String("comment_id", commentID), - zap.String("username", username)) - - return comment, nil -} - -// GetTutorialComments récupère tous les commentaires d'un tutoriel -func (tm *TutorialManager) GetTutorialComments(ctx context.Context, tutorialID string) ([]*TutorialComment, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - comments, exists := tm.comments[tutorialID] - if !exists { - return []*TutorialComment{}, nil - } - - return comments, nil -} - -// IncrementViews incrémente le nombre de vues d'un tutoriel -func (tm *TutorialManager) IncrementViews(ctx context.Context, tutorialID string) error { - tm.mu.Lock() - defer tm.mu.Unlock() - - tutorial, exists := tm.tutorials[tutorialID] - if !exists { - return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - tutorial.Views++ - tutorial.UpdatedAt = time.Now() - - tm.logger.Debug("Vues incrémentées", - zap.String("tutorial_id", tutorialID), - zap.Int64("views", tutorial.Views)) - - return nil -} - -// LikeTutorial ajoute un like à un tutoriel -func (tm *TutorialManager) LikeTutorial(ctx context.Context, tutorialID string) error { - tm.mu.Lock() - defer tm.mu.Unlock() - - tutorial, exists := tm.tutorials[tutorialID] - if !exists { - return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - tutorial.Likes++ - tutorial.UpdatedAt = time.Now() - - tm.logger.Debug("Like ajouté", - zap.String("tutorial_id", tutorialID), - zap.Int64("likes", tutorial.Likes)) - - return nil -} - -// DislikeTutorial ajoute un dislike à un tutoriel -func (tm *TutorialManager) DislikeTutorial(ctx context.Context, tutorialID string) error { - tm.mu.Lock() - defer tm.mu.Unlock() - - tutorial, exists := tm.tutorials[tutorialID] - if !exists { - return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) - } - - tutorial.Dislikes++ - tutorial.UpdatedAt = time.Now() - - tm.logger.Debug("Dislike ajouté", - zap.String("tutorial_id", tutorialID), - zap.Int64("dislikes", tutorial.Dislikes)) - - return nil -} - -// updateTutorialRating met à jour la note moyenne d'un tutoriel -func (tm *TutorialManager) updateTutorialRating(tutorialID string) { - comments, exists := tm.comments[tutorialID] - if !exists || len(comments) == 0 { - return - } - - var totalRating int - var ratedComments int - - for _, comment := range comments { - if comment.Rating > 0 { - totalRating += comment.Rating - ratedComments++ - } - } - - if ratedComments > 0 { - tutorial, exists := tm.tutorials[tutorialID] - if exists { - tutorial.Rating = float64(totalRating) / float64(ratedComments) - tutorial.UpdatedAt = time.Now() - } - } -} - -// SearchTutorials recherche des tutoriels par mots-clés -func (tm *TutorialManager) SearchTutorials(ctx context.Context, query string, filters map[string]interface{}) ([]*Tutorial, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - var results []*Tutorial - query = fmt.Sprintf("%%%s%%", query) // Recherche LIKE - - for _, tutorial := range tm.tutorials { - // Vérifier si le tutoriel correspond à la recherche - matches := false - if contains(tutorial.Title, query) || contains(tutorial.Description, query) || contains(tutorial.Author, query) { - matches = true - } - - // Vérifier les tags - for _, tag := range tutorial.Tags { - if contains(tag, query) { - matches = true - break - } - } - - if !matches { - continue - } - - // Appliquer les filtres si fournis - if filters != nil { - if category, ok := filters["category"].(string); ok && tutorial.Category != category { - continue - } - if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished { - continue - } - if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree { - continue - } - } - - results = append(results, tutorial) - } - - return results, nil -} - -// contains vérifie si une chaîne contient une sous-chaîne (insensible à la casse) -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || - (len(s) > len(substr) && (s[:len(substr)] == substr || - s[len(s)-len(substr):] == substr || - containsSubstring(s, substr)))) -} - -// containsSubstring vérifie si une chaîne contient une sous-chaîne -func containsSubstring(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -}