222 lines
6.2 KiB
Go
222 lines
6.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
|
|
"github.com/peterqiu0516/sub-store/internal/middleware"
|
|
"github.com/peterqiu0516/sub-store/internal/model"
|
|
)
|
|
|
|
// --- Share (download grant) handlers ---
|
|
|
|
func (d *Deps) HandleListShares(c fiber.Ctx) error {
|
|
grants, err := d.GrantRepo.List()
|
|
if err != nil {
|
|
return failed(c, "Failed to list shares", 500)
|
|
}
|
|
return success(c, grants)
|
|
}
|
|
|
|
func (d *Deps) HandleCreateShare(c fiber.Ctx) error {
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
resourceType := getStringValue(input["resourceType"])
|
|
if resourceType != "source" && resourceType != "collection" {
|
|
return failed(c, "Share resourceType and resourceId are required", 400)
|
|
}
|
|
resourceId := getStringValue(input["resourceId"])
|
|
if resourceId == "" {
|
|
return failed(c, "Share resourceType and resourceId are required", 400)
|
|
}
|
|
// Check resource exists
|
|
if resourceType == "source" {
|
|
s, _ := d.SourceRepo.Get(resourceId)
|
|
if s == nil {
|
|
return failed(c, "Share resource does not exist", 404)
|
|
}
|
|
} else {
|
|
col, _ := d.CollectionRepo.Get(resourceId)
|
|
if col == nil {
|
|
return failed(c, "Share resource does not exist", 404)
|
|
}
|
|
}
|
|
target := ""
|
|
if input["target"] != nil {
|
|
target = model.NormalizeTargetAlias(input["target"])
|
|
if input["target"] != "" && target == "" {
|
|
return failed(c, "Unsupported target", 400)
|
|
}
|
|
}
|
|
var expiresAt *int64
|
|
if v, ok := input["expiresAt"]; ok && v != nil {
|
|
if n, ok := v.(float64); ok && n > 0 {
|
|
e := int64(n)
|
|
expiresAt = &e
|
|
}
|
|
} else if v, ok := input["expiresIn"]; ok && v != nil {
|
|
if n, ok := v.(float64); ok && n > 0 {
|
|
e := time.Now().UnixMilli() + int64(n)*1000
|
|
expiresAt = &e
|
|
}
|
|
}
|
|
grant, token, err := d.GrantRepo.Create(resourceType, resourceId, target, expiresAt)
|
|
if err != nil {
|
|
return failed(c, "Failed to create share", 500)
|
|
}
|
|
// Build download URL
|
|
path := "/download/" + resourceType + "/" + resourceId
|
|
if target != "" {
|
|
path += "/" + target
|
|
}
|
|
url := getPublicBaseUrl(c) + path + "?token=" + token
|
|
return success(c, fiber.Map{
|
|
"id": grant.ID,
|
|
"resourceType": grant.ResourceType,
|
|
"resourceId": grant.ResourceId,
|
|
"target": grant.Target,
|
|
"expiresAt": grant.ExpiresAt,
|
|
"enabled": grant.Enabled,
|
|
"createdAt": grant.CreatedAt,
|
|
"updatedAt": grant.UpdatedAt,
|
|
"token": token,
|
|
"url": url,
|
|
})
|
|
}
|
|
|
|
func (d *Deps) HandleUpdateShare(c fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
var enabled *bool
|
|
if v, ok := input["enabled"].(bool); ok {
|
|
enabled = &v
|
|
}
|
|
var expiresAt *int64
|
|
if v, ok := input["expiresAt"]; ok {
|
|
if v == nil {
|
|
expiresAt = new(int64) // 0 means nil
|
|
} else if n, ok := v.(float64); ok {
|
|
e := int64(n)
|
|
expiresAt = &e
|
|
}
|
|
}
|
|
updated, err := d.GrantRepo.Update(id, enabled, expiresAt)
|
|
if err != nil || updated == nil {
|
|
return failed(c, "Share not found", 404)
|
|
}
|
|
return success(c, updated)
|
|
}
|
|
|
|
func (d *Deps) HandleDeleteShare(c fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
snapshot, err := d.GrantRepo.GetSnapshot(id)
|
|
if err != nil || snapshot == nil {
|
|
return failed(c, "Share not found", 404)
|
|
}
|
|
_, err = d.RecycleRepo.ArchiveAndDelete("share", id, snapshot, "DELETE FROM download_grants WHERE id = ?", id)
|
|
if err != nil {
|
|
return failed(c, "Failed to delete share", 500)
|
|
}
|
|
return success(c, fiber.Map{"deleted": true})
|
|
}
|
|
|
|
// --- Recycle bin handlers ---
|
|
|
|
func (d *Deps) HandleListRecycleBin(c fiber.Ctx) error {
|
|
entries, err := d.RecycleRepo.List()
|
|
if err != nil {
|
|
return failed(c, "Failed to list recycle bin", 500)
|
|
}
|
|
return success(c, entries)
|
|
}
|
|
|
|
func (d *Deps) HandleDeleteRecycleBinEntry(c fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
entry, err := d.RecycleRepo.Get(id)
|
|
if err != nil || entry == nil {
|
|
return failed(c, "Recycle entry not found", 404)
|
|
}
|
|
if err := d.RecycleRepo.Delete(id); err != nil {
|
|
return failed(c, "Failed to delete", 500)
|
|
}
|
|
return success(c, fiber.Map{"deleted": true})
|
|
}
|
|
|
|
func (d *Deps) HandleRestoreRecycleBinEntry(c fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
entry, err := d.RecycleRepo.Get(id)
|
|
if err != nil || entry == nil {
|
|
return failed(c, "Recycle entry not found", 404)
|
|
}
|
|
resourceType := getStringValue(entry["resourceType"])
|
|
resourceId := getStringValue(entry["resourceId"])
|
|
snapshot := getMapValue(entry["snapshot"])
|
|
|
|
switch resourceType {
|
|
case "source":
|
|
existing, _ := d.SourceRepo.Get(resourceId)
|
|
if existing != nil {
|
|
return failed(c, "Source id already exists", 409)
|
|
}
|
|
d.SourceRepo.Upsert(mapToSourceRecord(snapshot))
|
|
case "collection":
|
|
existing, _ := d.CollectionRepo.Get(resourceId)
|
|
if existing != nil {
|
|
return failed(c, "Collection id already exists", 409)
|
|
}
|
|
d.CollectionRepo.Upsert(mapToCollectionRecord(snapshot))
|
|
case "template":
|
|
existing, _ := d.TemplateRepo.Get(resourceId)
|
|
if existing != nil {
|
|
return failed(c, "Template id already exists", 409)
|
|
}
|
|
d.TemplateRepo.Upsert(mapToTemplateRecord(snapshot))
|
|
case "share":
|
|
existing, _ := d.GrantRepo.Get(resourceId)
|
|
if existing != nil {
|
|
return failed(c, "Share id already exists", 409)
|
|
}
|
|
// review-resolution #38: restore tokenHash
|
|
d.GrantRepo.RestoreFromSnapshot(snapshot)
|
|
}
|
|
d.RecycleRepo.Delete(id)
|
|
return success(c, fiber.Map{
|
|
"restored": true,
|
|
"resourceType": resourceType,
|
|
"resourceId": resourceId,
|
|
})
|
|
}
|
|
|
|
// getPublicBaseUrl returns the public base URL for download links.
|
|
// Per review-resolution #11: PUBLIC_DOWNLOAD_HOSTS config, fallback to request origin.
|
|
func getPublicBaseUrl(c fiber.Ctx) string {
|
|
// This is set by the handler using deps config
|
|
publicHosts := c.Locals("publicDownloadHosts")
|
|
if hosts, ok := publicHosts.(string); ok && hosts != "" {
|
|
parts := strings.Split(hosts, ",")
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
return "https://" + p
|
|
}
|
|
}
|
|
}
|
|
// Use X-Forwarded-Proto or default to http
|
|
proto := c.Get("X-Forwarded-Proto")
|
|
if proto == "" {
|
|
proto = "http"
|
|
}
|
|
return proto + "://" + c.Hostname()
|
|
}
|
|
|
|
// SetSafeResponseHeader wraps middleware.SetSafeResponseHeader for handler use.
|
|
var SetSafeResponseHeader = middleware.SetSafeResponseHeader
|