A URL shortener service that uses ATProto to allow self hosting and ensuring the user owns their data
at main 3.1 kB view raw
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 if did.String() != s.usersDID { 29 http.Error(w, "not authorized", http.StatusUnauthorized) 30 return 31 } 32 33 next(w, r) 34 } 35} 36 37func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { 38 tmpl := s.getTemplate("login.html") 39 data := LoginData{} 40 tmpl.Execute(w, data) 41} 42 43func (s *Server) HandlePostLogin(w http.ResponseWriter, r *http.Request) { 44 tmpl := s.getTemplate("login.html") 45 data := LoginData{} 46 47 err := r.ParseForm() 48 if err != nil { 49 slog.Error("parsing form", "error", err) 50 data.Error = "error parsing data" 51 tmpl.Execute(w, data) 52 return 53 } 54 55 handle := r.FormValue("handle") 56 57 redirectURL, err := s.oauthClient.StartAuthFlow(r.Context(), handle) 58 if err != nil { 59 slog.Error("starting oauth flow", "error", err) 60 data.Error = "error logging in" 61 tmpl.Execute(w, data) 62 return 63 } 64 65 http.Redirect(w, r, redirectURL, http.StatusFound) 66} 67 68func (s *Server) handleOauthCallback(w http.ResponseWriter, r *http.Request) { 69 tmpl := s.getTemplate("login.html") 70 data := LoginData{} 71 72 sessData, err := s.oauthClient.ProcessCallback(r.Context(), r.URL.Query()) 73 if err != nil { 74 slog.Error("processing OAuth callback", "error", err) 75 data.Error = "error logging in" 76 tmpl.Execute(w, data) 77 return 78 } 79 80 // create signed cookie session, indicating account DID 81 sess, _ := s.sessionStore.Get(r, sessionName) 82 sess.Values["account_did"] = sessData.AccountDID.String() 83 sess.Values["session_id"] = sessData.SessionID 84 if err := sess.Save(r, w); err != nil { 85 slog.Error("storing session data", "error", err) 86 data.Error = "error logging in" 87 tmpl.Execute(w, data) 88 return 89 } 90 91 http.Redirect(w, r, "/", http.StatusFound) 92} 93 94func (s *Server) HandleLogOut(w http.ResponseWriter, r *http.Request) { 95 did, sessionID := s.currentSessionDID(r) 96 if did != nil { 97 err := s.oauthClient.Store.DeleteSession(r.Context(), *did, sessionID) 98 if err != nil { 99 slog.Error("deleting oauth session", "error", err) 100 } 101 } 102 103 sess, _ := s.sessionStore.Get(r, sessionName) 104 sess.Values = make(map[any]any) 105 err := sess.Save(r, w) 106 if err != nil { 107 http.Error(w, err.Error(), http.StatusInternalServerError) 108 return 109 } 110 http.Redirect(w, r, "/", http.StatusFound) 111} 112 113func (s *Server) currentSessionDID(r *http.Request) (*syntax.DID, string) { 114 sess, _ := s.sessionStore.Get(r, sessionName) 115 accountDID, ok := sess.Values["account_did"].(string) 116 if !ok || accountDID == "" { 117 return nil, "" 118 } 119 did, err := syntax.ParseDID(accountDID) 120 if err != nil { 121 return nil, "" 122 } 123 sessionID, ok := sess.Values["session_id"].(string) 124 if !ok || sessionID == "" { 125 return nil, "" 126 } 127 128 return &did, sessionID 129}