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

feat(oauth/handler): pass error codes back to login page

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

authored by brookjeynes.dev and committed by Tangled 2edc4459 d701f58c

Changed files
+41 -8
internal
server
handlers
oauth
views
+2
internal/server/handlers/login.go
··· 19 19 switch r.Method { 20 20 case http.MethodGet: 21 21 returnURL := r.URL.Query().Get("return_url") 22 + errorCode := r.URL.Query().Get("error") 22 23 views.LoginPage(views.LoginPageParams{ 23 24 ReturnUrl: returnURL, 25 + ErrorCode: errorCode, 24 26 }).Render(r.Context(), w) 25 27 case http.MethodPost: 26 28 handle := r.FormValue("handle")
+15 -8
internal/server/oauth/handler.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "errors" 6 7 "fmt" 7 8 "net/http" 8 9 "time" 9 10 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 12 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 13 lexutil "github.com/bluesky-social/indigo/lex/util" 12 14 "github.com/go-chi/chi/v5" 13 15 "github.com/lestrrat-go/jwx/v2/jwk" ··· 16 18 "yoten.app/api/yoten" 17 19 ph "yoten.app/internal/clients/posthog" 18 20 "yoten.app/internal/db" 19 - "yoten.app/internal/server/htmx" 20 21 ) 21 22 22 23 func (o *OAuth) Router() http.Handler { ··· 76 77 } 77 78 78 79 func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 79 - l := o.Logger.With("handler", "callback") 80 80 ctx := r.Context() 81 + l := o.Logger.With("handler", "callback").With("query", r.URL.Query()) 81 82 82 83 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 83 84 if err != nil { 84 - o.Logger.Error("failed to process callback", "err", err) 85 - http.Error(w, err.Error(), http.StatusInternalServerError) 85 + var callbackErr *oauth.AuthRequestCallbackError 86 + if errors.As(err, &callbackErr) { 87 + l.Debug("callback error", "err", callbackErr) 88 + http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound) 89 + return 90 + } 91 + l.Error("failed to process callback", "err", err) 92 + http.Redirect(w, r, "/login?error=oauth", http.StatusFound) 86 93 return 87 94 } 88 95 89 96 if err := o.SaveSession(w, r, sessData); err != nil { 90 97 l.Error("failed to save session", "err", err) 91 - http.Error(w, err.Error(), http.StatusInternalServerError) 98 + http.Redirect(w, r, "/login?error=session", http.StatusFound) 92 99 return 93 100 } 94 101 ··· 96 103 resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) 97 104 if err != nil { 98 105 l.Error("failed to resolve handle", "handle", resolved.Handle.String(), "err", err) 99 - htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", resolved.Handle.String())) 106 + http.Redirect(w, r, "/login?error=handle", http.StatusFound) 100 107 return 101 108 } 102 109 103 110 client, err := o.AuthorizedClient(r) 104 111 if err != nil { 105 112 l.Error("failed to get authorized client", "err", err) 106 - http.Error(w, err.Error(), http.StatusInternalServerError) 113 + http.Redirect(w, r, "/login?error=client", http.StatusFound) 107 114 return 108 115 } 109 116 ··· 133 140 }) 134 141 if err != nil { 135 142 l.Error("failed to create profile record", "err", err) 136 - htmx.HxError(w, http.StatusInternalServerError, "Failed to announce profile creation, try again later") 143 + http.Redirect(w, r, "/login?error=profile-creation", http.StatusFound) 137 144 return 138 145 } 139 146
+23
internal/server/views/login.templ
··· 44 44 <i class="w-4 h-4" data-lucide="arrow-right"></i> 45 45 </button> 46 46 </form> 47 + if params.ErrorCode != "" { 48 + <div class="flex flex-col my-4 p-4 bg-red-50 border border-red-500 rounded-lg text-red-500"> 49 + <div class="flex items-center gap-1"> 50 + <i class="w-4 h-4" data-lucide="circle-alert"></i> 51 + <h5 class="font-medium">Login error</h5> 52 + </div> 53 + <div> 54 + <p class="text-sm"> 55 + switch (params.ErrorCode) { 56 + case "access_denied": 57 + You have not authorized the app. 58 + case "session": 59 + Server failed to create user session. 60 + case "handle": 61 + Server failed to validate your handle. 62 + default: 63 + Internal Server error. 64 + } 65 + Please try again. 66 + </p> 67 + </div> 68 + </div> 69 + } 47 70 <div class="mt-6 pt-6 border-t border-bg-dark"> 48 71 <div class="text-center"> 49 72 <p class="text-sm text-text-muted mb-3">New to the AT Protocol?</p>
+1
internal/server/views/views.go
··· 18 18 19 19 type LoginPageParams struct { 20 20 ReturnUrl string 21 + ErrorCode string 21 22 } 22 23 23 24 type ProfilePageParams struct {