repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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)
M db/postgres/storage.go
+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 {
M ui/api.go
+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)),