chore(audit 2.4, 2.5): supprimer code mort Education et cmd/modern-server

- Supprimer routes/handlers/core Education (backend)
- Supprimer handler MSW education, refs Sidebar/locales
- Basculer Makefile, make/dev.mk, scripts vers cmd/api/main.go
- Supprimer veza-backend-api/cmd/modern-server/
This commit is contained in:
senke 2026-02-15 14:39:40 +01:00
parent 43af35fd93
commit 22e5e21757
22 changed files with 291 additions and 2066 deletions

View file

@ -665,8 +665,8 @@ veza/
| 2.1 | ~~Unifier `components/views/` et `features/*/pages/`~~ | **L** | **✅ Fait** — pattern unique `features/*/pages/` | | 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.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.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.4 | ~~Supprimer le code mort (Education, Studio, Gamification)~~ | **M** | **✅ Fait** — routes backend, MSW, refs supprimés |
| 2.5 | Supprimer `cmd/modern-server/` | **S** | Serveur alternatif inutile | | 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.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.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` | | 2.8 | Gitignorer les fichiers `.out`, test results, `.turbo/` | **S** | Mettre à jour `.gitignore` |

View file

@ -3,7 +3,7 @@ import { useLocation, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag, Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag,
GraduationCap, BarChart2, Shield, Box, MessageSquare, BarChart2, Shield, Box, MessageSquare,
Layers, Cpu, Heart, ListMusic, CreditCard, DollarSign, Terminal, Layers, Cpu, Heart, ListMusic, CreditCard, DollarSign, Terminal,
ChevronLeft, ChevronRight, ChevronLeft, ChevronRight,
} from 'lucide-react'; } from 'lucide-react';
@ -38,7 +38,6 @@ const iconMap: Record<string, React.ReactNode> = {
marketplace: <ShoppingBag className="w-4 h-4" />, marketplace: <ShoppingBag className="w-4 h-4" />,
live: <Radio className="w-4 h-4" />, live: <Radio className="w-4 h-4" />,
chat: <MessageSquare className="w-4 h-4" />, chat: <MessageSquare className="w-4 h-4" />,
education: <GraduationCap className="w-4 h-4" />,
sell: <DollarSign className="w-4 h-4" />, sell: <DollarSign className="w-4 h-4" />,
wishlist: <Heart className="w-4 h-4" />, wishlist: <Heart className="w-4 h-4" />,
purchases: <CreditCard className="w-4 h-4" />, purchases: <CreditCard className="w-4 h-4" />,

View file

@ -47,7 +47,10 @@
"never": "Never", "never": "Never",
"changeTheme": "Change theme", "changeTheme": "Change theme",
"userMenu": "User menu", "userMenu": "User menu",
"notifications": "Notifications" "notifications": "Notifications",
"retry": "Retry",
"retrying": "Retrying...",
"dismiss": "Dismiss"
}, },
"auth": { "auth": {
"login": { "login": {
@ -199,7 +202,9 @@
"empty": { "empty": {
"title": "Your library is empty", "title": "Your library is empty",
"subtitle": "Start by uploading your first files", "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": { "profile": {
@ -510,7 +515,6 @@
"marketplace": "Marketplace", "marketplace": "Marketplace",
"live": "Live Sessions", "live": "Live Sessions",
"chat": "Channels", "chat": "Channels",
"education": "Academy",
"sell": "Seller Dashboard", "sell": "Seller Dashboard",
"wishlist": "Wishlist", "wishlist": "Wishlist",
"purchases": "Purchases", "purchases": "Purchases",

View file

@ -47,7 +47,10 @@
"never": "Jamais", "never": "Jamais",
"changeTheme": "Changer le thème", "changeTheme": "Changer le thème",
"userMenu": "Menu utilisateur", "userMenu": "Menu utilisateur",
"notifications": "Notifications" "notifications": "Notifications",
"retry": "Réessayer",
"retrying": "Nouvelle tentative...",
"dismiss": "Fermer"
}, },
"auth": { "auth": {
"login": { "login": {
@ -199,7 +202,9 @@
"empty": { "empty": {
"title": "Votre bibliothèque est vide", "title": "Votre bibliothèque est vide",
"subtitle": "Commencez par téléverser vos premiers fichiers", "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": { "profile": {
@ -510,7 +515,6 @@
"marketplace": "Marketplace", "marketplace": "Marketplace",
"live": "Sessions Live", "live": "Sessions Live",
"chat": "Canaux", "chat": "Canaux",
"education": "Académie",
"sell": "Tableau vendeur", "sell": "Tableau vendeur",
"wishlist": "Liste de souhaits", "wishlist": "Liste de souhaits",
"purchases": "Achats", "purchases": "Achats",

View file

@ -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',
},
},
});
}),
];

View file

@ -15,7 +15,7 @@ services:
language: "go" language: "go"
version: "1.23" version: "1.23"
health_endpoint: "/health" 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" working_directory: "veza-backend-api"
dependencies: dependencies:
- "postgresql" - "postgresql"

View file

@ -310,7 +310,7 @@ echo "✅ Redis démarré"
# Démarrer Backend API # Démarrer Backend API
cd veza-backend-api cd veza-backend-api
go run cmd/modern-server/main.go & go run cmd/api/main.go &
BACKEND_PID=$! BACKEND_PID=$!
echo "✅ Backend API démarré (PID: $BACKEND_PID)" echo "✅ Backend API démarré (PID: $BACKEND_PID)"

View file

@ -25,7 +25,7 @@ dev-full: check-ports infra-up ## [HIGH] Start Everything inc. Chat + Stream (Ru
if command -v air >/dev/null; then \ if command -v air >/dev/null; then \
$(ECHO_CMD) "${GREEN}[Go] Hot Reload Active (Air)${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & \ $(ECHO_CMD) "${GREEN}[Go] Hot Reload Active (Air)${NC}" && cd $(ROOT)/$(SERVICE_DIR_backend-api) && air & \
else \ 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; \ fi; \
if command -v cargo-watch >/dev/null; then \ 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 & \ $(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) dev-backend: check-ports infra-up ## [MID] Start Backends Only (Hot Reload supported)
@$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING BACKEND ONLY${NC}" @$(ECHO_CMD) "${BOLD}${PURPLE}🚀 STARTING BACKEND ONLY${NC}"
@(trap 'kill 0' SIGINT; \ @(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_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; \ 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) 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 dev-backend-api: check-ports infra-up ## [MID] Start Go backend only
@$(ECHO_CMD) "${GREEN}[Backend API] Starting...${NC}" @$(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 dev-chat-server: check-ports infra-up ## [MID] Start Chat server only
@$(ECHO_CMD) "${GREEN}[Chat] Starting...${NC}" @$(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 @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) 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) 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 @if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi
@$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}" @$(ECHO_CMD) "${BLUE}🚀 Starting $(SERVICE)...${NC}"
@case "$(SERVICE)" in \ @case "$(SERVICE)" in \
backend-api) \ 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) \ 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 ;; \ 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) \ 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) 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 @if [ -z "$(SERVICE)" ]; then $(ECHO_CMD) "${RED}❌ Please specify SERVICE=name${NC}"; exit 1; fi
@case "$(SERVICE)" in \ @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 ;; \ chat-server|stream-server) pkill -f "cargo.*$(SERVICE)" 2>/dev/null || true ;; \
web) pkill -f "npm run dev\|vite" 2>/dev/null || true ;; \ web) pkill -f "npm run dev\|vite" 2>/dev/null || true ;; \
*) $(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}" ;; \ *) $(ECHO_CMD) "${RED}Unknown service: $(SERVICE)${NC}" ;; \

View file

@ -56,13 +56,13 @@ else
echo -e "${BLUE}📍 API sera accessible sur http://127.0.0.1:8080/api/v1${NC}" echo -e "${BLUE}📍 API sera accessible sur http://127.0.0.1:8080/api/v1${NC}"
echo "" echo ""
# Chercher le point d'entrée # Point d'entrée canonique
if [ -f "cmd/modern-server/main.go" ]; then if [ -f "cmd/api/main.go" ]; then
exec go run cmd/modern-server/main.go exec go run cmd/api/main.go
elif [ -f "cmd/server/main.go" ]; then elif [ -f "cmd/server/main.go" ]; then
exec go run cmd/server/main.go exec go run cmd/server/main.go
else 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 exit 1
fi fi
fi fi

View file

@ -92,7 +92,7 @@ if lsof -i :8080 -t >/dev/null 2>&1; then
fi fi
# Start Backend in background # 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=$! BACKEND_PID=$!
echo $BACKEND_PID > backend.pid echo $BACKEND_PID > backend.pid
echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend_boot.log${NC}" echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend_boot.log${NC}"

View file

@ -31,7 +31,7 @@ cd ..
echo -e "${BLUE}⚙️ Starting Backend API (port 8080)...${NC}" 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? # 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. # 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=$! BACKEND_PID=$!
echo $BACKEND_PID > backend.pid echo $BACKEND_PID > backend.pid
echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend.log${NC}" echo -e "${GREEN}✅ Backend running (PID: $BACKEND_PID). Logs: backend.log${NC}"

View file

@ -13,7 +13,7 @@ echo "📜 Contract: docs/BUDGETS.md"
echo "🧠 Budget: ${MAX_RSS_MB}MB RSS (Idle)" echo "🧠 Budget: ${MAX_RSS_MB}MB RSS (Idle)"
cd "$BACKEND_DIR" 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 # Start
./server_mem_check > /dev/null 2>&1 & ./server_mem_check > /dev/null 2>&1 &

View file

@ -15,7 +15,7 @@ echo "⏱️ Budget: ${MAX_STARTUP_SECONDS}s to reach ready state"
cd "$BACKEND_DIR" cd "$BACKEND_DIR"
# Build first to not count compilation time # 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 # 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. # This is a naive check: we expect it to NOT crash and to output something quickly.

View file

@ -32,12 +32,12 @@ help: ## Affiche cette aide
# Développement # Développement
build: ## Compile l'application build: ## Compile l'application
@echo "$(GREEN)🔨 Compilation de l'application...$(NC)" @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)" @echo "$(GREEN)✅ Compilation terminée: bin/$(BINARY_NAME)$(NC)"
build-linux: ## Compile l'application pour Linux build-linux: ## Compile l'application pour Linux
@echo "$(GREEN)🔨 Compilation pour Linux...$(NC)" @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)" @echo "$(GREEN)✅ Compilation Linux terminée: bin/$(BINARY_NAME)-linux$(NC)"
# Tests # Tests
@ -107,7 +107,7 @@ deps: ## Installe les dépendances
install: ## Installe l'application install: ## Installe l'application
@echo "$(GREEN)📦 Installation de l'application...$(NC)" @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)" @echo "$(GREEN)✅ Application installée$(NC)"
# Nettoyage # Nettoyage
@ -125,7 +125,7 @@ run: build ## Compile et exécute l'application
dev: ## Exécute l'application en mode développement dev: ## Exécute l'application en mode développement
@echo "$(GREEN)🚀 Mode développement...$(NC)" @echo "$(GREEN)🚀 Mode développement...$(NC)"
@go run ./cmd/modern-server/main.go @go run ./cmd/api/main.go
# Docker # Docker
docker-build: ## Construit l'image Docker docker-build: ## Construit l'image Docker

View file

@ -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")
}
}

View file

@ -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")
}

View file

@ -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)
}
}
}
}

View file

@ -290,9 +290,6 @@ func (r *APIRouter) Setup(router *gin.Engine) error {
// Social Routes // Social Routes
r.setupSocialRoutes(v1) r.setupSocialRoutes(v1)
// Education Routes (courses, tutorials)
r.setupEducationRoutes(v1)
// Inventory / Gear Routes // Inventory / Gear Routes
r.setupGearRoutes(v1) r.setupGearRoutes(v1)

View file

@ -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)
}

View file

@ -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. // 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. // 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 // SetupMiddleware configure les middlewares globaux
// DÉPRÉCIÉ : Cette méthode est conservée pour compatibilité mais ne fait plus rien // DÉPRÉCIÉ : Cette méthode est conservée pour compatibilité mais ne fait plus rien

View file

@ -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
}

View file

@ -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
}