Expose paginated share redirect events to workspace admins
This commit is contained in:
@@ -114,6 +114,7 @@ func Register(mux *http.ServeMux, db *sql.DB, callbacks map[string]string, origi
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials/{material}/grants/{account}", s.protected(callbacks, s.deleteMaterialGrant))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials/{material}/layouts", s.protected(callbacks, s.layouts))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials/{material}/layouts/{layout}", s.protected(callbacks, s.layouts))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/shares/{link}/clicks", s.protected(callbacks, s.shareClicks))
|
||||
mux.HandleFunc("/projects/{project}/onboarding", s.protected(callbacks, s.onboarding))
|
||||
mux.HandleFunc("/s/{token}", s.shortLink)
|
||||
mux.HandleFunc("/projects/{project}/shares/redeem", s.protected(callbacks, s.redeemShare))
|
||||
|
||||
@@ -106,6 +106,11 @@ func TestOnboardingAndSharing(t *testing.T) {
|
||||
share := call("POST", workspacePath+"/shares", tokens[0], response{"role": "viewer", "max_uses": 1}, 201)
|
||||
token := share["token"].(string)
|
||||
call("GET", "/s/"+token, "", nil, 302)
|
||||
clicks := call("GET", workspacePath+"/shares/"+share["id"].(string)+"/clicks", tokens[0], nil, 200)
|
||||
if len(clicks["items"].([]any)) != 1 {
|
||||
t.Fatal("click event API did not return the redirect")
|
||||
}
|
||||
call("GET", workspacePath+"/shares/"+share["id"].(string)+"/clicks", tokens[2], nil, 404)
|
||||
call("POST", base+"/shares/redeem", tokens[1], response{"token": token}, 200)
|
||||
call("POST", base+"/shares/redeem", tokens[1], response{"token": token}, 200)
|
||||
call("POST", base+"/shares/redeem", tokens[2], response{"token": token}, 410)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -285,3 +286,71 @@ ON CONFLICT(project,workspace_id,material_id,account_id) DO UPDATE SET role=CASE
|
||||
reply(w, 200, response{"workspace_id": workspace, "material_id": material, "role": role, "already_redeemed": n == 0})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) shareClicks(w http.ResponseWriter, r *http.Request, project, account string) error {
|
||||
if r.Method != http.MethodGet {
|
||||
reject(w, 405, "method_not_allowed")
|
||||
return nil
|
||||
}
|
||||
before := r.URL.Query().Get("before")
|
||||
var cursor int64
|
||||
if before != "" {
|
||||
value, err := strconv.ParseInt(before, 10, 64)
|
||||
if err != nil || value < 1 {
|
||||
reject(w, 400, "invalid_cursor")
|
||||
return nil
|
||||
}
|
||||
cursor = value
|
||||
}
|
||||
tx, err := s.db.BeginTx(r.Context(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
workspace, link := r.PathValue("workspace"), r.PathValue("link")
|
||||
_, role, err := lockWorkspace(r.Context(), tx, project, workspace, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role != "admin" {
|
||||
reject(w, 403, "forbidden")
|
||||
return nil
|
||||
}
|
||||
var found string
|
||||
if err = tx.QueryRowContext(r.Context(), "SELECT id::text FROM share_links WHERE project=$1 AND workspace_id=$2 AND id=$3", project, workspace, link).Scan(&found); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := tx.QueryContext(r.Context(), `
|
||||
SELECT id,clicked_at,referrer_origin FROM link_clicks WHERE project=$1 AND workspace_id=$2 AND link_id=$3 AND ($4::bigint=0 OR id<$4)
|
||||
ORDER BY id DESC LIMIT 101
|
||||
`, project, workspace, link, cursor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []response{}
|
||||
var last int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var at time.Time
|
||||
var ref string
|
||||
if err := rows.Scan(&id, &at, &ref); err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, response{"id": strconv.FormatInt(id, 10), "clicked_at": at, "referrer_origin": ref})
|
||||
if len(items) <= 100 {
|
||||
last = id
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
var next *string
|
||||
if len(items) > 100 {
|
||||
items = items[:100]
|
||||
value := strconv.FormatInt(last, 10)
|
||||
next = &value
|
||||
}
|
||||
reply(w, 200, response{"items": items, "next_before": next})
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user