A URL shortener service that uses ATProto to allow self hosting and ensuring the user owns their data
1package atshorter
2
3import (
4 _ "embed"
5 "log/slog"
6 "net/http"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9)
10
11const (
12 sessionName = "at-shorter"
13)
14
15type LoginData struct {
16 Handle string
17 Error string
18}
19
20func (s *Server) authMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
21 return func(w http.ResponseWriter, r *http.Request) {
22 did, _ := s.currentSessionDID(r)
23 if did == nil {
24 http.Redirect(w, r, "/login", http.StatusFound)
25 return
26 }
27
28 next(w, r)
29 }
30}
31
32func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) {
33 tmpl := s.getTemplate("login.html")
34 data := LoginData{}
35 tmpl.Execute(w, data)
36}
37
38func (s *Server) HandlePostLogin(w http.ResponseWriter, r *http.Request) {
39 tmpl := s.getTemplate("login.html")
40 data := LoginData{}
41
42 err := r.ParseForm()
43 if err != nil {
44 slog.Error("parsing form", "error", err)
45 data.Error = "error parsing data"
46 tmpl.Execute(w, data)
47 return
48 }
49
50 handle := r.FormValue("handle")
51
52 redirectURL, err := s.oauthClient.StartAuthFlow(r.Context(), handle)
53 if err != nil {
54 slog.Error("starting oauth flow", "error", err)
55 data.Error = "error logging in"
56 tmpl.Execute(w, data)
57 return
58 }
59
60 http.Redirect(w, r, redirectURL, http.StatusFound)
61}
62
63func (s *Server) handleOauthCallback(w http.ResponseWriter, r *http.Request) {
64 tmpl := s.getTemplate("login.html")
65 data := LoginData{}
66
67 sessData, err := s.oauthClient.ProcessCallback(r.Context(), r.URL.Query())
68 if err != nil {
69 slog.Error("processing OAuth callback", "error", err)
70 data.Error = "error logging in"
71 tmpl.Execute(w, data)
72 return
73 }
74
75 // create signed cookie session, indicating account DID
76 sess, _ := s.sessionStore.Get(r, sessionName)
77 sess.Values["account_did"] = sessData.AccountDID.String()
78 sess.Values["session_id"] = sessData.SessionID
79 if err := sess.Save(r, w); err != nil {
80 slog.Error("storing session data", "error", err)
81 data.Error = "error logging in"
82 tmpl.Execute(w, data)
83 return
84 }
85
86 http.Redirect(w, r, "/", http.StatusFound)
87}
88
89func (s *Server) HandleLogOut(w http.ResponseWriter, r *http.Request) {
90 did, sessionID := s.currentSessionDID(r)
91 if did != nil {
92 err := s.oauthClient.Store.DeleteSession(r.Context(), *did, sessionID)
93 if err != nil {
94 slog.Error("deleting oauth session", "error", err)
95 }
96 }
97
98 sess, _ := s.sessionStore.Get(r, sessionName)
99 sess.Values = make(map[any]any)
100 err := sess.Save(r, w)
101 if err != nil {
102 http.Error(w, err.Error(), http.StatusInternalServerError)
103 return
104 }
105 http.Redirect(w, r, "/", http.StatusFound)
106}
107
108func (s *Server) currentSessionDID(r *http.Request) (*syntax.DID, string) {
109 sess, _ := s.sessionStore.Get(r, sessionName)
110 accountDID, ok := sess.Values["account_did"].(string)
111 if !ok || accountDID == "" {
112 return nil, ""
113 }
114 did, err := syntax.ParseDID(accountDID)
115 if err != nil {
116 return nil, ""
117 }
118 sessionID, ok := sess.Values["session_id"].(string)
119 if !ok || sessionID == "" {
120 return nil, ""
121 }
122
123 return &did, sessionID
124}