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