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:
parent
43af35fd93
commit
22e5e21757
22 changed files with 291 additions and 2066 deletions
|
|
@ -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` |
|
||||||
|
|
|
||||||
|
|
@ -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" />,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
253
apps/web/src/mocks/handlers-misc.ts
Normal file
253
apps/web/src/mocks/handlers-misc.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
|
|
||||||
|
|
|
||||||
12
make/dev.mk
12
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 \
|
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}" ;; \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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 &
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue