feat: list active public file links
This commit is contained in:
@@ -83,6 +83,7 @@ func main() {
|
|||||||
r.Get("/download", nodeH.Download)
|
r.Get("/download", nodeH.Download)
|
||||||
r.Get("/access", nodeH.ListAccess)
|
r.Get("/access", nodeH.ListAccess)
|
||||||
r.Put("/access", nodeH.ReplaceAccess)
|
r.Put("/access", nodeH.ReplaceAccess)
|
||||||
|
r.Get("/public-links", nodeH.ListPublicLinks)
|
||||||
r.Post("/public-links", nodeH.CreatePublicLink)
|
r.Post("/public-links", nodeH.CreatePublicLink)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,22 @@ func (h *NodeHandler) ReplaceAccess(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
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) {
|
func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
|
||||||
var req model.PublicLinkRequest
|
var req model.PublicLinkRequest
|
||||||
if err := decodeJSON(r, &req); err != nil {
|
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())
|
userID := commonmw.GetUserID(r.Context())
|
||||||
id := chi.URLParam(r, "id")
|
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) {
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
writeError(w, http.StatusNotFound, "file not found")
|
writeError(w, http.StatusNotFound, "file not found")
|
||||||
return
|
return
|
||||||
@@ -342,11 +358,8 @@ func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}")
|
h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}")
|
||||||
writeJSON(w, http.StatusCreated, model.PublicLinkResponse{
|
link.URL = h.publicURL(token)
|
||||||
ID: linkID,
|
writeJSON(w, http.StatusCreated, link)
|
||||||
URL: h.publicURL(token),
|
|
||||||
ExpiresAt: req.ExpiresAt,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *NodeHandler) PublicMeta(w http.ResponseWriter, r *http.Request) {
|
func (h *NodeHandler) PublicMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ type PublicLinkRequest struct {
|
|||||||
|
|
||||||
type PublicLinkResponse struct {
|
type PublicLinkResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url,omitempty"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -465,19 +465,49 @@ func normalizeAccess(access []model.Access) []model.Access {
|
|||||||
return out
|
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)
|
hash := TokenHash(token)
|
||||||
var id string
|
var link model.PublicLinkResponse
|
||||||
err := r.pool.QueryRow(ctx, `
|
err := r.pool.QueryRow(ctx, `
|
||||||
INSERT INTO files_public_links (node_id, token_hash, expires_at, created_by)
|
INSERT INTO files_public_links (node_id, token_hash, public_token, expires_at, created_by)
|
||||||
SELECT $1, $2, $3, $4
|
SELECT $1, $2, $3, $4, $5
|
||||||
WHERE effective_node_access($1, $4, '{}'::text[]) = 'edit'
|
WHERE effective_node_access($1, $5, '{}'::text[]) = 'edit'
|
||||||
RETURNING id
|
RETURNING id, expires_at, created_at
|
||||||
`, nodeID, hash, expiresAt, actorID).Scan(&id)
|
`, nodeID, hash, token, expiresAt, actorID).Scan(&link.ID, &link.ExpiresAt, &link.CreatedAt)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
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) {
|
func (r *NodeRepository) GetByPublicToken(ctx context.Context, token string) (*model.Node, error) {
|
||||||
|
|||||||
3
migrations/005_public_link_tokens.down.sql
Normal file
3
migrations/005_public_link_tokens.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE files_public_links
|
||||||
|
DROP COLUMN IF EXISTS public_token;
|
||||||
|
|
||||||
3
migrations/005_public_link_tokens.up.sql
Normal file
3
migrations/005_public_link_tokens.up.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE files_public_links
|
||||||
|
ADD COLUMN IF NOT EXISTS public_token TEXT;
|
||||||
|
|
||||||
Reference in New Issue
Block a user