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}