idle
This commit is contained in:
@@ -78,6 +78,18 @@ type Activity struct {
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID uuid.UUID
|
||||
OrgID *uuid.UUID
|
||||
UserID *uuid.UUID
|
||||
Name string
|
||||
Path string
|
||||
Type string
|
||||
Size int64
|
||||
LastModified time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (db *DB) GetOrCreateUser(ctx context.Context, sub, email, name string) (*User, error) {
|
||||
var user User
|
||||
err := db.QueryRowContext(ctx, `
|
||||
@@ -233,6 +245,140 @@ func (db *DB) GetOrgMembers(ctx context.Context, orgID uuid.UUID) ([]Membership,
|
||||
return memberships, rows.Err()
|
||||
}
|
||||
|
||||
// GetOrgFiles returns files for a given organization (top-level folder listing)
|
||||
func (db *DB) GetOrgFiles(ctx context.Context, orgID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 100
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// Basic search and pagination. Returns files under the given path (including nested).
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
|
||||
FROM files
|
||||
WHERE org_id = $1 AND path != $2 AND path LIKE $2 || '/%'
|
||||
AND ($4 = '' OR name ILIKE '%' || $4 || '%')
|
||||
ORDER BY name
|
||||
LIMIT $5 OFFSET $6
|
||||
`, orgID, path, path, q, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var files []File
|
||||
for rows.Next() {
|
||||
var f File
|
||||
var orgNull sql.NullString
|
||||
var userNull sql.NullString
|
||||
if err := rows.Scan(&f.ID, &orgNull, &userNull, &f.Name, &f.Path, &f.Type, &f.Size, &f.LastModified, &f.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orgNull.Valid {
|
||||
oid, _ := uuid.Parse(orgNull.String)
|
||||
f.OrgID = &oid
|
||||
}
|
||||
if userNull.Valid {
|
||||
uid, _ := uuid.Parse(userNull.String)
|
||||
f.UserID = &uid
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, rows.Err()
|
||||
}
|
||||
|
||||
// GetUserFiles returns files for a user's personal workspace at a given path
|
||||
func (db *DB) GetUserFiles(ctx context.Context, userID uuid.UUID, path string, q string, page, pageSize int) ([]File, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 100
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
|
||||
FROM files
|
||||
WHERE user_id = $1 AND path != $2 AND path LIKE $2 || '/%'
|
||||
AND ($4 = '' OR name ILIKE '%' || $4 || '%')
|
||||
ORDER BY name
|
||||
LIMIT $5 OFFSET $6
|
||||
`, userID, path, path, q, pageSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var files []File
|
||||
for rows.Next() {
|
||||
var f File
|
||||
var orgNull sql.NullString
|
||||
var userNull sql.NullString
|
||||
if err := rows.Scan(&f.ID, &orgNull, &userNull, &f.Name, &f.Path, &f.Type, &f.Size, &f.LastModified, &f.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orgNull.Valid {
|
||||
oid, _ := uuid.Parse(orgNull.String)
|
||||
f.OrgID = &oid
|
||||
}
|
||||
if userNull.Valid {
|
||||
uid, _ := uuid.Parse(userNull.String)
|
||||
f.UserID = &uid
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, rows.Err()
|
||||
}
|
||||
|
||||
// CreateFile inserts a file or folder record. orgID or userID may be nil.
|
||||
func (db *DB) CreateFile(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUID, name, path, fileType string, size int64) (*File, error) {
|
||||
var f File
|
||||
var orgIDVal interface{}
|
||||
var userIDVal interface{}
|
||||
if orgID != nil {
|
||||
orgIDVal = *orgID
|
||||
} else {
|
||||
orgIDVal = nil
|
||||
}
|
||||
if userID != nil {
|
||||
userIDVal = *userID
|
||||
} else {
|
||||
userIDVal = nil
|
||||
}
|
||||
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO files (org_id, user_id, name, path, type, size)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
|
||||
`, orgIDVal, userIDVal, name, path, fileType, size).Scan(&f.ID, new(sql.NullString), new(sql.NullString), &f.Name, &f.Path, &f.Type, &f.Size, &f.LastModified, &f.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// DeleteFileByPath removes a file or folder matching path for a given org or user
|
||||
func (db *DB) DeleteFileByPath(ctx context.Context, orgID *uuid.UUID, userID *uuid.UUID, path string) error {
|
||||
var res sql.Result
|
||||
var err error
|
||||
if orgID != nil {
|
||||
res, err = db.ExecContext(ctx, `DELETE FROM files WHERE org_id = $1 AND path = $2`, *orgID, path)
|
||||
} else if userID != nil {
|
||||
res, err = db.ExecContext(ctx, `DELETE FROM files WHERE user_id = $1 AND path = $2`, *userID, path)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = res.RowsAffected()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) UpdateMemberRole(ctx context.Context, orgID, userID uuid.UUID, role string) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE memberships
|
||||
|
||||
@@ -2,9 +2,13 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
@@ -64,6 +68,22 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
||||
r.Route("/", func(r chi.Router) {
|
||||
r.Use(middleware.Auth(jwtManager, db))
|
||||
|
||||
// User-scoped routes (personal workspace)
|
||||
r.Get("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
||||
userFilesHandler(w, req, db)
|
||||
})
|
||||
// Create / delete in user workspace
|
||||
r.Post("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
||||
createUserFileHandler(w, req, db, auditLogger)
|
||||
})
|
||||
r.Delete("/user/files", func(w http.ResponseWriter, req *http.Request) {
|
||||
deleteUserFileHandler(w, req, db, auditLogger)
|
||||
})
|
||||
// POST wrapper for delete
|
||||
r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
|
||||
deleteUserFilePostHandler(w, req, db, auditLogger)
|
||||
})
|
||||
|
||||
// Org routes
|
||||
r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) {
|
||||
listOrgsHandler(w, req, db, jwtManager)
|
||||
@@ -78,7 +98,21 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
||||
|
||||
// File routes
|
||||
r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files", func(w http.ResponseWriter, req *http.Request) {
|
||||
listFilesHandler(w, req)
|
||||
listFilesHandler(w, req, db)
|
||||
})
|
||||
|
||||
// Create file/folder in org workspace
|
||||
r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files", func(w http.ResponseWriter, req *http.Request) {
|
||||
createOrgFileHandler(w, req, db, auditLogger)
|
||||
})
|
||||
// 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) {
|
||||
deleteOrgFilePostHandler(w, req, db, auditLogger)
|
||||
})
|
||||
|
||||
// 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) {
|
||||
deleteOrgFileHandler(w, req, db, auditLogger)
|
||||
})
|
||||
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) {
|
||||
@@ -223,21 +257,45 @@ func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, a
|
||||
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"},
|
||||
func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
||||
// Org ID is provided by middleware.Org
|
||||
orgID := r.Context().Value("org").(uuid.UUID)
|
||||
// Query params: path, q (search), page, pageSize
|
||||
path := r.URL.Query().Get("path")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
q := r.URL.Query().Get("q")
|
||||
page := 1
|
||||
pageSize := 100
|
||||
if p := r.URL.Query().Get("page"); p != "" {
|
||||
fmt.Sscanf(p, "%d", &page)
|
||||
}
|
||||
if ps := r.URL.Query().Get("pageSize"); ps != "" {
|
||||
fmt.Sscanf(ps, "%d", &pageSize)
|
||||
}
|
||||
|
||||
files, err := db.GetOrgFiles(r.Context(), orgID, path, q, page, pageSize)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to get org files")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to a JSON-friendly shape expected by frontend
|
||||
out := make([]map[string]interface{}, 0, len(files))
|
||||
for _, f := range files {
|
||||
out = append(out, map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"path": f.Path,
|
||||
"type": f.Type,
|
||||
"size": f.Size,
|
||||
"lastModified": f.LastModified.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(files)
|
||||
json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
|
||||
@@ -735,3 +793,327 @@ func passwordLoginHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// userFilesHandler returns files for the authenticated user's personal workspace.
|
||||
func userFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
||||
userIDStr, ok := r.Context().Value("user").(string)
|
||||
if !ok || userIDStr == "" {
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Invalid user id in context")
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
path := r.URL.Query().Get("path")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
q := r.URL.Query().Get("q")
|
||||
page := 1
|
||||
pageSize := 100
|
||||
if p := r.URL.Query().Get("page"); p != "" {
|
||||
fmt.Sscanf(p, "%d", &page)
|
||||
}
|
||||
if ps := r.URL.Query().Get("pageSize"); ps != "" {
|
||||
fmt.Sscanf(ps, "%d", &pageSize)
|
||||
}
|
||||
|
||||
files, err := db.GetUserFiles(r.Context(), userID, path, q, page, pageSize)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to get user files")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]map[string]interface{}, 0, len(files))
|
||||
for _, f := range files {
|
||||
out = append(out, map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"path": f.Path,
|
||||
"type": f.Type,
|
||||
"size": f.Size,
|
||||
"lastModified": f.LastModified.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
orgID := r.Context().Value("org").(uuid.UUID)
|
||||
userIDStr, _ := r.Context().Value("user").(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
// Support multipart uploads (field "file") or JSON metadata for folders
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
// Handle file upload
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad multipart request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
parentPath := r.FormValue("path")
|
||||
if parentPath == "" {
|
||||
parentPath = "/"
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Save to disk under data/uploads/orgs/<orgId>/<parentPath>
|
||||
baseDir := filepath.Join("data", "uploads", "orgs", orgID.String())
|
||||
targetDir := filepath.Join(baseDir, parentPath)
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
errors.LogError(r, err, "Failed to create target dir")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
outPath := filepath.Join(targetDir, header.Filename)
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer outFile.Close()
|
||||
written, err := io.Copy(outFile, file)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to write file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Store metadata in DB; store path relative to workspace root
|
||||
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
|
||||
if !strings.HasPrefix(storedPath, "/") {
|
||||
storedPath = "/" + storedPath
|
||||
}
|
||||
f, err := db.CreateFile(r.Context(), &orgID, &userID, header.Filename, storedPath, "file", written)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create org file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
OrgID: &orgID,
|
||||
Action: "upload_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // file|folder
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := db.CreateFile(r.Context(), &orgID, &userID, req.Name, req.Path, req.Type, req.Size)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create org file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
OrgID: &orgID,
|
||||
Action: "create_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
|
||||
}
|
||||
|
||||
// deleteOrgFileHandler deletes a file/folder in org workspace by path
|
||||
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
|
||||
orgID := r.Context().Value("org").(uuid.UUID)
|
||||
userIDStr, _ := r.Context().Value("user").(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DeleteFileByPath(r.Context(), &orgID, nil, req.Path); err != nil {
|
||||
errors.LogError(r, err, "Failed to delete org file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
OrgID: &orgID,
|
||||
Action: "delete_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
deleteOrgFileHandler(w, r, db, auditLogger)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
userIDStr, ok := r.Context().Value("user").(string)
|
||||
if !ok || userIDStr == "" {
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
// Support multipart uploads for file content or JSON for folders
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad multipart request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
parentPath := r.FormValue("path")
|
||||
if parentPath == "" {
|
||||
parentPath = "/"
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
baseDir := filepath.Join("data", "uploads", "users", userID.String())
|
||||
targetDir := filepath.Join(baseDir, parentPath)
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
errors.LogError(r, err, "Failed to create target dir")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
outPath := filepath.Join(targetDir, header.Filename)
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer outFile.Close()
|
||||
written, err := io.Copy(outFile, file)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to write file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
|
||||
if !strings.HasPrefix(storedPath, "/") {
|
||||
storedPath = "/" + storedPath
|
||||
}
|
||||
f, err := db.CreateFile(r.Context(), nil, &userID, header.Filename, storedPath, "file", written)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create user file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "upload_user_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // file|folder
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := db.CreateFile(r.Context(), nil, &userID, req.Name, req.Path, req.Type, req.Size)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to create user file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "create_user_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
|
||||
}
|
||||
|
||||
// Also accept POST /user/files/delete
|
||||
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
|
||||
deleteUserFileHandler(w, r, db, auditLogger)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
userIDStr, ok := r.Context().Value("user").(string)
|
||||
if !ok || userIDStr == "" {
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DeleteFileByPath(r.Context(), nil, &userID, req.Path); err != nil {
|
||||
errors.LogError(r, err, "Failed to delete user file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "delete_user_file",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
17
go_cloud/migrations/0003_files.sql
Normal file
17
go_cloud/migrations/0003_files.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Create files table for org and user workspaces
|
||||
|
||||
CREATE TABLE files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID REFERENCES organizations(id),
|
||||
user_id UUID REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
size BIGINT DEFAULT 0,
|
||||
last_modified TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_files_org_id ON files(org_id);
|
||||
CREATE INDEX idx_files_user_id ON files(user_id);
|
||||
CREATE INDEX idx_files_path ON files(path);
|
||||
Reference in New Issue
Block a user