feat: list active public file links
All checks were successful
CI / hygiene (push) Successful in 1s
Build and Deploy / build-and-deploy (push) Successful in 30s
CI / test (push) Successful in 21s

This commit is contained in:
Grendgi
2026-06-16 16:29:59 +03:00
parent 44ea1fa36b
commit bfb1c2d0ab
6 changed files with 67 additions and 16 deletions

View File

@@ -83,6 +83,7 @@ func main() {
r.Get("/download", nodeH.Download)
r.Get("/access", nodeH.ListAccess)
r.Put("/access", nodeH.ReplaceAccess)
r.Get("/public-links", nodeH.ListPublicLinks)
r.Post("/public-links", nodeH.CreatePublicLink)
})
}

View File

@@ -315,6 +315,22 @@ func (h *NodeHandler) ReplaceAccess(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (h *NodeHandler) ListPublicLinks(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
links, err := h.repo.ListPublicLinks(r.Context(), id, userID)
if err != nil {
writeInternalError(w, r, err, "failed to list public links")
return
}
for i := range links {
if links[i].URL != "" {
links[i].URL = h.publicURL(links[i].URL)
}
}
writeJSON(w, http.StatusOK, links)
}
func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
var req model.PublicLinkRequest
if err := decodeJSON(r, &req); err != nil {
@@ -332,7 +348,7 @@ func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
}
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
linkID, err := h.repo.CreatePublicLink(r.Context(), id, userID, token, req.ExpiresAt)
link, err := h.repo.CreatePublicLink(r.Context(), id, userID, token, req.ExpiresAt)
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
@@ -342,11 +358,8 @@ func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
return
}
h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}")
writeJSON(w, http.StatusCreated, model.PublicLinkResponse{
ID: linkID,
URL: h.publicURL(token),
ExpiresAt: req.ExpiresAt,
})
link.URL = h.publicURL(token)
writeJSON(w, http.StatusCreated, link)
}
func (h *NodeHandler) PublicMeta(w http.ResponseWriter, r *http.Request) {

View File

@@ -86,6 +86,7 @@ type PublicLinkRequest struct {
type PublicLinkResponse struct {
ID string `json:"id"`
URL string `json:"url"`
URL string `json:"url,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at,omitempty"`
}

View File

@@ -465,19 +465,49 @@ func normalizeAccess(access []model.Access) []model.Access {
return out
}
func (r *NodeRepository) CreatePublicLink(ctx context.Context, nodeID, actorID, token string, expiresAt time.Time) (string, error) {
func (r *NodeRepository) CreatePublicLink(ctx context.Context, nodeID, actorID, token string, expiresAt time.Time) (*model.PublicLinkResponse, error) {
hash := TokenHash(token)
var id string
var link model.PublicLinkResponse
err := r.pool.QueryRow(ctx, `
INSERT INTO files_public_links (node_id, token_hash, expires_at, created_by)
SELECT $1, $2, $3, $4
WHERE effective_node_access($1, $4, '{}'::text[]) = 'edit'
RETURNING id
`, nodeID, hash, expiresAt, actorID).Scan(&id)
INSERT INTO files_public_links (node_id, token_hash, public_token, expires_at, created_by)
SELECT $1, $2, $3, $4, $5
WHERE effective_node_access($1, $5, '{}'::text[]) = 'edit'
RETURNING id, expires_at, created_at
`, nodeID, hash, token, expiresAt, actorID).Scan(&link.ID, &link.ExpiresAt, &link.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return "", ErrNotFound
return nil, ErrNotFound
}
return id, err
return &link, err
}
func (r *NodeRepository) ListPublicLinks(ctx context.Context, nodeID, actorID string) ([]model.PublicLinkResponse, error) {
rows, err := r.pool.Query(ctx, `
SELECT id, COALESCE(public_token, ''), expires_at, created_at
FROM files_public_links
WHERE node_id = $1
AND revoked_at IS NULL
AND expires_at > now()
AND effective_node_access($1, $2, '{}'::text[]) = 'edit'
ORDER BY expires_at DESC, created_at DESC
`, nodeID, actorID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]model.PublicLinkResponse, 0)
for rows.Next() {
var link model.PublicLinkResponse
var token string
if err := rows.Scan(&link.ID, &token, &link.ExpiresAt, &link.CreatedAt); err != nil {
return nil, err
}
if token != "" {
link.URL = token
}
out = append(out, link)
}
return out, rows.Err()
}
func (r *NodeRepository) GetByPublicToken(ctx context.Context, token string) (*model.Node, error) {

View File

@@ -0,0 +1,3 @@
ALTER TABLE files_public_links
DROP COLUMN IF EXISTS public_token;

View File

@@ -0,0 +1,3 @@
ALTER TABLE files_public_links
ADD COLUMN IF NOT EXISTS public_token TEXT;