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 64 idResolver := atproto.DefaultResolver() 65 66 - oauth, err := oauth.New(config, posthog, log.SubLogger(logger, "oauth")) 67 if err != nil { 68 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 69 }
··· 63 64 idResolver := atproto.DefaultResolver() 65 66 + oauth, err := oauth.New(config, posthog, idResolver, log.SubLogger(logger, "oauth")) 67 if err != nil { 68 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 69 }
+2 -2
internal/server/handlers/login.go
··· 56 // Basic handle validation 57 if !strings.Contains(handle, ".") { 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)) 60 return 61 } 62 63 resolved, err := h.IdResolver.ResolveIdent(context.Background(), handle) 64 if err != nil { 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)) 67 return 68 } else { 69 if !h.Config.Core.Dev && resolved.DID.String() != "" {
··· 56 // Basic handle validation 57 if !strings.Contains(handle, ".") { 58 l.Error("invalid handle format", "handle", handle) 59 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 60 return 61 } 62 63 resolved, err := h.IdResolver.ResolveIdent(context.Background(), handle) 64 if err != nil { 65 l.Error("failed to resolve handle", "handle", handle, "err", err) 66 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", handle)) 67 return 68 } else { 69 if !h.Config.Core.Dev && resolved.DID.String() != "" {
+1 -1
internal/server/handlers/profile.go
··· 130 }) 131 132 if err := g.Wait(); err != nil { 133 - l.Error("failed to fetch critical profile data for", "did", profileDid, "err", err) 134 htmx.HxError(w, http.StatusInternalServerError, "Failed to fetch profile data, try again later.") 135 return 136 }
··· 130 }) 131 132 if err := g.Wait(); err != nil { 133 + l.Error("failed to fetch critical profile data", "did", profileDid, "err", err) 134 htmx.HxError(w, http.StatusInternalServerError, "Failed to fetch profile data, try again later.") 135 return 136 }
+88 -6
internal/server/oauth/handler.go
··· 1 package oauth 2 3 import ( 4 "encoding/json" 5 "net/http" 6 7 "github.com/go-chi/chi/v5" 8 "github.com/lestrrat-go/jwx/v2/jwk" 9 "github.com/posthog/posthog-go" 10 11 ph "yoten.app/internal/clients/posthog" 12 ) 13 14 func (o *OAuth) Router() http.Handler { ··· 63 } 64 65 func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 66 ctx := r.Context() 67 68 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) ··· 73 } 74 75 if err := o.SaveSession(w, r, sessData); err != nil { 76 - o.Logger.Error("failed to save session", "err", err) 77 http.Error(w, err.Error(), http.StatusInternalServerError) 78 return 79 } 80 81 - if !o.Config.Core.Dev { 82 - err = o.Posthog.Enqueue(posthog.Capture{ 83 - DistinctId: sessData.AccountDID.String(), 84 - Event: ph.UserSignInSuccessEvent, 85 }) 86 if err != nil { 87 - o.Logger.Error("failed to enqueue posthog event", "err", err) 88 } 89 } 90
··· 1 package oauth 2 3 import ( 4 + "context" 5 "encoding/json" 6 + "fmt" 7 "net/http" 8 + "time" 9 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 "github.com/go-chi/chi/v5" 13 "github.com/lestrrat-go/jwx/v2/jwk" 14 "github.com/posthog/posthog-go" 15 16 + "yoten.app/api/yoten" 17 ph "yoten.app/internal/clients/posthog" 18 + "yoten.app/internal/db" 19 + "yoten.app/internal/server/htmx" 20 ) 21 22 func (o *OAuth) Router() http.Handler { ··· 71 } 72 73 func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 74 + l := o.Logger.With("handler", "callback") 75 ctx := r.Context() 76 77 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) ··· 82 } 83 84 if err := o.SaveSession(w, r, sessData); err != nil { 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) 101 http.Error(w, err.Error(), http.StatusInternalServerError) 102 return 103 } 104 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, 128 }) 129 if err != nil { 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 + } 170 } 171 } 172
+4 -1
internal/server/oauth/oauth.go
··· 15 "github.com/gorilla/sessions" 16 "github.com/posthog/posthog-go" 17 18 "yoten.app/internal/server/config" 19 "yoten.app/internal/types" 20 ) ··· 26 JwksUri string 27 Posthog posthog.Client 28 Logger *slog.Logger 29 } 30 31 - func New(config *config.Config, ph posthog.Client, logger *slog.Logger) (*OAuth, error) { 32 var oauthConfig oauth.ClientConfig 33 var clientUri string 34 ··· 58 SessionStore: sessStore, 59 JwksUri: jwksUri, 60 Posthog: ph, 61 Logger: logger, 62 }, nil 63
··· 15 "github.com/gorilla/sessions" 16 "github.com/posthog/posthog-go" 17 18 + "yoten.app/internal/atproto" 19 "yoten.app/internal/server/config" 20 "yoten.app/internal/types" 21 ) ··· 27 JwksUri string 28 Posthog posthog.Client 29 Logger *slog.Logger 30 + IdResolver *atproto.Resolver 31 } 32 33 + func New(config *config.Config, ph posthog.Client, idResolver *atproto.Resolver, logger *slog.Logger) (*OAuth, error) { 34 var oauthConfig oauth.ClientConfig 35 var clientUri string 36 ··· 60 SessionStore: sessStore, 61 JwksUri: jwksUri, 62 Posthog: ph, 63 + IdResolver: idResolver, 64 Logger: logger, 65 }, nil 66