A vibe coded tangled fork which supports pijul.
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}