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}