From b381a46483d160fb42c418f53e574ad41aa63cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Sat, 10 Jan 2026 04:48:28 +0100 Subject: [PATCH] Refactor authentication handling in HTTP routes to utilize middleware for user ID extraction and improve download URL encoding --- b0esche_cloud/lib/main.dart | 14 ++++-- go_cloud/internal/http/routes.go | 74 +++++++++++++++++++------------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/b0esche_cloud/lib/main.dart b/b0esche_cloud/lib/main.dart index 73545d0..b602472 100644 --- a/b0esche_cloud/lib/main.dart +++ b/b0esche_cloud/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'blocs/auth/auth_bloc.dart'; +import 'blocs/auth/auth_event.dart'; import 'blocs/session/session_bloc.dart'; import 'blocs/activity/activity_bloc.dart'; import 'services/api_client.dart'; @@ -57,15 +58,22 @@ class _MainAppState extends State { @override void initState() { super.initState(); - // Restore session from persistent storage early so ApiClient has token if present - _restoreFuture = SessionBloc.restoreSession(_sessionBloc); - // Configure DI to use HTTP repositories + // Configure DI first configureDependencies(_sessionBloc); + // Create AuthBloc first _authBloc = AuthBloc( apiClient: ApiClient(_sessionBloc), sessionBloc: _sessionBloc, ); + + // Restore session and then check auth + _restoreFuture = SessionBloc.restoreSession(_sessionBloc).then((_) { + // After session is restored, check if we should auto-authenticate + if (mounted) { + _authBloc.add(const CheckAuthRequested()); + } + }); } @override diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index c71920a..f33b77d 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -7,6 +7,7 @@ import ( "io" "mime/multipart" "net/http" + "net/url" "os" "path" "path/filepath" @@ -243,21 +244,14 @@ func logoutHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manag } func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) { - authHeader := r.Header.Get("Authorization") - if !strings.HasPrefix(authHeader, "Bearer ") { - errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) - return - } - tokenString := strings.TrimPrefix(authHeader, "Bearer ") - - claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db) - if err != nil { - errors.LogError(r, err, "Invalid token") + // User ID is already set by Auth middleware + userIDStr, ok := middleware.GetUserID(r.Context()) + if !ok { errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) return } - userID, _ := uuid.Parse(claims.UserID) + userID, _ := uuid.Parse(userIDStr) orgs, err := org.ResolveUserOrgs(r.Context(), db, userID) if err != nil { errors.LogError(r, err, "Failed to resolve user orgs") @@ -270,21 +264,14 @@ func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jw } 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 ") { - errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) - return - } - tokenString := strings.TrimPrefix(authHeader, "Bearer ") - - claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db) - if err != nil { - errors.LogError(r, err, "Invalid token") + // User ID is already set by Auth middleware + userIDStr, ok := middleware.GetUserID(r.Context()) + if !ok { errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) return } - userID, _ := uuid.Parse(claims.UserID) + userID, _ := uuid.Parse(userIDStr) var req struct { Name string `json:"name"` @@ -378,14 +365,15 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi // Log activity db.LogActivity(r.Context(), userID, orgID, &fileId, "view_file", map[string]interface{}{}) - // Build download URL - using full path for frontend to fetch with auth headers - viewUrl := fmt.Sprintf("https://go.b0esche.cloud/orgs/%s/files/download?path=%s", orgID.String(), file.Path) + // Build download URL with proper URL encoding + downloadPath := fmt.Sprintf("https://go.b0esche.cloud/orgs/%s/files/download?path=%s", orgID.String(), url.QueryEscape(file.Path)) // Determine if it's a PDF based on file extension isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf") session := struct { ViewUrl string `json:"viewUrl"` + Token string `json:"token"` Capabilities struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` @@ -393,7 +381,8 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi } `json:"capabilities"` ExpiresAt string `json:"expiresAt"` }{ - ViewUrl: viewUrl, + ViewUrl: downloadPath, + Token: userIDStr, // Session token - user is already authenticated via middleware Capabilities: struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` @@ -429,14 +418,15 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, // Optionally log activity without org id db.LogActivity(r.Context(), userID, uuid.Nil, &fileId, "view_user_file", map[string]interface{}{}) - // Build download URL for user files - using full path for frontend to fetch with auth headers - viewUrl := fmt.Sprintf("https://go.b0esche.cloud/user/files/download?path=%s", file.Path) + // Build download URL with proper URL encoding + downloadPath := fmt.Sprintf("https://go.b0esche.cloud/user/files/download?path=%s", url.QueryEscape(file.Path)) // Determine if it's a PDF based on file extension isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf") session := struct { ViewUrl string `json:"viewUrl"` + Token string `json:"token"` Capabilities struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` @@ -444,7 +434,8 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, } `json:"capabilities"` ExpiresAt string `json:"expiresAt"` }{ - ViewUrl: viewUrl, + ViewUrl: downloadPath, + Token: userIDStr, // Session token - user is already authenticated via middleware Capabilities: struct { CanEdit bool `json:"canEdit"` CanAnnotate bool `json:"canAnnotate"` @@ -467,17 +458,38 @@ func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi orgID := r.Context().Value("org").(uuid.UUID) fileId := chi.URLParam(r, "fileId") + // Get file metadata to determine path and type + fileUUID, err := uuid.Parse(fileId) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest) + return + } + + file, err := db.GetFileByID(r.Context(), fileUUID) + if err != nil { + errors.LogError(r, err, "Failed to get file metadata") + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + // Log activity db.LogActivity(r.Context(), userID, orgID, &fileId, "edit_file", map[string]interface{}{}) + // Build Collabora editor URL - Collabora needs the file download URL as the WOPISrc parameter + editUrl := fmt.Sprintf("https://go.b0esche.cloud/orgs/%s/files/download?path=%s", orgID.String(), url.QueryEscape(file.Path)) + collaboraUrl := fmt.Sprintf("https://collabora.b0esche.cloud/lool/dist/mobile/cool.html?WOPISrc=%s", url.QueryEscape(editUrl)) + + // Check if user can edit (for now, all org members can edit) + readOnly := false + 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", + EditUrl: collaboraUrl, + ReadOnly: readOnly, + ExpiresAt: time.Now().Add(15 * time.Minute).UTC().Format(time.RFC3339), } w.Header().Set("Content-Type", "application/json")