A vibe coded tangled fork which supports pijul.
at fd9baa09b253b1f0d614f95665f63c3f81714c5f 159 lines 4.5 kB view raw
1package state 2 3import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "strings" 8 "time" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/xrpc" 12 "tangled.org/core/appview/oauth" 13 "tangled.org/core/appview/pages" 14) 15 16func (s *State) Login(w http.ResponseWriter, r *http.Request) { 17 l := s.logger.With("handler", "Login") 18 19 switch r.Method { 20 case http.MethodGet: 21 returnURL := r.URL.Query().Get("return_url") 22 errorCode := r.URL.Query().Get("error") 23 addAccount := r.URL.Query().Get("mode") == "add_account" 24 25 user := s.oauth.GetMultiAccountUser(r) 26 if user == nil { 27 registry := s.oauth.GetAccounts(r) 28 if len(registry.Accounts) > 0 { 29 user = &oauth.MultiAccountUser{ 30 Active: nil, 31 Accounts: registry.Accounts, 32 } 33 } 34 } 35 s.pages.Login(w, pages.LoginParams{ 36 ReturnUrl: returnURL, 37 ErrorCode: errorCode, 38 AddAccount: addAccount, 39 LoggedInUser: user, 40 }) 41 case http.MethodPost: 42 handle := r.FormValue("handle") 43 returnURL := r.FormValue("return_url") 44 addAccount := r.FormValue("add_account") == "true" 45 46 // remove spaces around the handle, handles can't have spaces around them 47 handle = strings.TrimSpace(handle) 48 49 // when users copy their handle from bsky.app, it tends to have these characters around it: 50 // 51 // @nelind.dk: 52 // \u202a ensures that the handle is always rendered left to right and 53 // \u202c reverts that so the rest of the page renders however it should 54 handle = strings.TrimPrefix(handle, "\u202a") 55 handle = strings.TrimSuffix(handle, "\u202c") 56 57 // `@` is harmless 58 handle = strings.TrimPrefix(handle, "@") 59 60 // basic handle validation 61 if !strings.Contains(handle, ".") { 62 l.Error("invalid handle format", "raw", handle) 63 s.pages.Notice( 64 w, 65 "login-msg", 66 fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social or %s.tngl.sh?", handle, handle, handle), 67 ) 68 return 69 } 70 71 ident, err := s.idResolver.ResolveIdent(r.Context(), handle) 72 if err != nil { 73 l.Warn("handle resolution failed", "handle", handle, "err", err) 74 s.pages.Notice(w, "login-msg", fmt.Sprintf("Could not resolve handle \"%s\". The account may not exist.", handle)) 75 return 76 } 77 78 pdsEndpoint := ident.PDSEndpoint() 79 if pdsEndpoint == "" { 80 s.pages.Notice(w, "login-msg", fmt.Sprintf("No PDS found for \"%s\".", handle)) 81 return 82 } 83 84 pdsClient := &xrpc.Client{Host: pdsEndpoint, Client: &http.Client{Timeout: 5 * time.Second}} 85 _, err = comatproto.RepoDescribeRepo(r.Context(), pdsClient, ident.DID.String()) 86 if err != nil { 87 var xrpcErr *xrpc.Error 88 var xrpcBody *xrpc.XRPCError 89 isDeactivated := errors.As(err, &xrpcErr) && 90 errors.As(xrpcErr.Wrapped, &xrpcBody) && 91 xrpcBody.ErrStr == "RepoDeactivated" 92 93 if !isDeactivated { 94 l.Warn("describeRepo failed", "handle", handle, "did", ident.DID, "pds", pdsEndpoint, "err", err) 95 s.pages.Notice(w, "login-msg", fmt.Sprintf("Account \"%s\" is no longer available.", handle)) 96 return 97 } 98 } 99 100 if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil { 101 l.Error("failed to set auth return", "err", err) 102 } 103 104 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 105 if err != nil { 106 l.Error("failed to start auth", "err", err) 107 s.pages.Notice( 108 w, 109 "login-msg", 110 fmt.Sprintf("Failed to start auth flow: %v", err), 111 ) 112 return 113 } 114 115 s.pages.HxRedirect(w, redirectURL) 116 } 117} 118 119func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 120 l := s.logger.With("handler", "Logout") 121 122 currentUser := s.oauth.GetMultiAccountUser(r) 123 if currentUser == nil || currentUser.Active == nil { 124 s.pages.HxRedirect(w, "/login") 125 return 126 } 127 128 currentDid := currentUser.Active.Did 129 130 var remainingAccounts []string 131 for _, acc := range currentUser.Accounts { 132 if acc.Did != currentDid { 133 remainingAccounts = append(remainingAccounts, acc.Did) 134 } 135 } 136 137 if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil { 138 l.Error("failed to remove account from registry", "err", err) 139 } 140 141 if err := s.oauth.DeleteSession(w, r); err != nil { 142 l.Error("failed to delete session", "err", err) 143 } 144 145 if len(remainingAccounts) > 0 { 146 nextDid := remainingAccounts[0] 147 if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil { 148 l.Error("failed to switch to next account", "err", err) 149 s.pages.HxRedirect(w, "/login") 150 return 151 } 152 l.Info("switched to next account after logout", "did", nextDid) 153 s.pages.HxRefresh(w) 154 return 155 } 156 157 l.Info("logged out last account") 158 s.pages.HxRedirect(w, "/login") 159}