Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242package handler
import ( "context" "database/sql" _ "embed" "encoding/json" "fmt" "html/template" "net/http" "strings"
"github.com/bluesky-social/indigo/api/agnostic" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/did-method-plc/go-didplc" "github.com/gorilla/sessions" "tangled.org/core/knot2/config" "tangled.org/core/knot2/db" "tangled.org/core/log")
const ( // atproto serviceId = "tangled_knot" serviceType = "TangledKnot" // cookies sessionName = "oauth-demo" sessionId = "sessionId" sessionDid = "sessionDID")
//go:embed "templates/register.html"var tmplRegisgerText stringvar tmplRegister = template.Must(template.New("register.html").Parse(tmplRegisgerText))
func Register(jar *sessions.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "Register")
sess, _ := jar.Get(r, sessionName) var data map[string]any
if !sess.IsNew { // render Register { Handle, Web: true } did := syntax.DID(sess.Values[sessionDid].(string)) plcop := did.Method() == "plc" && r.URL.Query().Get("method") != "web" data = map[string]any{ "Did": did, "PlcOp": plcop, } }
err := tmplRegister.Execute(w, data) if err != nil { l.Error("failed to render", "err", err) } }}
func OauthClientMetadata(cfg *config.Config, clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { doc := clientApp.Config.ClientMetadata() var ( clientName = cfg.HostName clientUri = cfg.Uri() jwksUri = clientUri + "/oauth/jwks.json" ) doc.ClientName = &clientName doc.ClientURI = &clientUri doc.JWKSURI = &jwksUri
w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(doc); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }}
func OauthJwks(clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") body := clientApp.Config.PublicJWKS() if err := json.NewEncoder(w).Encode(body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }}
func OauthLoginPost(clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "OauthLoginPost")
handle := r.FormValue("handle")
handle = strings.TrimPrefix(handle, "\u202a") handle = strings.TrimSuffix(handle, "\u202c") // `@` is harmless handle = strings.TrimPrefix(handle, "@")
redirectURL, err := clientApp.StartAuthFlow(ctx, handle) if err != nil { l.Error("failed to start auth flow", "err", err) panic(err) }
w.Header().Set("HX-Redirect", redirectURL) w.WriteHeader(http.StatusOK) }}
func OauthCallback(oauth *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "OauthCallback")
data, err := oauth.ProcessCallback(ctx, r.URL.Query()) if err != nil { l.Error("failed to process oauth callback", "err", err) panic(err) }
// store session data to cookie jar sess, _ := jar.Get(r, sessionName) sess.Values[sessionDid] = data.AccountDID.String() sess.Values[sessionId] = data.SessionID if err = sess.Save(r, w); err != nil { l.Error("failed to save session", "err", err) panic(err) }
if data.AccountDID.Method() == "plc" { sess, err := oauth.ResumeSession(ctx, data.AccountDID, data.SessionID) if err != nil { l.Error("failed to resume atproto session", "err", err) panic(err) } client := sess.APIClient() err = atproto.IdentityRequestPlcOperationSignature(ctx, client) if err != nil { l.Error("failed to request plc operation signature", "err", err) panic(err) } }
http.Redirect(w, r, "/register", http.StatusSeeOther) }}
func RegisterPost(cfg *config.Config, d *sql.DB, clientApp *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc { plcop := func(ctx context.Context, did syntax.DID, sessId, token string) error { sess, err := clientApp.ResumeSession(ctx, did, sessId) if err != nil { return fmt.Errorf("failed to resume atproto session: %w", err) } client := sess.APIClient()
identity, err := clientApp.Dir.LookupDID(ctx, did) services := make(map[string]didplc.OpService) for id, service := range identity.Services { services[id] = didplc.OpService{ Type: service.Type, Endpoint: service.URL, } } services[serviceId] = didplc.OpService{ Type: serviceType, Endpoint: cfg.Uri(), }
rawServices, err := json.Marshal(services) if err != nil { return fmt.Errorf("failed to marshal services map: %w", err) } raw := json.RawMessage(rawServices)
signed, err := agnostic.IdentitySignPlcOperation(ctx, client, &agnostic.IdentitySignPlcOperation_Input{ Services: &raw, Token: &token, }) if err != nil { return fmt.Errorf("failed to sign plc operatino: %w", err) }
err = agnostic.IdentitySubmitPlcOperation(ctx, client, &agnostic.IdentitySubmitPlcOperation_Input{ Operation: signed.Operation, }) if err != nil { return fmt.Errorf("failed to submit plc operatino: %w", err) }
return nil } return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "RegisterPost")
sess, _ := jar.Get(r, sessionName)
var ( did = syntax.DID(sess.Values[sessionDid].(string)) sessId = sess.Values[sessionId].(string) token = r.FormValue("token") doPlcOp = r.FormValue("plcop") == "on" )
tx, err := d.BeginTx(ctx, nil) if err != nil { l.Error("failed to begin db tx", "err", err) panic(err) } defer tx.Rollback()
if err := db.AddUser(tx, did); err != nil { l.Error("failed to add user", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) return }
if doPlcOp { l.Debug("performing plc op", "did", did, "token", token) if err := plcop(ctx, did, sessId, token); err != nil { l.Error("failed to perform plc op", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) } } else { // TODO: check if did doc already include the knot service tx.Rollback() panic("unimplemented") } if err := tx.Commit(); err != nil { l.Error("failed to commit tx", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) } }}