Yōten: A social tracker for your language learning journey built on the atproto.

fix: accounts not created on sign-in

Signed-off-by: brookjeynes <me@brookjeynes.dev>

+96 -11
+1 -1
internal/server/app.go
··· 63 63 64 64 idResolver := atproto.DefaultResolver() 65 65 66 - oauth, err := oauth.New(config, posthog, log.SubLogger(logger, "oauth")) 66 + oauth, err := oauth.New(config, posthog, idResolver, log.SubLogger(logger, "oauth")) 67 67 if err != nil { 68 68 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 69 69 }
+2 -2
internal/server/handlers/login.go
··· 56 56 // Basic handle validation 57 57 if !strings.Contains(handle, ".") { 58 58 l.Error("invalid handle format", "handle", handle) 59 - htmx.HxError(w, http.StatusBadGateway, fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 59 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 60 60 return 61 61 } 62 62 63 63 resolved, err := h.IdResolver.ResolveIdent(context.Background(), handle) 64 64 if err != nil { 65 65 l.Error("failed to resolve handle", "handle", handle, "err", err) 66 - htmx.HxError(w, http.StatusBadGateway, fmt.Sprintf("'%s' is an invalid handle", handle)) 66 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", handle)) 67 67 return 68 68 } else { 69 69 if !h.Config.Core.Dev && resolved.DID.String() != "" {
+1 -1
internal/server/handlers/profile.go
··· 130 130 }) 131 131 132 132 if err := g.Wait(); err != nil { 133 - l.Error("failed to fetch critical profile data for", "did", profileDid, "err", err) 133 + l.Error("failed to fetch critical profile data", "did", profileDid, "err", err) 134 134 htmx.HxError(w, http.StatusInternalServerError, "Failed to fetch profile data, try again later.") 135 135 return 136 136 }
+88 -6
internal/server/oauth/handler.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "context" 4 5 "encoding/json" 6 + "fmt" 5 7 "net/http" 8 + "time" 6 9 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 7 12 "github.com/go-chi/chi/v5" 8 13 "github.com/lestrrat-go/jwx/v2/jwk" 9 14 "github.com/posthog/posthog-go" 10 15 16 + "yoten.app/api/yoten" 11 17 ph "yoten.app/internal/clients/posthog" 18 + "yoten.app/internal/db" 19 + "yoten.app/internal/server/htmx" 12 20 ) 13 21 14 22 func (o *OAuth) Router() http.Handler { ··· 63 71 } 64 72 65 73 func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 74 + l := o.Logger.With("handler", "callback") 66 75 ctx := r.Context() 67 76 68 77 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) ··· 73 82 } 74 83 75 84 if err := o.SaveSession(w, r, sessData); err != nil { 76 - o.Logger.Error("failed to save session", "err", err) 85 + l.Error("failed to save session", "err", err) 86 + http.Error(w, err.Error(), http.StatusInternalServerError) 87 + return 88 + } 89 + 90 + did := sessData.AccountDID.String() 91 + resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) 92 + if err != nil { 93 + l.Error("failed to resolve handle", "handle", resolved.Handle.String(), "err", err) 94 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", resolved.Handle.String())) 95 + return 96 + } 97 + 98 + client, err := o.AuthorizedClient(r) 99 + if err != nil { 100 + l.Error("failed to get authorized client", "err", err) 77 101 http.Error(w, err.Error(), http.StatusInternalServerError) 78 102 return 79 103 } 80 104 81 - if !o.Config.Core.Dev { 82 - err = o.Posthog.Enqueue(posthog.Capture{ 83 - DistinctId: sessData.AccountDID.String(), 84 - Event: ph.UserSignInSuccessEvent, 105 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", yoten.ActorProfileNSID, did, "self") 106 + var cid *string 107 + if ex != nil { 108 + cid = ex.Cid 109 + } 110 + 111 + // This should only occur once per account 112 + if ex == nil { 113 + createdAt := time.Now().Format(time.RFC3339) 114 + atresp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 115 + Collection: yoten.ActorProfileNSID, 116 + Repo: did, 117 + Rkey: "self", 118 + Record: &lexutil.LexiconTypeDecoder{ 119 + Val: &yoten.ActorProfile{ 120 + DisplayName: resolved.Handle.String(), 121 + Description: db.ToPtr(""), 122 + Languages: make([]string, 0), 123 + Location: db.ToPtr(""), 124 + CreatedAt: createdAt, 125 + }}, 126 + 127 + SwapRecord: cid, 85 128 }) 86 129 if err != nil { 87 - o.Logger.Error("failed to enqueue posthog event", "err", err) 130 + l.Error("failed to create profile record", "err", err) 131 + htmx.HxError(w, http.StatusInternalServerError, "Failed to announce profile creation, try again later") 132 + return 133 + } 134 + 135 + l.Debug("created profile record", "uri", atresp.Uri) 136 + 137 + if !o.Config.Core.Dev { 138 + err = o.Posthog.Enqueue(posthog.Capture{ 139 + DistinctId: sessData.AccountDID.String(), 140 + Event: ph.UserSignInSuccessEvent, 141 + }) 142 + if err != nil { 143 + l.Error("failed to enqueue posthog event", "err", err) 144 + } 145 + 146 + properties := posthog.NewProperties(). 147 + Set("display_name", resolved.Handle.String()). 148 + Set("language_count", 0). 149 + Set("$set_once", posthog.NewProperties(). 150 + Set("initial_did", did). 151 + Set("initial_handle", resolved.Handle.String()). 152 + Set("created_at", createdAt), 153 + ) 154 + 155 + err = o.Posthog.Enqueue(posthog.Identify{ 156 + DistinctId: did, 157 + Properties: properties, 158 + }) 159 + if err != nil { 160 + l.Error("failed to enqueue posthog identify event", "err", err) 161 + } 162 + 163 + err = o.Posthog.Enqueue(posthog.Capture{ 164 + DistinctId: did, 165 + Event: ph.ProfileRecordCreatedEvent, 166 + }) 167 + if err != nil { 168 + l.Error("failed to enqueue posthog event", "err", err) 169 + } 88 170 } 89 171 } 90 172
+4 -1
internal/server/oauth/oauth.go
··· 15 15 "github.com/gorilla/sessions" 16 16 "github.com/posthog/posthog-go" 17 17 18 + "yoten.app/internal/atproto" 18 19 "yoten.app/internal/server/config" 19 20 "yoten.app/internal/types" 20 21 ) ··· 26 27 JwksUri string 27 28 Posthog posthog.Client 28 29 Logger *slog.Logger 30 + IdResolver *atproto.Resolver 29 31 } 30 32 31 - func New(config *config.Config, ph posthog.Client, logger *slog.Logger) (*OAuth, error) { 33 + func New(config *config.Config, ph posthog.Client, idResolver *atproto.Resolver, logger *slog.Logger) (*OAuth, error) { 32 34 var oauthConfig oauth.ClientConfig 33 35 var clientUri string 34 36 ··· 58 60 SessionStore: sessStore, 59 61 JwksUri: jwksUri, 60 62 Posthog: ph, 63 + IdResolver: idResolver, 61 64 Logger: logger, 62 65 }, nil 63 66