This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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