repos / pico

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

commit
5fc34f7
parent
2023f81
author
Eric Bower
date
2024-02-19 03:48:28 +0000 UTC
feat(auth): pico+ rss feed
1 files changed,  +106, -2
M auth/auth.go
+106, -2
  1@@ -9,7 +9,9 @@ import (
  2 	"net/http"
  3 	"net/url"
  4 	"strings"
  5+	"time"
  6 
  7+	"github.com/gorilla/feeds"
  8 	"github.com/picosh/pico/db"
  9 	"github.com/picosh/pico/db/postgres"
 10 	"github.com/picosh/pico/shared"
 11@@ -30,11 +32,20 @@ func (client *Client) hasPrivilegedAccess(apiToken string) bool {
 12 }
 13 
 14 type ctxClient struct{}
 15+type ctxKey struct{}
 16 
 17 func getClient(r *http.Request) *Client {
 18 	return r.Context().Value(ctxClient{}).(*Client)
 19 }
 20 
 21+func getField(r *http.Request, index int) string {
 22+	fields := r.Context().Value(ctxKey{}).([]string)
 23+	if index >= len(fields) {
 24+		return ""
 25+	}
 26+	return fields[index]
 27+}
 28+
 29 func getApiToken(r *http.Request) string {
 30 	authHeader := r.Header.Get("authorization")
 31 	if authHeader == "" {
 32@@ -279,6 +290,94 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
 33 	}
 34 }
 35 
 36+func genFeedItem(now time.Time, expiresAt time.Time, warning time.Time, txt string) *feeds.Item {
 37+	if now.After(warning) {
 38+		realUrl := warning.Format("2006-01-02 15:04:05")
 39+		content := fmt.Sprintf(
 40+			"Your pico+ membership is going to expire on %s",
 41+			expiresAt.Format("2006-01-02 15:04:05"),
 42+		)
 43+		return &feeds.Item{
 44+			Id:          realUrl,
 45+			Title:       fmt.Sprintf("pico+ %s expiration notice", txt),
 46+			Link:        &feeds.Link{Href: realUrl},
 47+			Content:     content,
 48+			Created:     warning,
 49+			Updated:     warning,
 50+			Description: content,
 51+			Author:      &feeds.Author{Name: "team pico"},
 52+		}
 53+	}
 54+
 55+	return nil
 56+}
 57+
 58+func rssHandler(w http.ResponseWriter, r *http.Request) {
 59+	client := getClient(r)
 60+	apiToken, err := url.PathUnescape(getField(r, 0))
 61+	if err != nil {
 62+		client.Logger.Error(err.Error())
 63+		http.Error(w, err.Error(), http.StatusNotFound)
 64+		return
 65+	}
 66+	user, err := client.Dbpool.FindUserForToken(apiToken)
 67+	if err != nil {
 68+		client.Logger.Error(err.Error())
 69+		http.Error(w, "invalid token", http.StatusNotFound)
 70+		return
 71+	}
 72+
 73+	href := fmt.Sprintf("https://auth.pico.sh/rss/%s", apiToken)
 74+
 75+	feed := &feeds.Feed{
 76+		Title:       "pico+",
 77+		Link:        &feeds.Link{Href: href},
 78+		Description: "get notified of important membership updates",
 79+		Author:      &feeds.Author{Name: "team pico"},
 80+		Created:     time.Now(),
 81+	}
 82+	var feedItems []*feeds.Item
 83+
 84+	now := time.Now()
 85+	// using pgs as the signal
 86+	ff, err := client.Dbpool.FindFeatureForUser(user.ID, "pgs")
 87+	if err != nil {
 88+		// still want to send an empty feed
 89+	} else {
 90+		oneMonthWarning := ff.ExpiresAt.AddDate(0, -1, 0)
 91+		mo := genFeedItem(now, *ff.ExpiresAt, oneMonthWarning, "1-month")
 92+		if mo != nil {
 93+			feedItems = append(feedItems, mo)
 94+		}
 95+
 96+		oneWeekWarning := ff.ExpiresAt.AddDate(0, 0, -7)
 97+		wk := genFeedItem(now, *ff.ExpiresAt, oneWeekWarning, "1-week")
 98+		if wk != nil {
 99+			feedItems = append(feedItems, wk)
100+		}
101+
102+		oneDayWarning := ff.ExpiresAt.AddDate(0, 0, -1)
103+		day := genFeedItem(now, *ff.ExpiresAt, oneDayWarning, "1-day")
104+		if day != nil {
105+			feedItems = append(feedItems, day)
106+		}
107+	}
108+
109+	feed.Items = feedItems
110+
111+	rss, err := feed.ToAtom()
112+	if err != nil {
113+		client.Logger.Error(err.Error())
114+		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
115+	}
116+
117+	w.Header().Add("Content-Type", "application/atom+xml")
118+	_, err = w.Write([]byte(rss))
119+	if err != nil {
120+		client.Logger.Error(err.Error())
121+	}
122+}
123+
124 func createMainRoutes() []shared.Route {
125 	fileServer := http.FileServer(http.Dir("auth/public"))
126 
127@@ -288,6 +387,7 @@ func createMainRoutes() []shared.Route {
128 		shared.NewRoute("GET", "/authorize", authorizeHandler),
129 		shared.NewRoute("POST", "/token", tokenHandler),
130 		shared.NewRoute("POST", "/key", keyHandler),
131+		shared.NewRoute("GET", "/rss/(.+)", rssHandler),
132 		shared.NewRoute("POST", "/redirect", redirectHandler),
133 		shared.NewRoute("GET", "/main.css", fileServer.ServeHTTP),
134 		shared.NewRoute("GET", "/card.png", fileServer.ServeHTTP),
135@@ -313,7 +413,8 @@ func handler(routes []shared.Route, client *Client) shared.ServeFn {
136 					continue
137 				}
138 				clientCtx := context.WithValue(r.Context(), ctxClient{}, client)
139-				route.Handler(w, r.WithContext(clientCtx))
140+				ctx := context.WithValue(clientCtx, ctxKey{}, matches[1:])
141+				route.Handler(w, r.WithContext(ctx))
142 				return
143 			}
144 		}
145@@ -364,5 +465,8 @@ func StartApiServer() {
146 
147 	portStr := fmt.Sprintf(":%s", cfg.Port)
148 	client.Logger.Info("starting server on port", "port", cfg.Port)
149-	client.Logger.Error(http.ListenAndServe(portStr, router).Error())
150+	err := http.ListenAndServe(portStr, router)
151+	if err != nil {
152+		client.Logger.Info(err.Error())
153+	}
154 }