this repo has no description
1package handler
2
3import (
4 "context"
5 "database/sql"
6 _ "embed"
7 "encoding/json"
8 "fmt"
9 "html/template"
10 "net/http"
11 "strings"
12
13 "github.com/bluesky-social/indigo/api/agnostic"
14 "github.com/bluesky-social/indigo/api/atproto"
15 "github.com/bluesky-social/indigo/atproto/auth/oauth"
16 "github.com/bluesky-social/indigo/atproto/syntax"
17 "github.com/did-method-plc/go-didplc"
18 "github.com/gorilla/sessions"
19 "tangled.org/core/knot2/config"
20 "tangled.org/core/knot2/db"
21 "tangled.org/core/log"
22)
23
24const (
25 // atproto
26 serviceId = "tangled_knot"
27 serviceType = "TangledKnot"
28 // cookies
29 sessionName = "oauth-demo"
30 sessionId = "sessionId"
31 sessionDid = "sessionDID"
32)
33
34//go:embed "templates/register.html"
35var tmplRegisgerText string
36var tmplRegister = template.Must(template.New("register.html").Parse(tmplRegisgerText))
37
38func Register(jar *sessions.CookieStore) http.HandlerFunc {
39 return func(w http.ResponseWriter, r *http.Request) {
40 ctx := r.Context()
41 l := log.FromContext(ctx).With("handler", "Register")
42
43 sess, _ := jar.Get(r, sessionName)
44 var data map[string]any
45
46 if !sess.IsNew {
47 // render Register { Handle, Web: true }
48 did := syntax.DID(sess.Values[sessionDid].(string))
49 plcop := did.Method() == "plc" && r.URL.Query().Get("method") != "web"
50 data = map[string]any{
51 "Did": did,
52 "PlcOp": plcop,
53 }
54 }
55
56 err := tmplRegister.Execute(w, data)
57 if err != nil {
58 l.Error("failed to render", "err", err)
59 }
60 }
61}
62
63func OauthClientMetadata(cfg *config.Config, clientApp *oauth.ClientApp) http.HandlerFunc {
64 return func(w http.ResponseWriter, r *http.Request) {
65 doc := clientApp.Config.ClientMetadata()
66 var (
67 clientName = cfg.HostName
68 clientUri = cfg.Uri()
69 jwksUri = clientUri + "/oauth/jwks.json"
70 )
71 doc.ClientName = &clientName
72 doc.ClientURI = &clientUri
73 doc.JWKSURI = &jwksUri
74
75 w.Header().Set("Content-Type", "application/json")
76 if err := json.NewEncoder(w).Encode(doc); err != nil {
77 http.Error(w, err.Error(), http.StatusInternalServerError)
78 return
79 }
80 }
81}
82
83func OauthJwks(clientApp *oauth.ClientApp) http.HandlerFunc {
84 return func(w http.ResponseWriter, r *http.Request) {
85 w.Header().Set("Content-Type", "application/json")
86 body := clientApp.Config.PublicJWKS()
87 if err := json.NewEncoder(w).Encode(body); err != nil {
88 http.Error(w, err.Error(), http.StatusInternalServerError)
89 return
90 }
91 }
92}
93
94func OauthLoginPost(clientApp *oauth.ClientApp) http.HandlerFunc {
95 return func(w http.ResponseWriter, r *http.Request) {
96 ctx := r.Context()
97 l := log.FromContext(ctx).With("handler", "OauthLoginPost")
98
99 handle := r.FormValue("handle")
100
101 handle = strings.TrimPrefix(handle, "\u202a")
102 handle = strings.TrimSuffix(handle, "\u202c")
103 // `@` is harmless
104 handle = strings.TrimPrefix(handle, "@")
105
106 redirectURL, err := clientApp.StartAuthFlow(ctx, handle)
107 if err != nil {
108 l.Error("failed to start auth flow", "err", err)
109 panic(err)
110 }
111
112 w.Header().Set("HX-Redirect", redirectURL)
113 w.WriteHeader(http.StatusOK)
114 }
115}
116
117func OauthCallback(oauth *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc {
118 return func(w http.ResponseWriter, r *http.Request) {
119 ctx := r.Context()
120 l := log.FromContext(ctx).With("handler", "OauthCallback")
121
122 data, err := oauth.ProcessCallback(ctx, r.URL.Query())
123 if err != nil {
124 l.Error("failed to process oauth callback", "err", err)
125 panic(err)
126 }
127
128 // store session data to cookie jar
129 sess, _ := jar.Get(r, sessionName)
130 sess.Values[sessionDid] = data.AccountDID.String()
131 sess.Values[sessionId] = data.SessionID
132 if err = sess.Save(r, w); err != nil {
133 l.Error("failed to save session", "err", err)
134 panic(err)
135 }
136
137 if data.AccountDID.Method() == "plc" {
138 sess, err := oauth.ResumeSession(ctx, data.AccountDID, data.SessionID)
139 if err != nil {
140 l.Error("failed to resume atproto session", "err", err)
141 panic(err)
142 }
143 client := sess.APIClient()
144 err = atproto.IdentityRequestPlcOperationSignature(ctx, client)
145 if err != nil {
146 l.Error("failed to request plc operation signature", "err", err)
147 panic(err)
148 }
149 }
150
151 http.Redirect(w, r, "/register", http.StatusSeeOther)
152 }
153}
154
155func RegisterPost(cfg *config.Config, d *sql.DB, clientApp *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc {
156 plcop := func(ctx context.Context, did syntax.DID, sessId, token string) error {
157 sess, err := clientApp.ResumeSession(ctx, did, sessId)
158 if err != nil {
159 return fmt.Errorf("failed to resume atproto session: %w", err)
160 }
161 client := sess.APIClient()
162
163 identity, err := clientApp.Dir.LookupDID(ctx, did)
164 services := make(map[string]didplc.OpService)
165 for id, service := range identity.Services {
166 services[id] = didplc.OpService{
167 Type: service.Type,
168 Endpoint: service.URL,
169 }
170 }
171 services[serviceId] = didplc.OpService{
172 Type: serviceType,
173 Endpoint: cfg.Uri(),
174 }
175
176 rawServices, err := json.Marshal(services)
177 if err != nil {
178 return fmt.Errorf("failed to marshal services map: %w", err)
179 }
180 raw := json.RawMessage(rawServices)
181
182 signed, err := agnostic.IdentitySignPlcOperation(ctx, client, &agnostic.IdentitySignPlcOperation_Input{
183 Services: &raw,
184 Token: &token,
185 })
186 if err != nil {
187 return fmt.Errorf("failed to sign plc operatino: %w", err)
188 }
189
190 err = agnostic.IdentitySubmitPlcOperation(ctx, client, &agnostic.IdentitySubmitPlcOperation_Input{
191 Operation: signed.Operation,
192 })
193 if err != nil {
194 return fmt.Errorf("failed to submit plc operatino: %w", err)
195 }
196
197 return nil
198 }
199 return func(w http.ResponseWriter, r *http.Request) {
200 ctx := r.Context()
201 l := log.FromContext(ctx).With("handler", "RegisterPost")
202
203 sess, _ := jar.Get(r, sessionName)
204
205 var (
206 did = syntax.DID(sess.Values[sessionDid].(string))
207 sessId = sess.Values[sessionId].(string)
208 token = r.FormValue("token")
209 doPlcOp = r.FormValue("plcop") == "on"
210 )
211
212 tx, err := d.BeginTx(ctx, nil)
213 if err != nil {
214 l.Error("failed to begin db tx", "err", err)
215 panic(err)
216 }
217 defer tx.Rollback()
218
219 if err := db.AddUser(tx, did); err != nil {
220 l.Error("failed to add user", "err", err)
221 http.Error(w, err.Error(), http.StatusInternalServerError)
222 return
223 }
224
225 if doPlcOp {
226 l.Debug("performing plc op", "did", did, "token", token)
227 if err := plcop(ctx, did, sessId, token); err != nil {
228 l.Error("failed to perform plc op", "err", err)
229 http.Error(w, err.Error(), http.StatusInternalServerError)
230 }
231 } else {
232 // TODO: check if did doc already include the knot service
233 tx.Rollback()
234 panic("unimplemented")
235 }
236 if err := tx.Commit(); err != nil {
237 l.Error("failed to commit tx", "err", err)
238 http.Error(w, err.Error(), http.StatusInternalServerError)
239 }
240 }
241}