Add temporary token generation for secure file downloads

This commit is contained in:
2025-05-26 19:36:37 +08:00
parent 532b3ff8df
commit 764ec5f38c
3 changed files with 75 additions and 2 deletions

View File

@@ -1,9 +1,12 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"net/url"
"nysoure/server/model" "nysoure/server/model"
"nysoure/server/service" "nysoure/server/service"
"nysoure/server/utils"
"strconv" "strconv"
"strings" "strings"
@@ -21,6 +24,7 @@ func AddFileRoutes(router fiber.Router) {
fileGroup.Get("/:id", getFile) fileGroup.Get("/:id", getFile)
fileGroup.Put("/:id", updateFile) fileGroup.Put("/:id", updateFile)
fileGroup.Delete("/:id", deleteFile) fileGroup.Delete("/:id", deleteFile)
fileGroup.Get("/download/local", downloadLocalFile)
fileGroup.Get("/download/:id", downloadFile) fileGroup.Get("/download/:id", downloadFile)
} }
} }
@@ -204,6 +208,40 @@ func downloadFile(c fiber.Ctx) error {
if strings.HasPrefix(s, "http") { if strings.HasPrefix(s, "http") {
return c.Redirect().Status(fiber.StatusFound).To(s) return c.Redirect().Status(fiber.StatusFound).To(s)
} }
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) data := map[string]string{
return c.SendFile(s) "path": s,
"filename": filename,
}
j, _ := json.Marshal(data)
token, err := utils.GenerateTemporaryToken(string(j))
if err != nil {
return model.NewInternalServerError("Failed to generate download token")
}
return c.Redirect().Status(fiber.StatusFound).To(fmt.Sprintf("%s/api/files/download/local?token=%s", c.BaseURL(), token))
}
func downloadLocalFile(c fiber.Ctx) error {
token := c.Query("token")
if token == "" {
return model.NewRequestError("Download token is required")
}
data, err := utils.ParseTemporaryToken(token)
if err != nil {
return model.NewRequestError("Invalid or expired download token")
}
var fileData map[string]string
if err := json.Unmarshal([]byte(data), &fileData); err != nil {
return model.NewInternalServerError("Failed to parse download data")
}
path := fileData["path"]
filename := fileData["filename"]
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", url.PathEscape(filename)))
return c.SendFile(path, fiber.SendFile{
ByteRange: true,
})
} }

View File

@@ -403,6 +403,7 @@ func GetFile(fid string) (*model.FileView, error) {
return file.ToView(), nil return file.ToView(), nil
} }
// DownloadFile handles the file download request. Return a presigned URL or a direct file path.
func DownloadFile(ip, fid, cfToken string) (string, string, error) { func DownloadFile(ip, fid, cfToken string) (string, string, error) {
passed, err := verifyCfToken(cfToken) passed, err := verifyCfToken(cfToken)
if err != nil { if err != nil {

View File

@@ -60,3 +60,37 @@ func ParseToken(token string) (uint, error) {
} }
return 0, errors.New("invalid token") return 0, errors.New("invalid token")
} }
// GenerateTemporaryToken creates a JWT token that expires in 15 minutes
func GenerateTemporaryToken(data string) (string, error) {
t := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.MapClaims{
"data": data,
"exp": time.Now().Add(15 * time.Minute).Unix(),
})
s, err := t.SignedString(key)
if err != nil {
return "", err
}
return s, nil
}
// ParseTemporaryToken parses a JWT token and returns the data if valid
func ParseTemporaryToken(token string) (string, error) {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return key, nil
})
if err != nil {
return "", err
}
if claims, ok := t.Claims.(jwt.MapClaims); ok && t.Valid {
data := claims["data"].(string)
expF := claims["exp"].(float64)
exp := time.Unix(int64(expF), 0)
if time.Now().After(exp) {
return "", errors.New("token expired")
}
return data, nil
}
return "", errors.New("invalid token")
}