- commit
- fda8746
- parent
- 815cc66
- author
- Eric Bower
- date
- 2024-03-29 14:46:21 +0000 UTC
feat(ui): ability to enable/disable analytics
3 files changed,
+110,
-0
M
db/db.go
+2,
-0
1@@ -376,6 +376,8 @@ type DB interface {
2 FindFeaturesForUser(userID string) ([]*FeatureFlag, error)
3 HasFeatureForUser(userID string, feature string) bool
4 FindTotalSizeForUser(userID string) (int, error)
5+ InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error)
6+ RemoveFeature(userID, names string) error
7
8 InsertFeedItems(postID string, items []*FeedItem) error
9 FindFeedItemsByPostID(postID string) ([]*FeedItem, error)
+25,
-0
1@@ -1842,6 +1842,31 @@ func (me *PsqlDB) FindTokensForUser(userID string) ([]*db.Token, error) {
2 return keys, nil
3 }
4
5+func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) {
6+ var featureID string
7+ err := me.Db.QueryRow(
8+ `INSERT INTO feature_flags (user_id, name, expires_at) VALUES ($1, $2, $3) RETURNING id;`,
9+ userID,
10+ name,
11+ expiresAt,
12+ ).Scan(&featureID)
13+ if err != nil {
14+ return nil, err
15+ }
16+
17+ feature, err := me.FindFeatureForUser(userID, name)
18+ if err != nil {
19+ return nil, err
20+ }
21+
22+ return feature, nil
23+}
24+
25+func (me *PsqlDB) RemoveFeature(userID string, name string) error {
26+ _, err := me.Db.Exec(`DELETE FROM feature_flags WHERE user_id = $1 AND name = $2`, userID, name)
27+ return err
28+}
29+
30 func (me *PsqlDB) createFeatureExpiresAt(userID, name string) time.Time {
31 ff, _ := me.FindFeatureForUser(userID, name)
32 if ff == nil {
+83,
-0
1@@ -5,6 +5,8 @@ import (
2 "fmt"
3 "io"
4 "net/http"
5+ "slices"
6+ "strings"
7 "time"
8
9 "github.com/charmbracelet/ssh"
10@@ -479,6 +481,85 @@ type ProjectObject struct {
11 ModTime time.Time `json:"mod_time"`
12 }
13
14+type createFeaturePayload struct {
15+ Name string `json:"name"`
16+}
17+
18+var featureAllowList = []string{
19+ "analytics",
20+}
21+
22+func createFeature(httpCtx *shared.ApiConfig, ctx ssh.Context) http.HandlerFunc {
23+ logger := httpCtx.Cfg.Logger
24+ return func(w http.ResponseWriter, r *http.Request) {
25+ w.Header().Set("Content-Type", "application/json")
26+ user, _ := shared.GetUserCtx(ctx)
27+ if !ensureUser(w, user) {
28+ return
29+ }
30+
31+ dbpool := shared.GetDB(r)
32+ var payload createFeaturePayload
33+ body, _ := io.ReadAll(r.Body)
34+ _ = json.Unmarshal(body, &payload)
35+
36+ // only allow the user to add certain features to their account
37+ if !slices.Contains(featureAllowList, payload.Name) {
38+ err := fmt.Errorf(
39+ "(%s) is not in feature allowlist (%s)",
40+ payload.Name,
41+ strings.Join(featureAllowList, ", "),
42+ )
43+ shared.JSONError(w, err.Error(), http.StatusUnprocessableEntity)
44+ return
45+ }
46+
47+ now := time.Now()
48+ expiresAt := now.AddDate(100, 0, 0)
49+ feature, err := dbpool.InsertFeature(user.ID, payload.Name, expiresAt)
50+ if err != nil {
51+ shared.JSONError(w, err.Error(), http.StatusUnprocessableEntity)
52+ return
53+ }
54+
55+ err = json.NewEncoder(w).Encode(feature)
56+ if err != nil {
57+ logger.Error("json encode", "err", err.Error())
58+ }
59+ }
60+}
61+
62+func deleteFeature(httpCtx *shared.ApiConfig, ctx ssh.Context) http.HandlerFunc {
63+ logger := httpCtx.Cfg.Logger
64+ return func(w http.ResponseWriter, r *http.Request) {
65+ w.Header().Set("Content-Type", "application/json")
66+ user, _ := shared.GetUserCtx(ctx)
67+ if !ensureUser(w, user) {
68+ return
69+ }
70+ dbpool := shared.GetDB(r)
71+ featureName := shared.GetField(r, 0)
72+
73+ if !slices.Contains(featureAllowList, featureName) {
74+ err := fmt.Errorf(
75+ "(%s) is not in feature allowlist (%s)",
76+ featureName,
77+ strings.Join(featureAllowList, ", "),
78+ )
79+ shared.JSONError(w, err.Error(), http.StatusUnprocessableEntity)
80+ return
81+ }
82+
83+ err := dbpool.RemoveFeature(user.ID, featureName)
84+ if err != nil {
85+ logger.Error("could not remove features", "err", err.Error())
86+ shared.JSONError(w, err.Error(), http.StatusUnprocessableEntity)
87+ return
88+ }
89+ w.WriteHeader(http.StatusNoContent)
90+ }
91+}
92+
93 func getProjectObjects(apiConfig *shared.ApiConfig, ctx ssh.Context) http.HandlerFunc {
94 logger := apiConfig.Cfg.Logger
95 storage := apiConfig.Storage
96@@ -584,6 +665,8 @@ func CreateRoutes(apiConfig *shared.ApiConfig, ctx ssh.Context) []shared.Route {
97 shared.NewCorsRoute("GET", "/api/pubkeys", getPublicKeys(apiConfig, ctx)),
98 shared.NewCorsRoute("POST", "/api/pubkeys", createPubkey(apiConfig, ctx)),
99 shared.NewCorsRoute("DELETE", "/api/pubkeys/(.+)", deletePubkey(apiConfig, ctx)),
100+ shared.NewCorsRoute("POST", "/api/features", createFeature(apiConfig, ctx)),
101+ shared.NewCorsRoute("DELETE", "/api/features/(.+)", deleteFeature(apiConfig, ctx)),
102 shared.NewCorsRoute("PATCH", "/api/pubkeys/(.+)", patchPubkey(apiConfig, ctx)),
103 shared.NewCorsRoute("GET", "/api/tokens", getTokens(apiConfig, ctx)),
104 shared.NewCorsRoute("POST", "/api/tokens", createToken(apiConfig, ctx)),