~bigbes/lethe

ref: 74314eeb68171bddd6ed722efaa9fc4164428990 lethe/internal/domain/stats/handler.go -rw-r--r-- 3.1 KiB
74314eeb — Eugene Blikh docs(lethe-web-ui-login): record verify checks a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package stats

import (
	"context"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"go.bigb.es/auxilia/culpa"
	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/lethe/internal/domain/session"
	"sourcecraft.dev/bigbes/lethe/internal/pkg/apierror"
	"sourcecraft.dev/bigbes/lethe/internal/pkg/httputil"
	"sourcecraft.dev/bigbes/lethe/internal/server/auth"
)

// allOwnersSentinel is the ?owner=* value that an admin uses to read across
// all owners. Identical to the session and project handler sentinels.
const allOwnersSentinel = "*"

// validRanges is the set of accepted ?range= values.
var validRanges = map[string]int{
	"7d":  7,
	"30d": 30,
	"90d": 90,
}

// Handler is the steward-managed HTTP boundary for the stats aggregation API.
type Handler struct {
	Repo *Repository `inject:""`
}

// Init satisfies the steward Initer contract.
func (h *Handler) Init(_ context.Context) error { return nil }

// Mount registers the single read route under r. Server.Init mounts this
// inside the /api/v1 group, so the effective path is /api/v1/stats.
func (h *Handler) Mount(r chi.Router) {
	r.Get("/stats", h.List)
}

// List handles GET /stats. Reads ?range=7d|30d|90d|all (defaults to 30d),
// resolves the owner scope, and returns a Stats bundle.
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
	scope, err := h.resolveScope(r)
	if err != nil {
		apierror.Render(w, r, err)
		return
	}

	now := time.Now().Unix()
	f := Filter{Owner: scope, Now: now}

	rangeParam := r.URL.Query().Get("range")
	switch {
	case rangeParam == "" || rangeParam == "30d":
		// default: 30 days
		since := now - 30*86400
		f.RangeSince = &since
	case rangeParam == "all":
		f.RangeSince = nil
	default:
		days, ok := validRanges[rangeParam]
		if !ok {
			apierror.Render(w, r, culpa.WithCode(
				culpa.WithPublic(
					culpa.Errorf("unrecognized range %q", rangeParam),
					`?range= must be one of: 7d, 30d, 90d, all`,
				),
				"INVALID",
			))
			return
		}
		since := now - int64(days)*86400
		f.RangeSince = &since
	}

	result, err := h.Repo.Stats(r.Context(), f)
	if err != nil {
		apierror.Render(w, r, err)
		return
	}

	if writeErr := httputil.WriteJSON(w, http.StatusOK, result); writeErr != nil {
		slog.Default().ErrorContext(r.Context(), "write stats response", scribe.Err(writeErr))
	}
}

// resolveScope reads the authenticated identity off the context and the
// optional ?owner= query parameter, then returns the appropriate
// session.OwnerScope. Non-admin requests with ?owner= set are 403.
func (h *Handler) resolveScope(r *http.Request) (session.OwnerScope, error) {
	id := auth.MustIdentity(r.Context())
	param := r.URL.Query().Get("owner")
	if param == "" {
		return session.OwnerScope{User: id.User}, nil
	}
	if !id.IsAdmin {
		return session.OwnerScope{}, culpa.WithCode(
			culpa.WithPublic(culpa.New("?owner= is admin-only"), "?owner= is admin-only"),
			"FORBIDDEN",
		)
	}
	if param == allOwnersSentinel {
		return session.OwnerScope{User: id.User, AllOwners: true}, nil
	}
	owner := strings.ToLower(param)
	return session.OwnerScope{User: id.User, SpecificOwner: &owner}, nil
}