[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,17 +2,10 @@ package jitsi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// JitsiService handles Jitsi Meet integration for video conferences
|
||||
@@ -292,275 +285,3 @@ func (s *JitsiService) CreateClassMeeting(ctx context.Context, className string,
|
||||
return s.CreateMeetingLink(ctx, meeting)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// JWT Generation
|
||||
// ========================================
|
||||
|
||||
// generateJWT creates a signed JWT for Jitsi authentication
|
||||
func (s *JitsiService) generateJWT(meeting Meeting, roomName string) (string, *time.Time, error) {
|
||||
if s.appSecret == "" {
|
||||
return "", nil, fmt.Errorf("app secret not configured")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Default expiration: 24 hours or based on meeting duration
|
||||
expiration := now.Add(24 * time.Hour)
|
||||
if meeting.Duration > 0 {
|
||||
expiration = now.Add(time.Duration(meeting.Duration+30) * time.Minute)
|
||||
}
|
||||
if meeting.StartTime != nil {
|
||||
expiration = meeting.StartTime.Add(time.Duration(meeting.Duration+60) * time.Minute)
|
||||
}
|
||||
|
||||
claims := JWTClaims{
|
||||
Audience: "jitsi",
|
||||
Issuer: s.appID,
|
||||
Subject: "meet.jitsi",
|
||||
Room: roomName,
|
||||
ExpiresAt: expiration.Unix(),
|
||||
NotBefore: now.Add(-5 * time.Minute).Unix(), // 5 min grace period
|
||||
Moderator: meeting.Moderator,
|
||||
Context: &JWTContext{
|
||||
User: &JWTUser{
|
||||
ID: uuid.New().String(),
|
||||
Name: meeting.DisplayName,
|
||||
Email: meeting.Email,
|
||||
Avatar: meeting.Avatar,
|
||||
Moderator: meeting.Moderator,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add features if specified
|
||||
if meeting.Features != nil {
|
||||
claims.Features = &JWTFeatures{
|
||||
Recording: boolToString(meeting.Features.Recording),
|
||||
Livestreaming: boolToString(meeting.Features.Livestreaming),
|
||||
Transcription: boolToString(meeting.Features.Transcription),
|
||||
OutboundCall: boolToString(meeting.Features.OutboundCall),
|
||||
}
|
||||
}
|
||||
|
||||
// Create JWT
|
||||
token, err := s.signJWT(claims)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return token, &expiration, nil
|
||||
}
|
||||
|
||||
// signJWT creates and signs a JWT token
|
||||
func (s *JitsiService) signJWT(claims JWTClaims) (string, error) {
|
||||
// Header
|
||||
header := map[string]string{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT",
|
||||
}
|
||||
|
||||
headerJSON, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Payload
|
||||
payloadJSON, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encode
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
|
||||
|
||||
// Sign
|
||||
message := headerB64 + "." + payloadB64
|
||||
h := hmac.New(sha256.New, []byte(s.appSecret))
|
||||
h.Write([]byte(message))
|
||||
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return message + "." + signature, nil
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Health Check
|
||||
// ========================================
|
||||
|
||||
// HealthCheck verifies the Jitsi server is accessible
|
||||
func (s *JitsiService) HealthCheck(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", s.baseURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jitsi server unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 500 {
|
||||
return fmt.Errorf("jitsi server error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServerInfo returns information about the Jitsi server
|
||||
func (s *JitsiService) GetServerInfo() map[string]string {
|
||||
return map[string]string{
|
||||
"base_url": s.baseURL,
|
||||
"app_id": s.appID,
|
||||
"auth_enabled": boolToString(s.appSecret != ""),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// URL Building
|
||||
// ========================================
|
||||
|
||||
// BuildEmbedURL creates an embeddable iframe URL
|
||||
func (s *JitsiService) BuildEmbedURL(roomName string, displayName string, config *MeetingConfig) string {
|
||||
params := url.Values{}
|
||||
|
||||
if displayName != "" {
|
||||
params.Set("userInfo.displayName", displayName)
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
if config.StartWithAudioMuted {
|
||||
params.Set("config.startWithAudioMuted", "true")
|
||||
}
|
||||
if config.StartWithVideoMuted {
|
||||
params.Set("config.startWithVideoMuted", "true")
|
||||
}
|
||||
if config.DisableDeepLinking {
|
||||
params.Set("config.disableDeepLinking", "true")
|
||||
}
|
||||
}
|
||||
|
||||
embedURL := fmt.Sprintf("%s/%s", s.baseURL, s.sanitizeRoomName(roomName))
|
||||
if len(params) > 0 {
|
||||
embedURL += "#" + params.Encode()
|
||||
}
|
||||
|
||||
return embedURL
|
||||
}
|
||||
|
||||
// BuildIFrameCode generates HTML iframe code for embedding
|
||||
func (s *JitsiService) BuildIFrameCode(roomName string, width int, height int) string {
|
||||
if width == 0 {
|
||||
width = 800
|
||||
}
|
||||
if height == 0 {
|
||||
height = 600
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`<iframe src="%s/%s" width="%d" height="%d" allow="camera; microphone; fullscreen; display-capture; autoplay" style="border: 0;"></iframe>`,
|
||||
s.baseURL,
|
||||
s.sanitizeRoomName(roomName),
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Helper Functions
|
||||
// ========================================
|
||||
|
||||
// generateRoomName creates a unique room name
|
||||
func (s *JitsiService) generateRoomName() string {
|
||||
return fmt.Sprintf("breakpilot-%s", uuid.New().String()[:8])
|
||||
}
|
||||
|
||||
// generateTrainingRoomName creates a room name for training sessions
|
||||
func (s *JitsiService) generateTrainingRoomName(title string) string {
|
||||
sanitized := s.sanitizeRoomName(title)
|
||||
if sanitized == "" {
|
||||
sanitized = "schulung"
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", sanitized, time.Now().Format("20060102-1504"))
|
||||
}
|
||||
|
||||
// sanitizeRoomName removes invalid characters from room names
|
||||
func (s *JitsiService) sanitizeRoomName(name string) string {
|
||||
// Replace spaces and special characters
|
||||
result := strings.ToLower(name)
|
||||
result = strings.ReplaceAll(result, " ", "-")
|
||||
result = strings.ReplaceAll(result, "ä", "ae")
|
||||
result = strings.ReplaceAll(result, "ö", "oe")
|
||||
result = strings.ReplaceAll(result, "ü", "ue")
|
||||
result = strings.ReplaceAll(result, "ß", "ss")
|
||||
|
||||
// Remove any remaining non-alphanumeric characters except hyphen
|
||||
var cleaned strings.Builder
|
||||
for _, r := range result {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
cleaned.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove consecutive hyphens
|
||||
result = cleaned.String()
|
||||
for strings.Contains(result, "--") {
|
||||
result = strings.ReplaceAll(result, "--", "-")
|
||||
}
|
||||
|
||||
// Trim hyphens from start and end
|
||||
result = strings.Trim(result, "-")
|
||||
|
||||
// Limit length
|
||||
if len(result) > 50 {
|
||||
result = result[:50]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// generatePassword creates a random meeting password
|
||||
func (s *JitsiService) generatePassword() string {
|
||||
return uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
// buildConfigParams creates URL parameters from config
|
||||
func (s *JitsiService) buildConfigParams(config *MeetingConfig) string {
|
||||
params := url.Values{}
|
||||
|
||||
if config.StartWithAudioMuted {
|
||||
params.Set("config.startWithAudioMuted", "true")
|
||||
}
|
||||
if config.StartWithVideoMuted {
|
||||
params.Set("config.startWithVideoMuted", "true")
|
||||
}
|
||||
if config.DisableDeepLinking {
|
||||
params.Set("config.disableDeepLinking", "true")
|
||||
}
|
||||
if config.RequireDisplayName {
|
||||
params.Set("config.requireDisplayName", "true")
|
||||
}
|
||||
if config.EnableLobby {
|
||||
params.Set("config.enableLobby", "true")
|
||||
}
|
||||
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
// boolToString converts bool to "true"/"false" string
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// GetBaseURL returns the configured base URL
|
||||
func (s *JitsiService) GetBaseURL() string {
|
||||
return s.baseURL
|
||||
}
|
||||
|
||||
// IsAuthEnabled returns whether JWT authentication is configured
|
||||
func (s *JitsiService) IsAuthEnabled() bool {
|
||||
return s.appSecret != ""
|
||||
}
|
||||
|
||||
290
consent-service/internal/services/jitsi/jitsi_service_helpers.go
Normal file
290
consent-service/internal/services/jitsi/jitsi_service_helpers.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package jitsi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ========================================
|
||||
// JWT Generation
|
||||
// ========================================
|
||||
|
||||
// generateJWT creates a signed JWT for Jitsi authentication
|
||||
func (s *JitsiService) generateJWT(meeting Meeting, roomName string) (string, *time.Time, error) {
|
||||
if s.appSecret == "" {
|
||||
return "", nil, fmt.Errorf("app secret not configured")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Default expiration: 24 hours or based on meeting duration
|
||||
expiration := now.Add(24 * time.Hour)
|
||||
if meeting.Duration > 0 {
|
||||
expiration = now.Add(time.Duration(meeting.Duration+30) * time.Minute)
|
||||
}
|
||||
if meeting.StartTime != nil {
|
||||
expiration = meeting.StartTime.Add(time.Duration(meeting.Duration+60) * time.Minute)
|
||||
}
|
||||
|
||||
claims := JWTClaims{
|
||||
Audience: "jitsi",
|
||||
Issuer: s.appID,
|
||||
Subject: "meet.jitsi",
|
||||
Room: roomName,
|
||||
ExpiresAt: expiration.Unix(),
|
||||
NotBefore: now.Add(-5 * time.Minute).Unix(), // 5 min grace period
|
||||
Moderator: meeting.Moderator,
|
||||
Context: &JWTContext{
|
||||
User: &JWTUser{
|
||||
ID: uuid.New().String(),
|
||||
Name: meeting.DisplayName,
|
||||
Email: meeting.Email,
|
||||
Avatar: meeting.Avatar,
|
||||
Moderator: meeting.Moderator,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add features if specified
|
||||
if meeting.Features != nil {
|
||||
claims.Features = &JWTFeatures{
|
||||
Recording: boolToString(meeting.Features.Recording),
|
||||
Livestreaming: boolToString(meeting.Features.Livestreaming),
|
||||
Transcription: boolToString(meeting.Features.Transcription),
|
||||
OutboundCall: boolToString(meeting.Features.OutboundCall),
|
||||
}
|
||||
}
|
||||
|
||||
// Create JWT
|
||||
token, err := s.signJWT(claims)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return token, &expiration, nil
|
||||
}
|
||||
|
||||
// signJWT creates and signs a JWT token
|
||||
func (s *JitsiService) signJWT(claims JWTClaims) (string, error) {
|
||||
// Header
|
||||
header := map[string]string{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT",
|
||||
}
|
||||
|
||||
headerJSON, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Payload
|
||||
payloadJSON, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encode
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
|
||||
|
||||
// Sign
|
||||
message := headerB64 + "." + payloadB64
|
||||
h := hmac.New(sha256.New, []byte(s.appSecret))
|
||||
h.Write([]byte(message))
|
||||
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return message + "." + signature, nil
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Health Check
|
||||
// ========================================
|
||||
|
||||
// HealthCheck verifies the Jitsi server is accessible
|
||||
func (s *JitsiService) HealthCheck(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", s.baseURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jitsi server unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 500 {
|
||||
return fmt.Errorf("jitsi server error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServerInfo returns information about the Jitsi server
|
||||
func (s *JitsiService) GetServerInfo() map[string]string {
|
||||
return map[string]string{
|
||||
"base_url": s.baseURL,
|
||||
"app_id": s.appID,
|
||||
"auth_enabled": boolToString(s.appSecret != ""),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// URL Building
|
||||
// ========================================
|
||||
|
||||
// BuildEmbedURL creates an embeddable iframe URL
|
||||
func (s *JitsiService) BuildEmbedURL(roomName string, displayName string, config *MeetingConfig) string {
|
||||
params := url.Values{}
|
||||
|
||||
if displayName != "" {
|
||||
params.Set("userInfo.displayName", displayName)
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
if config.StartWithAudioMuted {
|
||||
params.Set("config.startWithAudioMuted", "true")
|
||||
}
|
||||
if config.StartWithVideoMuted {
|
||||
params.Set("config.startWithVideoMuted", "true")
|
||||
}
|
||||
if config.DisableDeepLinking {
|
||||
params.Set("config.disableDeepLinking", "true")
|
||||
}
|
||||
}
|
||||
|
||||
embedURL := fmt.Sprintf("%s/%s", s.baseURL, s.sanitizeRoomName(roomName))
|
||||
if len(params) > 0 {
|
||||
embedURL += "#" + params.Encode()
|
||||
}
|
||||
|
||||
return embedURL
|
||||
}
|
||||
|
||||
// BuildIFrameCode generates HTML iframe code for embedding
|
||||
func (s *JitsiService) BuildIFrameCode(roomName string, width int, height int) string {
|
||||
if width == 0 {
|
||||
width = 800
|
||||
}
|
||||
if height == 0 {
|
||||
height = 600
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`<iframe src="%s/%s" width="%d" height="%d" allow="camera; microphone; fullscreen; display-capture; autoplay" style="border: 0;"></iframe>`,
|
||||
s.baseURL,
|
||||
s.sanitizeRoomName(roomName),
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Helper Functions
|
||||
// ========================================
|
||||
|
||||
// generateRoomName creates a unique room name
|
||||
func (s *JitsiService) generateRoomName() string {
|
||||
return fmt.Sprintf("breakpilot-%s", uuid.New().String()[:8])
|
||||
}
|
||||
|
||||
// generateTrainingRoomName creates a room name for training sessions
|
||||
func (s *JitsiService) generateTrainingRoomName(title string) string {
|
||||
sanitized := s.sanitizeRoomName(title)
|
||||
if sanitized == "" {
|
||||
sanitized = "schulung"
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", sanitized, time.Now().Format("20060102-1504"))
|
||||
}
|
||||
|
||||
// sanitizeRoomName removes invalid characters from room names
|
||||
func (s *JitsiService) sanitizeRoomName(name string) string {
|
||||
// Replace spaces and special characters
|
||||
result := strings.ToLower(name)
|
||||
result = strings.ReplaceAll(result, " ", "-")
|
||||
result = strings.ReplaceAll(result, "ä", "ae")
|
||||
result = strings.ReplaceAll(result, "ö", "oe")
|
||||
result = strings.ReplaceAll(result, "ü", "ue")
|
||||
result = strings.ReplaceAll(result, "ß", "ss")
|
||||
|
||||
// Remove any remaining non-alphanumeric characters except hyphen
|
||||
var cleaned strings.Builder
|
||||
for _, r := range result {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
cleaned.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove consecutive hyphens
|
||||
result = cleaned.String()
|
||||
for strings.Contains(result, "--") {
|
||||
result = strings.ReplaceAll(result, "--", "-")
|
||||
}
|
||||
|
||||
// Trim hyphens from start and end
|
||||
result = strings.Trim(result, "-")
|
||||
|
||||
// Limit length
|
||||
if len(result) > 50 {
|
||||
result = result[:50]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// generatePassword creates a random meeting password
|
||||
func (s *JitsiService) generatePassword() string {
|
||||
return uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
// buildConfigParams creates URL parameters from config
|
||||
func (s *JitsiService) buildConfigParams(config *MeetingConfig) string {
|
||||
params := url.Values{}
|
||||
|
||||
if config.StartWithAudioMuted {
|
||||
params.Set("config.startWithAudioMuted", "true")
|
||||
}
|
||||
if config.StartWithVideoMuted {
|
||||
params.Set("config.startWithVideoMuted", "true")
|
||||
}
|
||||
if config.DisableDeepLinking {
|
||||
params.Set("config.disableDeepLinking", "true")
|
||||
}
|
||||
if config.RequireDisplayName {
|
||||
params.Set("config.requireDisplayName", "true")
|
||||
}
|
||||
if config.EnableLobby {
|
||||
params.Set("config.enableLobby", "true")
|
||||
}
|
||||
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
// boolToString converts bool to "true"/"false" string
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// GetBaseURL returns the configured base URL
|
||||
func (s *JitsiService) GetBaseURL() string {
|
||||
return s.baseURL
|
||||
}
|
||||
|
||||
// IsAuthEnabled returns whether JWT authentication is configured
|
||||
func (s *JitsiService) IsAuthEnabled() bool {
|
||||
return s.appSecret != ""
|
||||
}
|
||||
Reference in New Issue
Block a user