Implement user provisioning for Nextcloud accounts and enhance WebDAV client handling
This commit is contained in:
BIN
go_cloud/api
BIN
go_cloud/api
Binary file not shown.
@@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -28,10 +29,55 @@ import (
|
|||||||
"go.b0esche.cloud/backend/internal/storage"
|
"go.b0esche.cloud/backend/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// getUserWebDAVClient gets or creates a user's Nextcloud account and returns a WebDAV client for them
|
||||||
|
func getUserWebDAVClient(ctx context.Context, db *database.DB, userID uuid.UUID, nextcloudBaseURL, adminUser, adminPass string) (*storage.WebDAVClient, error) {
|
||||||
|
var user struct {
|
||||||
|
NextcloudUsername string
|
||||||
|
NextcloudPassword string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.QueryRowContext(ctx,
|
||||||
|
"SELECT nextcloud_username, nextcloud_password, email FROM users WHERE id = $1",
|
||||||
|
userID).Scan(&user.NextcloudUsername, &user.NextcloudPassword, &user.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user doesn't have Nextcloud credentials, create them
|
||||||
|
if user.NextcloudUsername == "" || user.NextcloudPassword == "" {
|
||||||
|
// Use email prefix as username
|
||||||
|
ncUsername := strings.Split(user.Email, "@")[0]
|
||||||
|
ncPassword, err := storage.GenerateSecurePassword(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Nextcloud user account
|
||||||
|
err = storage.CreateNextcloudUser(nextcloudBaseURL, adminUser, adminPass, ncUsername, ncPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Nextcloud user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database with Nextcloud credentials
|
||||||
|
_, err = db.ExecContext(ctx,
|
||||||
|
"UPDATE users SET nextcloud_username = $1, nextcloud_password = $2 WHERE id = $3",
|
||||||
|
ncUsername, ncPassword, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update user credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.NextcloudUsername = ncUsername
|
||||||
|
user.NextcloudPassword = ncPassword
|
||||||
|
fmt.Printf("[AUTO-PROVISION] Created Nextcloud account for user %s: %s\n", userID, ncUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user-specific WebDAV client
|
||||||
|
return storage.NewUserWebDAVClient(nextcloudBaseURL, user.NextcloudUsername, user.NextcloudPassword), nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler {
|
func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
// optional WebDAV/Nextcloud client
|
|
||||||
storageClient := storage.NewWebDAVClient(cfg)
|
|
||||||
|
|
||||||
// Global middleware
|
// Global middleware
|
||||||
r.Use(middleware.RequestID)
|
r.Use(middleware.RequestID)
|
||||||
@@ -87,18 +133,18 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
|||||||
})
|
})
|
||||||
// Download user file
|
// Download user file
|
||||||
r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
|
r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
|
||||||
downloadUserFileHandler(w, req, db, storageClient)
|
downloadUserFileHandler(w, req, db, cfg)
|
||||||
})
|
})
|
||||||
// Create / delete in user workspace
|
// Create / delete in user workspace
|
||||||
r.Post("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
||||||
createUserFileHandler(w, req, db, auditLogger, storageClient)
|
createUserFileHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
r.Delete("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
r.Delete("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
||||||
deleteUserFileHandler(w, req, db, auditLogger, storageClient)
|
deleteUserFileHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
// POST wrapper for delete
|
// POST wrapper for delete
|
||||||
r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
||||||
deleteUserFilePostHandler(w, req, db, auditLogger, storageClient)
|
deleteUserFilePostHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Org routes
|
// Org routes
|
||||||
@@ -119,21 +165,21 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
|||||||
})
|
})
|
||||||
// Download org file
|
// Download org file
|
||||||
r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files/download", func(w http.ResponseWriter, req *http.Request) {
|
r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files/download", func(w http.ResponseWriter, req *http.Request) {
|
||||||
downloadOrgFileHandler(w, req, db, storageClient)
|
downloadOrgFileHandler(w, req, db, cfg)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create file/folder in org workspace
|
// Create file/folder in org workspace
|
||||||
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files", func(w http.ResponseWriter, req *http.Request) {
|
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files", func(w http.ResponseWriter, req *http.Request) {
|
||||||
createOrgFileHandler(w, req, db, auditLogger, storageClient)
|
createOrgFileHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
// Also accept POST delete for clients that cannot send DELETE with body
|
// Also accept POST delete for clients that cannot send DELETE with body
|
||||||
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
||||||
deleteOrgFilePostHandler(w, req, db, auditLogger, storageClient)
|
deleteOrgFilePostHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete file/folder in org workspace (body: {"path":"/path"})
|
// Delete file/folder in org workspace (body: {"path":"/path"})
|
||||||
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Delete("/files", func(w http.ResponseWriter, req *http.Request) {
|
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Delete("/files", func(w http.ResponseWriter, req *http.Request) {
|
||||||
deleteOrgFileHandler(w, req, db, auditLogger, storageClient)
|
deleteOrgFileHandler(w, req, db, auditLogger, cfg)
|
||||||
})
|
})
|
||||||
r.Route("/files/{fileId}", func(r chi.Router) {
|
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) {
|
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) {
|
||||||
@@ -1020,7 +1066,7 @@ func userFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createOrgFileHandler creates a file or folder record for an org workspace.
|
// createOrgFileHandler creates a file or folder record for an org workspace.
|
||||||
func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
orgID := r.Context().Value("org").(uuid.UUID)
|
orgID := r.Context().Value("org").(uuid.UUID)
|
||||||
userIDStr, _ := middleware.GetUserID(r.Context())
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
userID, _ := uuid.Parse(userIDStr)
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
@@ -1048,7 +1094,7 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Read file into memory (so we can attempt WebDAV upload and fallback to disk)
|
// Read file into memory
|
||||||
data, err := io.ReadAll(file)
|
data, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Failed to read uploaded file")
|
errors.LogError(r, err, "Failed to read uploaded file")
|
||||||
@@ -1056,19 +1102,21 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONLY use Nextcloud WebDAV storage
|
|
||||||
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
|
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
|
||||||
if !strings.HasPrefix(storedPath, "/") {
|
if !strings.HasPrefix(storedPath, "/") {
|
||||||
storedPath = "/" + storedPath
|
storedPath = "/" + storedPath
|
||||||
}
|
}
|
||||||
written := int64(len(data))
|
written := int64(len(data))
|
||||||
|
|
||||||
if storageClient == nil {
|
// Get or create user's WebDAV client
|
||||||
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get user WebDAV client")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build remote path under /orgs/<orgId>
|
// Upload to user's Nextcloud space under /orgs/<orgID>/
|
||||||
rel := strings.TrimPrefix(storedPath, "/")
|
rel := strings.TrimPrefix(storedPath, "/")
|
||||||
remotePath := path.Join("/orgs", orgID.String(), rel)
|
remotePath := path.Join("/orgs", orgID.String(), rel)
|
||||||
if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
|
if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
|
||||||
@@ -1125,7 +1173,7 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deleteOrgFileHandler deletes a file/folder in org workspace by path
|
// deleteOrgFileHandler deletes a file/folder in org workspace by path
|
||||||
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
orgID := r.Context().Value("org").(uuid.UUID)
|
orgID := r.Context().Value("org").(uuid.UUID)
|
||||||
userIDStr, _ := middleware.GetUserID(r.Context())
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
userID, _ := uuid.Parse(userIDStr)
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
@@ -1138,8 +1186,11 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete from Nextcloud if configured
|
// Get or create user's WebDAV client and delete from Nextcloud
|
||||||
if storageClient != nil {
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database deletion)")
|
||||||
|
} else {
|
||||||
rel := strings.TrimPrefix(req.Path, "/")
|
rel := strings.TrimPrefix(req.Path, "/")
|
||||||
remotePath := path.Join("/orgs", orgID.String(), rel)
|
remotePath := path.Join("/orgs", orgID.String(), rel)
|
||||||
if err := storageClient.Delete(r.Context(), remotePath); err != nil {
|
if err := storageClient.Delete(r.Context(), remotePath); err != nil {
|
||||||
@@ -1166,12 +1217,12 @@ func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Also accept POST /orgs/{orgId}/files/delete for clients that cannot send DELETE with body
|
// Also accept POST /orgs/{orgId}/files/delete for clients that cannot send DELETE with body
|
||||||
func deleteOrgFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func deleteOrgFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
deleteOrgFileHandler(w, r, db, auditLogger, storageClient)
|
deleteOrgFileHandler(w, r, db, auditLogger, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createUserFileHandler creates a file or folder record for the authenticated user's personal workspace.
|
// createUserFileHandler creates a file or folder record for the authenticated user's personal workspace.
|
||||||
func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
userIDStr, ok := middleware.GetUserID(r.Context())
|
userIDStr, ok := middleware.GetUserID(r.Context())
|
||||||
if !ok || userIDStr == "" {
|
if !ok || userIDStr == "" {
|
||||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||||
@@ -1199,7 +1250,7 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// Read file into memory to allow WebDAV upload and disk fallback
|
// Read file into memory to allow WebDAV upload
|
||||||
data, err := io.ReadAll(file)
|
data, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Failed to read uploaded file")
|
errors.LogError(r, err, "Failed to read uploaded file")
|
||||||
@@ -1213,16 +1264,18 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
written := int64(len(data))
|
written := int64(len(data))
|
||||||
fmt.Printf("[DEBUG] Upload: user=%s, file=%s, size=%d, path=%s\n", userID.String(), header.Filename, len(data), storedPath)
|
fmt.Printf("[DEBUG] Upload: user=%s, file=%s, size=%d, path=%s\n", userID.String(), header.Filename, len(data), storedPath)
|
||||||
|
|
||||||
// ONLY use Nextcloud WebDAV storage
|
// Get or create user's WebDAV client
|
||||||
if storageClient == nil {
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get user WebDAV client")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rel := strings.TrimPrefix(storedPath, "/")
|
// Upload to user's personal Nextcloud space (just the path, no username prefix)
|
||||||
remotePath := path.Join("/users", userID.String(), rel)
|
remotePath := strings.TrimPrefix(storedPath, "/")
|
||||||
fmt.Printf("[DEBUG] Uploading to WebDAV: %s\n", remotePath)
|
fmt.Printf("[DEBUG] Uploading to user WebDAV: /%s\n", remotePath)
|
||||||
if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
|
if err = storageClient.Upload(r.Context(), "/"+remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
|
||||||
errors.LogError(r, err, "WebDAV upload failed")
|
errors.LogError(r, err, "WebDAV upload failed")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
|
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -1275,12 +1328,12 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Also accept POST /user/files/delete
|
// Also accept POST /user/files/delete
|
||||||
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
deleteUserFileHandler(w, r, db, auditLogger, storageClient)
|
deleteUserFileHandler(w, r, db, auditLogger, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteUserFileHandler deletes a file/folder in user's personal workspace by path
|
// deleteUserFileHandler deletes a file/folder in user's personal workspace by path
|
||||||
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, storageClient *storage.WebDAVClient) {
|
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
|
||||||
userIDStr, ok := middleware.GetUserID(r.Context())
|
userIDStr, ok := middleware.GetUserID(r.Context())
|
||||||
if !ok || userIDStr == "" {
|
if !ok || userIDStr == "" {
|
||||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||||
@@ -1296,12 +1349,13 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete from Nextcloud if configured
|
// Get or create user's WebDAV client and delete from Nextcloud
|
||||||
if storageClient != nil {
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
rel := strings.TrimPrefix(req.Path, "/")
|
if err != nil {
|
||||||
// Keep remote user workspace path consistent with uploads: "/users/<userID>/<rel>"
|
errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database deletion)")
|
||||||
remotePath := path.Join("/users", userID.String(), rel)
|
} else {
|
||||||
if err := storageClient.Delete(r.Context(), remotePath); err != nil {
|
remotePath := strings.TrimPrefix(req.Path, "/")
|
||||||
|
if err := storageClient.Delete(r.Context(), "/"+remotePath); err != nil {
|
||||||
errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
|
errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1324,8 +1378,10 @@ func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// downloadOrgFileHandler downloads a file from org workspace
|
// downloadOrgFileHandler downloads a file from org workspace
|
||||||
func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, storageClient *storage.WebDAVClient) {
|
func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) {
|
||||||
orgID := r.Context().Value("org").(uuid.UUID)
|
orgID := r.Context().Value("org").(uuid.UUID)
|
||||||
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
|
|
||||||
filePath := r.URL.Query().Get("path")
|
filePath := r.URL.Query().Get("path")
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
@@ -1333,15 +1389,17 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONLY use Nextcloud WebDAV storage
|
// Get or create user's WebDAV client
|
||||||
if storageClient == nil {
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get user WebDAV client")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download from user's Nextcloud space under /orgs/<orgID>/
|
||||||
rel := strings.TrimPrefix(filePath, "/")
|
rel := strings.TrimPrefix(filePath, "/")
|
||||||
remotePath := path.Join("/orgs", orgID.String(), rel)
|
remotePath := path.Join("/orgs", orgID.String(), rel)
|
||||||
|
|
||||||
reader, size, err := storageClient.Download(r.Context(), remotePath)
|
reader, size, err := storageClient.Download(r.Context(), remotePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Failed to download from Nextcloud")
|
errors.LogError(r, err, "Failed to download from Nextcloud")
|
||||||
@@ -1373,7 +1431,7 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
}
|
}
|
||||||
|
|
||||||
// downloadUserFileHandler downloads a file from user's personal workspace
|
// downloadUserFileHandler downloads a file from user's personal workspace
|
||||||
func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, storageClient *storage.WebDAVClient) {
|
func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) {
|
||||||
userIDStr, ok := middleware.GetUserID(r.Context())
|
userIDStr, ok := middleware.GetUserID(r.Context())
|
||||||
if !ok || userIDStr == "" {
|
if !ok || userIDStr == "" {
|
||||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||||
@@ -1390,18 +1448,19 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas
|
|||||||
// Log the requested file path for debugging
|
// Log the requested file path for debugging
|
||||||
fmt.Printf("[DEBUG] Download request - User: %s, Path: %s\n", userID.String(), filePath)
|
fmt.Printf("[DEBUG] Download request - User: %s, Path: %s\n", userID.String(), filePath)
|
||||||
|
|
||||||
// ONLY use Nextcloud WebDAV storage
|
// Get or create user's WebDAV client
|
||||||
if storageClient == nil {
|
storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get user WebDAV client")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rel := strings.TrimPrefix(filePath, "/")
|
// Download from user's personal Nextcloud space
|
||||||
// Keep remote user workspace path consistent with uploads: "/users/<userID>/<rel>"
|
remotePath := strings.TrimPrefix(filePath, "/")
|
||||||
remotePath := path.Join("/users", userID.String(), rel)
|
fmt.Printf("[DEBUG] Downloading from user WebDAV: /%s\n", remotePath)
|
||||||
fmt.Printf("[DEBUG] Trying WebDAV path: %s\n", remotePath)
|
|
||||||
|
|
||||||
reader, size, err := storageClient.Download(r.Context(), remotePath)
|
reader, size, err := storageClient.Download(r.Context(), "/"+remotePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Failed to download from Nextcloud")
|
errors.LogError(r, err, "Failed to download from Nextcloud")
|
||||||
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||||
|
|||||||
77
go_cloud/internal/storage/nextcloud.go
Normal file
77
go_cloud/internal/storage/nextcloud.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateNextcloudUser creates a new Nextcloud user account via OCS API
|
||||||
|
func CreateNextcloudUser(nextcloudBaseURL, adminUser, adminPass, username, password string) error {
|
||||||
|
// Remove any path from base URL, we need just the scheme://host:port
|
||||||
|
baseURL := strings.Split(nextcloudBaseURL, "/remote.php")[0]
|
||||||
|
url := fmt.Sprintf("%s/ocs/v1.php/cloud/users", baseURL)
|
||||||
|
|
||||||
|
payload := map[string]string{
|
||||||
|
"userid": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetBasicAuth(adminUser, adminPass)
|
||||||
|
req.Header.Set("OCS-APIRequest", "true")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
// 200 = success, 409 = user already exists (which is fine)
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 409 {
|
||||||
|
return fmt.Errorf("failed to create Nextcloud user (status %d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[NEXTCLOUD] Created user account: %s\n", username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSecurePassword generates a random secure password
|
||||||
|
func GenerateSecurePassword(length int) (string, error) {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(bytes)[:length], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserWebDAVClient creates a WebDAV client for a specific user
|
||||||
|
func NewUserWebDAVClient(nextcloudBaseURL, username, password string) *WebDAVClient {
|
||||||
|
baseURL := fmt.Sprintf("%s/remote.php/dav/files/%s", nextcloudBaseURL, username)
|
||||||
|
|
||||||
|
return &WebDAVClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
user: username,
|
||||||
|
pass: password,
|
||||||
|
basePrefix: "/",
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user