package oauth import ( "context" "encoding/json" "errors" "fmt" "net/http" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/auth/oauth" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/go-chi/chi/v5" "github.com/posthog/posthog-go" "yoten.app/api/yoten" ph "yoten.app/internal/clients/posthog" "yoten.app/internal/db" ) func (o *OAuth) Router() http.Handler { r := chi.NewRouter() r.Get("/oauth/client-metadata.json", o.clientMetadata) r.Get("/oauth/jwks.json", o.jwks) r.Get("/oauth/callback", o.callback) return r } func (o *OAuth) clientMetadata(w http.ResponseWriter, r *http.Request) { clientName := ClientName clientUri := ClientURI meta := o.ClientApp.Config.ClientMetadata() meta.JWKSURI = &o.JwksUri meta.ClientName = &clientName meta.ClientURI = &clientUri w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(meta); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") body := o.ClientApp.Config.PublicJWKS() if err := json.NewEncoder(w).Encode(body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := o.Logger.With("handler", "callback").With("query", r.URL.Query()) authReturn := o.GetAuthReturn(r) _ = o.ClearAuthReturn(w, r) sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) if err != nil { var callbackErr *oauth.AuthRequestCallbackError if errors.As(err, &callbackErr) { l.Debug("callback error", "err", callbackErr) http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound) return } l.Error("failed to process callback", "err", err) http.Redirect(w, r, "/login?error=oauth", http.StatusFound) return } if err := o.SaveSession(w, r, sessData); err != nil { l.Error("failed to save session", "err", err) http.Redirect(w, r, "/login?error=session", http.StatusFound) return } did := sessData.AccountDID.String() resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) if err != nil { l.Error("failed to resolve handle", "handle", resolved.Handle.String(), "err", err) http.Redirect(w, r, "/login?error=handle", http.StatusFound) return } clientSession, err := o.ClientApp.ResumeSession(r.Context(), sessData.AccountDID, sessData.SessionID) if err != nil { l.Error("failed to get authorized client", "err", err) http.Redirect(w, r, "/login?error=client", http.StatusFound) return } client := clientSession.APIClient() ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", yoten.ActorProfileNSID, did, "self") var cid *string if ex != nil { cid = ex.Cid } // This should only occur once per account if ex == nil { createdAt := time.Now().Format(time.RFC3339) atresp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: yoten.ActorProfileNSID, Repo: did, Rkey: "self", Record: &lexutil.LexiconTypeDecoder{ Val: &yoten.ActorProfile{ DisplayName: resolved.Handle.String(), Description: db.ToPtr(""), Languages: make([]string, 0), Location: db.ToPtr(""), CreatedAt: createdAt, }}, SwapRecord: cid, }) if err != nil { l.Error("failed to create profile record", "err", err) http.Redirect(w, r, "/login?error=profile-creation", http.StatusFound) return } l.Debug("created profile record", "uri", atresp.Uri) if !o.Config.Core.Dev { properties := posthog.NewProperties(). Set("display_name", resolved.Handle.String()). Set("language_count", 0). Set("$set_once", posthog.NewProperties(). Set("initial_did", did). Set("initial_handle", resolved.Handle.String()). Set("created_at", createdAt), ) err = o.Posthog.Enqueue(posthog.Identify{ DistinctId: did, Properties: properties, }) if err != nil { l.Error("failed to enqueue posthog identify event", "err", err) } err = o.Posthog.Enqueue(posthog.Capture{ DistinctId: did, Event: ph.ProfileRecordCreatedEvent, }) if err != nil { l.Error("failed to enqueue posthog event", "err", err) } } } if !o.Config.Core.Dev { err = o.Posthog.Enqueue(posthog.Capture{ DistinctId: sessData.AccountDID.String(), Event: ph.UserSignInSuccessEvent, }) if err != nil { l.Error("failed to enqueue posthog event", "err", err) } } redirectURL := "/" if authReturn.ReturnURL != "" { redirectURL = authReturn.ReturnURL } http.Redirect(w, r, redirectURL, http.StatusFound) }