package api import ( "fmt" "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/academy" "github.com/breakpilot/ai-compliance-sdk/internal/db" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/rag" "github.com/gin-gonic/gin" ) // AcademyHandler handles all Academy-related HTTP requests type AcademyHandler struct { dbPool *db.Pool llmService *llm.Service ragService *rag.Service academyStore *db.AcademyMemStore } // NewAcademyHandler creates a new Academy handler func NewAcademyHandler(dbPool *db.Pool, llmService *llm.Service, ragService *rag.Service) *AcademyHandler { return &AcademyHandler{ dbPool: dbPool, llmService: llmService, ragService: ragService, academyStore: db.NewAcademyMemStore(), } } func (h *AcademyHandler) getTenantID(c *gin.Context) string { tid := c.GetHeader("X-Tenant-ID") if tid == "" { tid = c.Query("tenantId") } if tid == "" { tid = "default-tenant" } return tid } // --------------------------------------------------------------------------- // Course CRUD // --------------------------------------------------------------------------- // ListCourses returns all courses for the tenant func (h *AcademyHandler) ListCourses(c *gin.Context) { tenantID := h.getTenantID(c) rows := h.academyStore.ListCourses(tenantID) courses := make([]AcademyCourse, 0, len(rows)) for _, row := range rows { lessons := h.buildLessonsForCourse(row.ID) courses = append(courses, courseRowToResponse(row, lessons)) } SuccessResponse(c, courses) } // GetCourse returns a single course with its lessons func (h *AcademyHandler) GetCourse(c *gin.Context) { id := c.Param("id") row, err := h.academyStore.GetCourse(id) if err != nil { ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") return } lessons := h.buildLessonsForCourse(row.ID) SuccessResponse(c, courseRowToResponse(row, lessons)) } // CreateCourse creates a new course with optional lessons func (h *AcademyHandler) CreateCourse(c *gin.Context) { var req CreateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } passingScore := req.PassingScore if passingScore == 0 { passingScore = 70 } roles := req.RequiredForRoles if len(roles) == 0 { roles = []string{"all"} } courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{ TenantID: req.TenantID, Title: req.Title, Description: req.Description, Category: req.Category, PassingScore: passingScore, DurationMinutes: req.DurationMinutes, RequiredForRoles: roles, Status: "draft", }) // Create lessons for i, lessonReq := range req.Lessons { order := lessonReq.Order if order == 0 { order = i + 1 } lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{ CourseID: courseRow.ID, Title: lessonReq.Title, Type: lessonReq.Type, ContentMarkdown: lessonReq.ContentMarkdown, VideoURL: lessonReq.VideoURL, SortOrder: order, DurationMinutes: lessonReq.DurationMinutes, }) // Create quiz questions for this lesson for j, qReq := range lessonReq.QuizQuestions { qOrder := qReq.Order if qOrder == 0 { qOrder = j + 1 } h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{ LessonID: lessonRow.ID, Question: qReq.Question, Options: qReq.Options, CorrectOptionIndex: qReq.CorrectOptionIndex, Explanation: qReq.Explanation, SortOrder: qOrder, }) } } lessons := h.buildLessonsForCourse(courseRow.ID) c.JSON(http.StatusCreated, Response{ Success: true, Data: courseRowToResponse(courseRow, lessons), }) } // UpdateCourse updates an existing course func (h *AcademyHandler) UpdateCourse(c *gin.Context) { id := c.Param("id") var req UpdateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } updates := make(map[string]interface{}) if req.Title != nil { updates["title"] = *req.Title } if req.Description != nil { updates["description"] = *req.Description } if req.Category != nil { updates["category"] = *req.Category } if req.DurationMinutes != nil { updates["durationminutes"] = *req.DurationMinutes } if req.PassingScore != nil { updates["passingscore"] = *req.PassingScore } if req.RequiredForRoles != nil { updates["requiredforroles"] = req.RequiredForRoles } row, err := h.academyStore.UpdateCourse(id, updates) if err != nil { ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") return } lessons := h.buildLessonsForCourse(row.ID) SuccessResponse(c, courseRowToResponse(row, lessons)) } // DeleteCourse deletes a course and all related data func (h *AcademyHandler) DeleteCourse(c *gin.Context) { id := c.Param("id") if err := h.academyStore.DeleteCourse(id); err != nil { ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") return } SuccessResponse(c, gin.H{ "courseId": id, "deletedAt": now(), }) } // GetStatistics returns academy statistics for the tenant func (h *AcademyHandler) GetStatistics(c *gin.Context) { tenantID := h.getTenantID(c) stats := h.academyStore.GetStatistics(tenantID) SuccessResponse(c, AcademyStatistics{ TotalCourses: stats.TotalCourses, TotalEnrollments: stats.TotalEnrollments, CompletionRate: int(stats.CompletionRate), OverdueCount: stats.OverdueCount, ByCategory: stats.ByCategory, ByStatus: stats.ByStatus, }) } // --------------------------------------------------------------------------- // Enrollments // --------------------------------------------------------------------------- // ListEnrollments returns enrollments filtered by tenant and optionally course func (h *AcademyHandler) ListEnrollments(c *gin.Context) { tenantID := h.getTenantID(c) courseID := c.Query("courseId") rows := h.academyStore.ListEnrollments(tenantID, courseID) enrollments := make([]AcademyEnrollment, 0, len(rows)) for _, row := range rows { enrollments = append(enrollments, enrollmentRowToResponse(row)) } SuccessResponse(c, enrollments) } // EnrollUser enrolls a user in a course func (h *AcademyHandler) EnrollUser(c *gin.Context) { var req EnrollUserRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } deadline, err := time.Parse(time.RFC3339, req.Deadline) if err != nil { deadline, err = time.Parse("2006-01-02", req.Deadline) if err != nil { ErrorResponse(c, http.StatusBadRequest, "Invalid deadline format. Use RFC3339 or YYYY-MM-DD.", "INVALID_DEADLINE") return } } row := h.academyStore.CreateEnrollment(&db.AcademyEnrollmentRow{ TenantID: req.TenantID, CourseID: req.CourseID, UserID: req.UserID, UserName: req.UserName, UserEmail: req.UserEmail, Status: "not_started", Progress: 0, Deadline: deadline, }) c.JSON(http.StatusCreated, Response{ Success: true, Data: enrollmentRowToResponse(row), }) } // UpdateProgress updates the progress of an enrollment func (h *AcademyHandler) UpdateProgress(c *gin.Context) { id := c.Param("id") var req UpdateProgressRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } enrollment, err := h.academyStore.GetEnrollment(id) if err != nil { ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") return } updates := map[string]interface{}{ "progress": req.Progress, } // Auto-update status based on progress if req.Progress >= 100 { updates["status"] = "completed" t := time.Now() updates["completedat"] = &t } else if req.Progress > 0 && enrollment.Status == "not_started" { updates["status"] = "in_progress" } row, err := h.academyStore.UpdateEnrollment(id, updates) if err != nil { ErrorResponse(c, http.StatusInternalServerError, "Failed to update progress", "UPDATE_FAILED") return } // Upsert lesson progress if lessonID provided if req.LessonID != "" { t := time.Now() h.academyStore.UpsertLessonProgress(&db.AcademyLessonProgressRow{ EnrollmentID: id, LessonID: req.LessonID, Completed: true, CompletedAt: &t, }) } SuccessResponse(c, enrollmentRowToResponse(row)) } // CompleteEnrollment marks an enrollment as completed func (h *AcademyHandler) CompleteEnrollment(c *gin.Context) { id := c.Param("id") t := time.Now() updates := map[string]interface{}{ "status": "completed", "progress": 100, "completedat": &t, } row, err := h.academyStore.UpdateEnrollment(id, updates) if err != nil { ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") return } SuccessResponse(c, enrollmentRowToResponse(row)) } // --------------------------------------------------------------------------- // Quiz // --------------------------------------------------------------------------- // SubmitQuiz evaluates quiz answers for a lesson func (h *AcademyHandler) SubmitQuiz(c *gin.Context) { lessonID := c.Param("id") var req SubmitQuizRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } // Get the lesson lesson, err := h.academyStore.GetLesson(lessonID) if err != nil { ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND") return } // Get quiz questions questions := h.academyStore.ListQuizQuestions(lessonID) if len(questions) == 0 { ErrorResponse(c, http.StatusBadRequest, "No quiz questions found for this lesson", "NO_QUIZ_QUESTIONS") return } if len(req.Answers) != len(questions) { ErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Expected %d answers, got %d", len(questions), len(req.Answers)), "ANSWER_COUNT_MISMATCH") return } // Evaluate answers correctCount := 0 results := make([]QuizQuestionResult, len(questions)) for i, q := range questions { correct := req.Answers[i] == q.CorrectOptionIndex if correct { correctCount++ } results[i] = QuizQuestionResult{ QuestionID: q.ID, Correct: correct, Explanation: q.Explanation, } } score := 0 if len(questions) > 0 { score = int(float64(correctCount) / float64(len(questions)) * 100) } // Determine pass/fail based on course's passing score passingScore := 70 // default course, err := h.academyStore.GetCourse(lesson.CourseID) if err == nil && course.PassingScore > 0 { passingScore = course.PassingScore } SuccessResponse(c, SubmitQuizResponse{ Score: score, Passed: score >= passingScore, CorrectAnswers: correctCount, TotalQuestions: len(questions), Results: results, }) } // --------------------------------------------------------------------------- // Certificates // --------------------------------------------------------------------------- // GenerateCertificateEndpoint generates a certificate for a completed enrollment func (h *AcademyHandler) GenerateCertificateEndpoint(c *gin.Context) { enrollmentID := c.Param("id") enrollment, err := h.academyStore.GetEnrollment(enrollmentID) if err != nil { ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") return } // Check if already has certificate if enrollment.CertificateID != "" { existing, err := h.academyStore.GetCertificate(enrollment.CertificateID) if err == nil { SuccessResponse(c, certificateRowToResponse(existing)) return } } // Get course name courseName := "Unbekannter Kurs" course, err := h.academyStore.GetCourse(enrollment.CourseID) if err == nil { courseName = course.Title } issuedAt := time.Now() validUntil := issuedAt.AddDate(1, 0, 0) // 1 year validity cert := h.academyStore.CreateCertificate(&db.AcademyCertificateRow{ TenantID: enrollment.TenantID, EnrollmentID: enrollmentID, CourseID: enrollment.CourseID, UserID: enrollment.UserID, UserName: enrollment.UserName, CourseName: courseName, Score: enrollment.Progress, IssuedAt: issuedAt, ValidUntil: validUntil, }) // Update enrollment with certificate ID h.academyStore.UpdateEnrollment(enrollmentID, map[string]interface{}{ "certificateid": cert.ID, }) c.JSON(http.StatusCreated, Response{ Success: true, Data: certificateRowToResponse(cert), }) } // GetCertificate returns a certificate by ID func (h *AcademyHandler) GetCertificate(c *gin.Context) { id := c.Param("id") cert, err := h.academyStore.GetCertificate(id) if err != nil { ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND") return } SuccessResponse(c, certificateRowToResponse(cert)) } // DownloadCertificatePDF returns the PDF for a certificate func (h *AcademyHandler) DownloadCertificatePDF(c *gin.Context) { id := c.Param("id") cert, err := h.academyStore.GetCertificate(id) if err != nil { ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND") return } if cert.PdfURL != "" { c.Redirect(http.StatusFound, cert.PdfURL) return } // Generate PDF on-the-fly pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{ CertificateID: cert.ID, UserName: cert.UserName, CourseName: cert.CourseName, CompanyName: "", Score: cert.Score, IssuedAt: cert.IssuedAt, ValidUntil: cert.ValidUntil, }) if err != nil { ErrorResponse(c, http.StatusInternalServerError, "Failed to generate PDF", "PDF_GENERATION_FAILED") return } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=zertifikat-%s.pdf", cert.ID[:min(8, len(cert.ID))])) c.Data(http.StatusOK, "application/pdf", pdfBytes) } // --------------------------------------------------------------------------- // AI Course Generation // --------------------------------------------------------------------------- // GenerateCourse generates a course using AI func (h *AcademyHandler) GenerateCourse(c *gin.Context) { var req GenerateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") return } // Get RAG context if requested var ragSources []SearchResult if req.UseRAG && h.ragService != nil { query := req.RAGQuery if query == "" { query = req.Topic + " Compliance Schulung" } results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "") for _, r := range results { ragSources = append(ragSources, SearchResult{ ID: r.ID, Content: r.Content, Source: r.Source, Score: r.Score, Metadata: r.Metadata, }) } } // Generate course content (mock for now) course := h.generateMockCourse(req) // Save to store courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{ TenantID: req.TenantID, Title: course.Title, Description: course.Description, Category: req.Category, PassingScore: 70, DurationMinutes: course.DurationMinutes, RequiredForRoles: []string{"all"}, Status: "draft", }) for _, lesson := range course.Lessons { lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{ CourseID: courseRow.ID, Title: lesson.Title, Type: lesson.Type, ContentMarkdown: lesson.ContentMarkdown, SortOrder: lesson.Order, DurationMinutes: lesson.DurationMinutes, }) for _, q := range lesson.QuizQuestions { h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{ LessonID: lessonRow.ID, Question: q.Question, Options: q.Options, CorrectOptionIndex: q.CorrectOptionIndex, Explanation: q.Explanation, SortOrder: q.Order, }) } } lessons := h.buildLessonsForCourse(courseRow.ID) c.JSON(http.StatusCreated, Response{ Success: true, Data: gin.H{ "course": courseRowToResponse(courseRow, lessons), "ragSources": ragSources, "model": h.llmService.GetModel(), }, }) } // RegenerateLesson regenerates a single lesson using AI func (h *AcademyHandler) RegenerateLesson(c *gin.Context) { lessonID := c.Param("id") _, err := h.academyStore.GetLesson(lessonID) if err != nil { ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND") return } // For now, return the existing lesson SuccessResponse(c, gin.H{ "lessonId": lessonID, "status": "regeneration_pending", "message": "AI lesson regeneration will be available in a future version", }) } // --------------------------------------------------------------------------- // Video Generation // --------------------------------------------------------------------------- // GenerateVideos initiates video generation for all lessons in a course func (h *AcademyHandler) GenerateVideos(c *gin.Context) { courseID := c.Param("id") _, err := h.academyStore.GetCourse(courseID) if err != nil { ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") return } lessons := h.academyStore.ListLessons(courseID) lessonStatuses := make([]LessonVideoStatus, 0, len(lessons)) for _, l := range lessons { if l.Type == "text" || l.Type == "video" { lessonStatuses = append(lessonStatuses, LessonVideoStatus{ LessonID: l.ID, Status: "pending", }) } } SuccessResponse(c, VideoStatusResponse{ CourseID: courseID, Status: "pending", Lessons: lessonStatuses, }) } // GetVideoStatus returns the video generation status for a course func (h *AcademyHandler) GetVideoStatus(c *gin.Context) { courseID := c.Param("id") _, err := h.academyStore.GetCourse(courseID) if err != nil { ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") return } lessons := h.academyStore.ListLessons(courseID) lessonStatuses := make([]LessonVideoStatus, 0, len(lessons)) for _, l := range lessons { status := LessonVideoStatus{ LessonID: l.ID, Status: "not_started", VideoURL: l.VideoURL, AudioURL: l.AudioURL, } if l.VideoURL != "" { status.Status = "completed" } lessonStatuses = append(lessonStatuses, status) } overallStatus := "not_started" hasCompleted := false hasPending := false for _, s := range lessonStatuses { if s.Status == "completed" { hasCompleted = true } else { hasPending = true } } if hasCompleted && !hasPending { overallStatus = "completed" } else if hasCompleted && hasPending { overallStatus = "processing" } SuccessResponse(c, VideoStatusResponse{ CourseID: courseID, Status: overallStatus, Lessons: lessonStatuses, }) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func (h *AcademyHandler) buildLessonsForCourse(courseID string) []AcademyLesson { lessonRows := h.academyStore.ListLessons(courseID) lessons := make([]AcademyLesson, 0, len(lessonRows)) for _, lr := range lessonRows { var questions []AcademyQuizQuestion if lr.Type == "quiz" { qRows := h.academyStore.ListQuizQuestions(lr.ID) questions = make([]AcademyQuizQuestion, 0, len(qRows)) for _, qr := range qRows { questions = append(questions, quizQuestionRowToResponse(qr)) } } lessons = append(lessons, lessonRowToResponse(lr, questions)) } return lessons } func courseRowToResponse(row *db.AcademyCourseRow, lessons []AcademyLesson) AcademyCourse { return AcademyCourse{ ID: row.ID, TenantID: row.TenantID, Title: row.Title, Description: row.Description, Category: row.Category, PassingScore: row.PassingScore, DurationMinutes: row.DurationMinutes, RequiredForRoles: row.RequiredForRoles, Status: row.Status, Lessons: lessons, CreatedAt: row.CreatedAt.Format(time.RFC3339), UpdatedAt: row.UpdatedAt.Format(time.RFC3339), } } func lessonRowToResponse(row *db.AcademyLessonRow, questions []AcademyQuizQuestion) AcademyLesson { return AcademyLesson{ ID: row.ID, CourseID: row.CourseID, Title: row.Title, Type: row.Type, ContentMarkdown: row.ContentMarkdown, VideoURL: row.VideoURL, AudioURL: row.AudioURL, Order: row.SortOrder, DurationMinutes: row.DurationMinutes, QuizQuestions: questions, } } func quizQuestionRowToResponse(row *db.AcademyQuizQuestionRow) AcademyQuizQuestion { return AcademyQuizQuestion{ ID: row.ID, LessonID: row.LessonID, Question: row.Question, Options: row.Options, CorrectOptionIndex: row.CorrectOptionIndex, Explanation: row.Explanation, Order: row.SortOrder, } } func enrollmentRowToResponse(row *db.AcademyEnrollmentRow) AcademyEnrollment { e := AcademyEnrollment{ ID: row.ID, TenantID: row.TenantID, CourseID: row.CourseID, UserID: row.UserID, UserName: row.UserName, UserEmail: row.UserEmail, Status: row.Status, Progress: row.Progress, StartedAt: row.StartedAt.Format(time.RFC3339), CertificateID: row.CertificateID, Deadline: row.Deadline.Format(time.RFC3339), CreatedAt: row.CreatedAt.Format(time.RFC3339), UpdatedAt: row.UpdatedAt.Format(time.RFC3339), } if row.CompletedAt != nil { e.CompletedAt = row.CompletedAt.Format(time.RFC3339) } return e } func certificateRowToResponse(row *db.AcademyCertificateRow) AcademyCertificate { return AcademyCertificate{ ID: row.ID, TenantID: row.TenantID, EnrollmentID: row.EnrollmentID, CourseID: row.CourseID, UserID: row.UserID, UserName: row.UserName, CourseName: row.CourseName, Score: row.Score, IssuedAt: row.IssuedAt.Format(time.RFC3339), ValidUntil: row.ValidUntil.Format(time.RFC3339), PdfURL: row.PdfURL, } } // --------------------------------------------------------------------------- // Mock Course Generator (used when LLM is not available) // --------------------------------------------------------------------------- func (h *AcademyHandler) generateMockCourse(req GenerateCourseRequest) AcademyCourse { switch req.Category { case "dsgvo_basics": return h.mockDSGVOCourse(req) case "it_security": return h.mockITSecurityCourse(req) case "ai_literacy": return h.mockAILiteracyCourse(req) case "whistleblower_protection": return h.mockWhistleblowerCourse(req) default: return h.mockDSGVOCourse(req) } } func (h *AcademyHandler) mockDSGVOCourse(req GenerateCourseRequest) AcademyCourse { return AcademyCourse{ Title: "DSGVO-Grundlagen fuer Mitarbeiter", Description: "Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten.", DurationMinutes: 90, Lessons: []AcademyLesson{ { Title: "Was ist die DSGVO?", Type: "text", Order: 1, DurationMinutes: 15, ContentMarkdown: "# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der EU, die seit dem 25. Mai 2018 gilt. Sie schuetzt die Grundrechte natuerlicher Personen bei der Verarbeitung personenbezogener Daten.\n\n## Warum ist die DSGVO wichtig?\n\n- **Einheitlicher Datenschutz** in der gesamten EU\n- **Hohe Bussgelder** bei Verstoessen (bis 20 Mio. EUR oder 4% des Jahresumsatzes)\n- **Staerkung der Betroffenenrechte** (Auskunft, Loeschung, Widerspruch)\n\n## Zentrale Begriffe\n\n- **Personenbezogene Daten**: Alle Informationen, die sich auf eine identifizierte oder identifizierbare Person beziehen\n- **Verantwortlicher**: Die Stelle, die ueber Zweck und Mittel der Verarbeitung entscheidet\n- **Auftragsverarbeiter**: Verarbeitet Daten im Auftrag des Verantwortlichen", }, { Title: "Die 7 Grundsaetze der DSGVO", Type: "text", Order: 2, DurationMinutes: 20, ContentMarkdown: "# Die 7 Grundsaetze der DSGVO (Art. 5)\n\n## 1. Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz\nPersonenbezogene Daten muessen auf rechtmaessige Weise verarbeitet werden.\n\n## 2. Zweckbindung\nDaten duerfen nur fuer festgelegte, eindeutige und legitime Zwecke erhoben werden.\n\n## 3. Datenminimierung\nEs duerfen nur Daten erhoben werden, die fuer den Zweck erforderlich sind.\n\n## 4. Richtigkeit\nDaten muessen sachlich richtig und auf dem neuesten Stand sein.\n\n## 5. Speicherbegrenzung\nDaten duerfen nur so lange gespeichert werden, wie es fuer den Zweck erforderlich ist.\n\n## 6. Integritaet und Vertraulichkeit\nDaten muessen vor unbefugtem Zugriff geschuetzt werden.\n\n## 7. Rechenschaftspflicht\nDer Verantwortliche muss die Einhaltung der Grundsaetze nachweisen koennen.", }, { Title: "Betroffenenrechte (Art. 15-22 DSGVO)", Type: "text", Order: 3, DurationMinutes: 20, ContentMarkdown: "# Betroffenenrechte\n\n## Recht auf Auskunft (Art. 15)\nJede Person hat das Recht zu erfahren, ob und welche Daten ueber sie verarbeitet werden.\n\n## Recht auf Berichtigung (Art. 16)\nUnrichtige Daten muessen berichtigt werden.\n\n## Recht auf Loeschung (Art. 17)\nDas 'Recht auf Vergessenwerden' ermoeglicht die Loeschung personenbezogener Daten.\n\n## Recht auf Einschraenkung (Art. 18)\nBetroffene koennen die Verarbeitung einschraenken lassen.\n\n## Recht auf Datenuebertragbarkeit (Art. 20)\nDaten muessen in einem maschinenlesbaren Format bereitgestellt werden.\n\n## Widerspruchsrecht (Art. 21)\nBetroffene koennen der Verarbeitung widersprechen.", }, { Title: "Datenschutz im Arbeitsalltag", Type: "text", Order: 4, DurationMinutes: 15, ContentMarkdown: "# Datenschutz im Arbeitsalltag\n\n## E-Mails\n- Keine personenbezogenen Daten unverschluesselt versenden\n- BCC statt CC bei Massenversand\n- Vorsicht bei Anhangen\n\n## Bildschirmsperre\n- Computer bei Abwesenheit sperren (Win+L / Cmd+Ctrl+Q)\n- Automatische Sperre nach 5 Minuten\n\n## Clean Desk Policy\n- Keine sensiblen Dokumente offen liegen lassen\n- Aktenvernichter fuer Papierdokumente\n\n## Homeoffice\n- VPN nutzen\n- Kein oeffentliches WLAN fuer Firmendaten\n- Bildschirm vor Mitlesern schuetzen\n\n## Datenpannen melden\n- **Sofort** den Datenschutzbeauftragten informieren\n- Innerhalb von 72 Stunden an die Aufsichtsbehoerde\n- Dokumentation der Panne", }, { Title: "Wissenstest: DSGVO-Grundlagen", Type: "quiz", Order: 5, DurationMinutes: 20, QuizQuestions: []AcademyQuizQuestion{ { Question: "Seit wann gilt die DSGVO?", Options: []string{"1. Januar 2016", "25. Mai 2018", "1. Januar 2020", "25. Mai 2020"}, CorrectOptionIndex: 1, Explanation: "Die DSGVO gilt seit dem 25. Mai 2018 in allen EU-Mitgliedstaaten.", Order: 1, }, { Question: "Was sind personenbezogene Daten?", Options: []string{"Nur Name und Adresse", "Alle Informationen, die sich auf eine identifizierbare Person beziehen", "Nur digitale Daten", "Nur sensible Gesundheitsdaten"}, CorrectOptionIndex: 1, Explanation: "Personenbezogene Daten umfassen alle Informationen, die sich auf eine identifizierte oder identifizierbare natuerliche Person beziehen.", Order: 2, }, { Question: "Wie hoch kann das Bussgeld bei DSGVO-Verstoessen maximal sein?", Options: []string{"1 Mio. EUR", "5 Mio. EUR", "10 Mio. EUR oder 2% des Jahresumsatzes", "20 Mio. EUR oder 4% des Jahresumsatzes"}, CorrectOptionIndex: 3, Explanation: "Bei schwerwiegenden Verstoessen koennen Bussgelder von bis zu 20 Mio. EUR oder 4% des weltweiten Jahresumsatzes verhaengt werden.", Order: 3, }, { Question: "Was bedeutet das Prinzip der Datenminimierung?", Options: []string{"Alle Daten muessen verschluesselt werden", "Es duerfen nur fuer den Zweck erforderliche Daten erhoben werden", "Daten muessen nach 30 Tagen geloescht werden", "Nur Administratoren duerfen auf Daten zugreifen"}, CorrectOptionIndex: 1, Explanation: "Datenminimierung bedeutet, dass nur die fuer den jeweiligen Zweck erforderlichen Daten erhoben und verarbeitet werden duerfen.", Order: 4, }, { Question: "Innerhalb welcher Frist muss eine Datenpanne der Aufsichtsbehoerde gemeldet werden?", Options: []string{"24 Stunden", "48 Stunden", "72 Stunden", "7 Tage"}, CorrectOptionIndex: 2, Explanation: "Gemaess Art. 33 DSGVO muss eine Datenpanne innerhalb von 72 Stunden nach Bekanntwerden der Aufsichtsbehoerde gemeldet werden.", Order: 5, }, }, }, }, } } func (h *AcademyHandler) mockITSecurityCourse(req GenerateCourseRequest) AcademyCourse { return AcademyCourse{ Title: "IT-Sicherheit & Cybersecurity Awareness", Description: "Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern und Social Engineering.", DurationMinutes: 60, Lessons: []AcademyLesson{ {Title: "Phishing erkennen und vermeiden", Type: "text", Order: 1, DurationMinutes: 15, ContentMarkdown: "# Phishing erkennen\n\n## Typische Merkmale\n- Dringlichkeit ('Ihr Konto wird gesperrt!')\n- Unbekannter Absender\n- Verdaechtige Links\n- Rechtschreibfehler\n\n## Was tun bei Verdacht?\n1. Link NICHT anklicken\n2. Anhang NICHT oeffnen\n3. IT-Sicherheit informieren"}, {Title: "Sichere Passwoerter und MFA", Type: "text", Order: 2, DurationMinutes: 15, ContentMarkdown: "# Sichere Passwoerter\n\n## Regeln\n- Mindestens 12 Zeichen\n- Gross-/Kleinbuchstaben, Zahlen, Sonderzeichen\n- Fuer jeden Dienst ein eigenes Passwort\n- Passwort-Manager verwenden\n\n## Multi-Faktor-Authentifizierung\n- Immer aktivieren wenn moeglich\n- App-basiert (z.B. Microsoft Authenticator) bevorzugen"}, {Title: "Social Engineering", Type: "text", Order: 3, DurationMinutes: 15, ContentMarkdown: "# Social Engineering\n\nAngreifer nutzen menschliche Schwaechen aus.\n\n## Methoden\n- **Pretexting**: Falsche Identitaet vortaeuschen\n- **Tailgating**: Unbefugter Zutritt durch Hinterherfolgen\n- **CEO Fraud**: Gefaelschte Anweisungen vom Vorgesetzten\n\n## Schutz\n- Identitaet immer verifizieren\n- Bei Unsicherheit nachfragen"}, {Title: "Wissenstest: IT-Sicherheit", Type: "quiz", Order: 4, DurationMinutes: 15, QuizQuestions: []AcademyQuizQuestion{ {Question: "Was ist ein typisches Merkmal einer Phishing-E-Mail?", Options: []string{"Professionelles Design", "Kuenstliche Dringlichkeit", "Bekannter Absender", "Kurzer Text"}, CorrectOptionIndex: 1, Explanation: "Phishing-Mails erzeugen oft kuenstliche Dringlichkeit.", Order: 1}, {Question: "Wie lang sollte ein sicheres Passwort mindestens sein?", Options: []string{"6 Zeichen", "8 Zeichen", "10 Zeichen", "12 Zeichen"}, CorrectOptionIndex: 3, Explanation: "Mindestens 12 Zeichen werden empfohlen.", Order: 2}, {Question: "Was ist CEO Fraud?", Options: []string{"Hacker-Angriff auf Server", "Gefaelschte Anweisung vom Vorgesetzten", "Virus in E-Mail-Anhang", "DDoS-Attacke"}, CorrectOptionIndex: 1, Explanation: "CEO Fraud ist eine Social-Engineering-Methode mit gefaelschten Anweisungen.", Order: 3}, }}, }, } } func (h *AcademyHandler) mockAILiteracyCourse(req GenerateCourseRequest) AcademyCourse { return AcademyCourse{ Title: "AI Literacy - Sicherer Umgang mit KI", Description: "Grundlagen kuenstlicher Intelligenz, EU AI Act und verantwortungsvoller Einsatz von KI-Werkzeugen im Unternehmen.", DurationMinutes: 75, Lessons: []AcademyLesson{ {Title: "Was ist Kuenstliche Intelligenz?", Type: "text", Order: 1, DurationMinutes: 15, ContentMarkdown: "# Was ist KI?\n\nKuenstliche Intelligenz (KI) bezeichnet Systeme, die menschenaehnliche kognitive Faehigkeiten zeigen.\n\n## Arten von KI\n- **Machine Learning**: Lernt aus Daten\n- **Deep Learning**: Neuronale Netze\n- **Generative AI**: Erstellt neue Inhalte (Text, Bild)\n- **LLMs**: Large Language Models wie ChatGPT"}, {Title: "Der EU AI Act", Type: "text", Order: 2, DurationMinutes: 20, ContentMarkdown: "# EU AI Act\n\n## Risikoklassen\n- **Unakzeptabel**: Social Scoring, Manipulation\n- **Hochrisiko**: Bildung, HR, Kritische Infrastruktur\n- **Begrenzt**: Chatbots, Empfehlungssysteme\n- **Minimal**: Spam-Filter\n\n## Art. 4: AI Literacy Pflicht\nAlle Mitarbeiter, die KI-Systeme nutzen, muessen geschult werden."}, {Title: "KI sicher im Unternehmen nutzen", Type: "text", Order: 3, DurationMinutes: 20, ContentMarkdown: "# KI sicher nutzen\n\n## Dos\n- Ergebnisse immer pruefen\n- Keine vertraulichen Daten eingeben\n- Firmenpolicies beachten\n\n## Don'ts\n- Blindes Vertrauen in KI-Ergebnisse\n- Personenbezogene Daten in externe KI-Tools\n- KI-generierte Inhalte ohne Pruefung veroeffentlichen"}, {Title: "Wissenstest: AI Literacy", Type: "quiz", Order: 4, DurationMinutes: 20, QuizQuestions: []AcademyQuizQuestion{ {Question: "Was verlangt Art. 4 des EU AI Acts?", Options: []string{"Verbot aller KI-Systeme", "AI Literacy Schulung fuer KI-Nutzer", "Nur Open-Source KI erlaubt", "KI nur in der IT-Abteilung"}, CorrectOptionIndex: 1, Explanation: "Art. 4 EU AI Act fordert AI Literacy fuer alle Mitarbeiter, die KI-Systeme nutzen.", Order: 1}, {Question: "Duerfen vertrauliche Firmendaten in externe KI-Tools eingegeben werden?", Options: []string{"Ja, immer", "Nur in ChatGPT", "Nein, grundsaetzlich nicht", "Nur mit VPN"}, CorrectOptionIndex: 2, Explanation: "Vertrauliche Daten duerfen nicht in externe KI-Tools eingegeben werden.", Order: 2}, }}, }, } } func (h *AcademyHandler) mockWhistleblowerCourse(req GenerateCourseRequest) AcademyCourse { return AcademyCourse{ Title: "Hinweisgeberschutz (HinSchG)", Description: "Einführung in das Hinweisgeberschutzgesetz, interne Meldewege und Schutz von Whistleblowern.", DurationMinutes: 45, Lessons: []AcademyLesson{ {Title: "Das Hinweisgeberschutzgesetz", Type: "text", Order: 1, DurationMinutes: 15, ContentMarkdown: "# Hinweisgeberschutzgesetz (HinSchG)\n\nSeit Juli 2023 muessen Unternehmen ab 50 Mitarbeitern interne Meldestellen einrichten.\n\n## Was ist geschuetzt?\n- Meldungen ueber Rechtsverstoesse\n- Verstoesse gegen EU-Recht\n- Straftaten und Ordnungswidrigkeiten"}, {Title: "Interne Meldewege", Type: "text", Order: 2, DurationMinutes: 15, ContentMarkdown: "# Interne Meldewege\n\n## Wie melde ich einen Verstoss?\n1. **Interne Meldestelle** (bevorzugt)\n2. **Externe Meldestelle** (BfJ)\n3. **Offenlegung** (nur als letztes Mittel)\n\n## Schutz fuer Hinweisgeber\n- Kuendigungsschutz\n- Keine Benachteiligung\n- Vertraulichkeit"}, {Title: "Wissenstest: Hinweisgeberschutz", Type: "quiz", Order: 3, DurationMinutes: 15, QuizQuestions: []AcademyQuizQuestion{ {Question: "Ab wie vielen Mitarbeitern muessen Unternehmen eine Meldestelle einrichten?", Options: []string{"10", "25", "50", "250"}, CorrectOptionIndex: 2, Explanation: "Unternehmen ab 50 Beschaeftigten muessen eine interne Meldestelle einrichten.", Order: 1}, {Question: "Welche Meldung ist NICHT durch das HinSchG geschuetzt?", Options: []string{"Straftaten", "Verstoesse gegen EU-Recht", "Persoenliche Beschwerden ueber Kollegen", "Umweltverstoesse"}, CorrectOptionIndex: 2, Explanation: "Persoenliche Konflikte fallen nicht unter das HinSchG.", Order: 2}, }}, }, } }