package http import ( "encoding/json" "net/http" "strings" "go.b0esche.cloud/backend/internal/audit" "go.b0esche.cloud/backend/internal/auth" "go.b0esche.cloud/backend/internal/config" "go.b0esche.cloud/backend/internal/database" "go.b0esche.cloud/backend/internal/middleware" "go.b0esche.cloud/backend/internal/org" "go.b0esche.cloud/backend/internal/permission" "go.b0esche.cloud/backend/pkg/jwt" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler { r := chi.NewRouter() // Global middleware r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.RateLimit) // Health check r.Get("/health", healthHandler) // Auth routes (no auth required) r.Route("/auth", func(r chi.Router) { r.Get("/login", func(w http.ResponseWriter, req *http.Request) { authLoginHandler(w, req, authService) }) r.Get("/callback", func(w http.ResponseWriter, req *http.Request) { authCallbackHandler(w, req, cfg, authService, jwtManager, auditLogger, db) }) r.Post("/refresh", func(w http.ResponseWriter, req *http.Request) { refreshHandler(w, req, jwtManager, db) }) }) // Auth middleware for protected routes r.Use(middleware.Auth(jwtManager, db)) // Org routes r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) { listOrgsHandler(w, req, db, jwtManager) }) r.Post("/orgs", func(w http.ResponseWriter, req *http.Request) { createOrgHandler(w, req, db, auditLogger, jwtManager) }) // Org-scoped routes r.Route("/orgs/{orgId}", func(r chi.Router) { r.Use(middleware.Org(db, auditLogger)) // File routes r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files", func(w http.ResponseWriter, req *http.Request) { listFilesHandler(w, req) }) r.Route("/files/{fileId}", func(r chi.Router) { r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) { viewerHandler(w, req, db, auditLogger) }) r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Get("/edit", func(w http.ResponseWriter, req *http.Request) { editorHandler(w, req, db, auditLogger) }) r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Post("/annotations", func(w http.ResponseWriter, req *http.Request) { annotationsHandler(w, req, db, auditLogger) }) r.Get("/meta", func(w http.ResponseWriter, req *http.Request) { fileMetaHandler(w, req) }) }) r.Get("/activity", func(w http.ResponseWriter, req *http.Request) { activityHandler(w, req, db) }) r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/members", func(w http.ResponseWriter, req *http.Request) { listMembersHandler(w, req, db) }) r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Patch("/members/{userId}", func(w http.ResponseWriter, req *http.Request) { updateMemberRoleHandler(w, req, db, auditLogger) }) }) return r } func healthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func authLoginHandler(w http.ResponseWriter, r *http.Request, authService *auth.Service) { state, err := auth.GenerateState() if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } // TODO: Store state securely (e.g., in session or cache) url := authService.LoginURL(state) http.Redirect(w, r, url, http.StatusFound) } func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Config, authService *auth.Service, jwtManager *jwt.Manager, auditLogger *audit.Logger, db *database.DB) { code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") // TODO: Validate state user, session, err := authService.HandleCallback(r.Context(), code, state) if err != nil { auditLogger.Log(r.Context(), audit.Entry{ Action: "login", Success: false, Metadata: map[string]interface{}{"error": err.Error()}, }) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } // Get user orgs orgs, err := org.ResolveUserOrgs(r.Context(), db, user.ID) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } orgIDs := make([]string, len(orgs)) for i, o := range orgs { orgIDs[i] = o.ID.String() } token, err := jwtManager.Generate(user.Email, orgIDs, session.ID.String()) if err != nil { http.Error(w, "Token generation failed", http.StatusInternalServerError) return } auditLogger.Log(r.Context(), audit.Entry{ UserID: &user.ID, Action: "login", Success: true, }) w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"token": "` + token + `"}`)) } func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manager, db *database.DB) { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } userID, _ := uuid.Parse(claims.UserID) orgs, err := db.GetUserOrganizations(r.Context(), userID) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } orgIDs := make([]string, len(orgs)) for i, o := range orgs { orgIDs[i] = o.ID.String() } newToken, err := jwtManager.Generate(claims.UserID, orgIDs, session.ID.String()) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"token": "` + newToken + `"}`)) } func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } userID, _ := uuid.Parse(claims.UserID) orgs, err := org.ResolveUserOrgs(r.Context(), db, userID) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(orgs) } func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, jwtManager *jwt.Manager) { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } userID, _ := uuid.Parse(claims.UserID) var req struct { Name string `json:"name"` Slug string `json:"slug,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } org, err := org.CreateOrg(r.Context(), db, userID, req.Name, req.Slug) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } auditLogger.Log(r.Context(), audit.Entry{ UserID: &userID, OrgID: &org.ID, Action: "create_org", Success: true, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(org) } func listFilesHandler(w http.ResponseWriter, r *http.Request) { // Mock files files := []struct { Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` Size int `json:"size"` LastModified string `json:"lastModified"` }{ {"test.pdf", "/test.pdf", "file", 1234, "2023-01-01T00:00:00Z"}, {"folder", "/folder", "folder", 0, "2023-01-01T00:00:00Z"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(files) } func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) { userIDStr := r.Context().Value("user").(string) userID, _ := uuid.Parse(userIDStr) orgID := r.Context().Value("org").(uuid.UUID) fileId := chi.URLParam(r, "fileId") // Log activity db.LogActivity(r.Context(), userID, orgID, &fileId, "view_file", map[string]interface{}{}) session := struct { ViewUrl string `json:"viewUrl"` Capabilities struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` IsPdf bool `json:"isPdf"` } `json:"capabilities"` ExpiresAt string `json:"expiresAt"` }{ ViewUrl: "https://view.example.com/" + fileId, Capabilities: struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` IsPdf bool `json:"isPdf"` }{CanEdit: true, CanAnnotate: true, IsPdf: true}, ExpiresAt: "2023-01-01T01:00:00Z", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(session) } func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) { userIDStr := r.Context().Value("user").(string) userID, _ := uuid.Parse(userIDStr) orgID := r.Context().Value("org").(uuid.UUID) fileId := chi.URLParam(r, "fileId") // Log activity db.LogActivity(r.Context(), userID, orgID, &fileId, "edit_file", map[string]interface{}{}) session := struct { EditUrl string `json:"editUrl"` ReadOnly bool `json:"readOnly"` ExpiresAt string `json:"expiresAt"` }{ EditUrl: "https://edit.example.com/" + fileId, ReadOnly: false, ExpiresAt: "2023-01-01T01:00:00Z", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(session) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(session) } func annotationsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) { userIDStr := r.Context().Value("user").(string) userID, _ := uuid.Parse(userIDStr) orgID := r.Context().Value("org").(uuid.UUID) fileId := chi.URLParam(r, "fileId") // Parse payload var payload struct { Annotations []interface{} `json:"annotations"` BaseVersionId string `json:"baseVersionId"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } // Log activity auditLogger.Log(r.Context(), audit.Entry{ UserID: &userID, OrgID: &orgID, Resource: &fileId, Action: "annotate_pdf", Success: true, Metadata: map[string]interface{}{"count": len(payload.Annotations)}, }) w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status": "ok"}`)) } func activityHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { orgID := r.Context().Value("org").(uuid.UUID) activities, err := db.GetOrgActivities(r.Context(), orgID, 50) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(activities) } func listMembersHandler(w http.ResponseWriter, r *http.Request, db *database.DB) { orgID := r.Context().Value("org").(uuid.UUID) members, err := db.GetOrgMembers(r.Context(), orgID) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(members) } func updateMemberRoleHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) { orgID := r.Context().Value("org").(uuid.UUID) userIDStr := chi.URLParam(r, "userId") userID, err := uuid.Parse(userIDStr) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } var req struct { Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } if err := db.UpdateMemberRole(r.Context(), orgID, userID, req.Role); err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status": "ok"}`)) } func fileMetaHandler(w http.ResponseWriter, r *http.Request) { meta := struct { LastModified string `json:"lastModified"` LastModifiedBy string `json:"lastModifiedBy"` VersionCount int `json:"versionCount"` }{ LastModified: "2023-01-01T00:00:00Z", LastModifiedBy: "user@example.com", VersionCount: 1, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(meta) }