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