diff --git a/b0esche_cloud/lib/pages/home_page.dart b/b0esche_cloud/lib/pages/home_page.dart index 0b984c1..54b520d 100644 --- a/b0esche_cloud/lib/pages/home_page.dart +++ b/b0esche_cloud/lib/pages/home_page.dart @@ -402,7 +402,7 @@ class _HomePageState extends State with TickerProviderStateMixin { } return Column( children: [ - const SizedBox(height: 80), + const SizedBox(height: 16), _buildOrgRow(context), Expanded( child: _buildDrive( diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index d580dd4..c71920a 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -1383,8 +1383,38 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage") } - // Fallback to local storage (if implemented) - errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + // Fallback to local disk (used when WebDAV is not configured) + baseDir := filepath.Clean(filepath.Join("/tmp/uploads/orgs", orgID.String())) + rel := strings.TrimPrefix(filePath, "/") + localPath := filepath.Join(baseDir, rel) + // Prevent path traversal escaping the baseDir + if !strings.HasPrefix(localPath, baseDir+string(os.PathSeparator)) && localPath != baseDir { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } + + f, err := os.Open(localPath) + if err != nil { + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + defer f.Close() + info, _ := f.Stat() + fileName := path.Base(filePath) + contentType := "application/octet-stream" + if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { + contentType = "application/pdf" + } else if strings.HasSuffix(strings.ToLower(fileName), ".png") { + contentType = "image/png" + } else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") { + contentType = "image/jpeg" + } + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) + w.Header().Set("Content-Type", contentType) + if info != nil { + w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) + } + io.Copy(w, f) } // downloadUserFileHandler downloads a file from user's personal workspace @@ -1437,6 +1467,36 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage") } - // Fallback to local storage (if implemented) - errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + // Fallback to local disk (used when WebDAV is not configured) + baseDir := filepath.Clean(filepath.Join("/tmp/uploads/users", userID.String())) + rel := strings.TrimPrefix(filePath, "/") + localPath := filepath.Join(baseDir, rel) + // Prevent path traversal escaping the baseDir + if !strings.HasPrefix(localPath, baseDir+string(os.PathSeparator)) && localPath != baseDir { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest) + return + } + + f, err := os.Open(localPath) + if err != nil { + errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) + return + } + defer f.Close() + info, _ := f.Stat() + fileName := path.Base(filePath) + contentType := "application/octet-stream" + if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { + contentType = "application/pdf" + } else if strings.HasSuffix(strings.ToLower(fileName), ".png") { + contentType = "image/png" + } else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") { + contentType = "image/jpeg" + } + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) + w.Header().Set("Content-Type", contentType) + if info != nil { + w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) + } + io.Copy(w, f) } diff --git a/go_cloud/internal/org/org.go b/go_cloud/internal/org/org.go index 1c40892..4d180ea 100644 --- a/go_cloud/internal/org/org.go +++ b/go_cloud/internal/org/org.go @@ -2,10 +2,14 @@ package org import ( "context" + "fmt" + "regexp" + "strings" "go.b0esche.cloud/backend/internal/database" "github.com/google/uuid" + "github.com/jackc/pgconn" ) // ResolveUserOrgs returns the organizations a user belongs to @@ -24,17 +28,58 @@ func CheckMembership(ctx context.Context, db *database.DB, userID, orgID uuid.UU // CreateOrg creates a new organization and adds the user as owner func CreateOrg(ctx context.Context, db *database.DB, userID uuid.UUID, name, slug string) (*database.Organization, error) { - if slug == "" { - // Simple slug generation - slug = name // TODO: make URL safe + trimmedName := strings.TrimSpace(name) + if trimmedName == "" { + return nil, fmt.Errorf("organization name cannot be empty") + } + + baseSlug := slugify(slug) + if baseSlug == "" { + baseSlug = slugify(trimmedName) + } + if baseSlug == "" { + baseSlug = fmt.Sprintf("org-%s", uuid.NewString()[:8]) + } + + var org *database.Organization + var err error + // Try a handful of suffixes on unique constraint violation + for i := 0; i < 5; i++ { + candidate := baseSlug + if i > 0 { + candidate = fmt.Sprintf("%s-%d", baseSlug, i+1) + } + org, err = db.CreateOrg(ctx, trimmedName, candidate) + if err != nil { + if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23505" { + // Unique violation; try next suffix + continue + } + return nil, err + } + break } - org, err := db.CreateOrg(ctx, name, slug) if err != nil { return nil, err } - err = db.AddMembership(ctx, userID, org.ID, "owner") - if err != nil { + + if err = db.AddMembership(ctx, userID, org.ID, "owner"); err != nil { return nil, err } return org, nil } + +// slugify converts a string to a URL-safe slug with hyphens. +func slugify(s string) string { + lower := strings.ToLower(strings.TrimSpace(s)) + if lower == "" { + return "" + } + // Replace non-alphanumeric with hyphen + re := regexp.MustCompile(`[^a-z0-9]+`) + slug := re.ReplaceAllString(lower, "-") + slug = strings.Trim(slug, "-") + // Collapse multiple hyphens + slug = strings.ReplaceAll(slug, "--", "-") + return slug +}