- 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
+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 }