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