Monorepo for Tangled
1package state
2
3import (
4 "fmt"
5 "net/http"
6 "strings"
7
8 "tangled.org/core/appview/oauth"
9 "tangled.org/core/appview/pages"
10)
11
12func (s *State) Login(w http.ResponseWriter, r *http.Request) {
13 l := s.logger.With("handler", "Login")
14
15 switch r.Method {
16 case http.MethodGet:
17 returnURL := r.URL.Query().Get("return_url")
18 errorCode := r.URL.Query().Get("error")
19 addAccount := r.URL.Query().Get("mode") == "add_account"
20
21 user := s.oauth.GetMultiAccountUser(r)
22 if user == nil {
23 registry := s.oauth.GetAccounts(r)
24 if len(registry.Accounts) > 0 {
25 user = &oauth.MultiAccountUser{
26 Active: nil,
27 Accounts: registry.Accounts,
28 }
29 }
30 }
31 s.pages.Login(w, pages.LoginParams{
32 ReturnUrl: returnURL,
33 ErrorCode: errorCode,
34 AddAccount: addAccount,
35 LoggedInUser: user,
36 })
37 case http.MethodPost:
38 handle := r.FormValue("handle")
39 returnURL := r.FormValue("return_url")
40 addAccount := r.FormValue("add_account") == "true"
41
42 // remove spaces around the handle, handles can't have spaces around them
43 handle = strings.TrimSpace(handle)
44
45 // when users copy their handle from bsky.app, it tends to have these characters around it:
46 //
47 // @nelind.dk:
48 // \u202a ensures that the handle is always rendered left to right and
49 // \u202c reverts that so the rest of the page renders however it should
50 handle = strings.TrimPrefix(handle, "\u202a")
51 handle = strings.TrimSuffix(handle, "\u202c")
52
53 // `@` is harmless
54 handle = strings.TrimPrefix(handle, "@")
55
56 // basic handle validation
57 if !strings.Contains(handle, ".") {
58 l.Error("invalid handle format", "raw", handle)
59 s.pages.Notice(
60 w,
61 "login-msg",
62 fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social or %s.tngl.sh?", handle, handle, handle),
63 )
64 return
65 }
66
67 if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil {
68 l.Error("failed to set auth return", "err", err)
69 }
70
71 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
72 if err != nil {
73 l.Error("failed to start auth", "err", err)
74 s.pages.Notice(
75 w,
76 "login-msg",
77 fmt.Sprintf("Failed to start auth flow: %v", err),
78 )
79 return
80 }
81
82 s.pages.HxRedirect(w, redirectURL)
83 }
84}
85
86func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
87 l := s.logger.With("handler", "Logout")
88
89 currentUser := s.oauth.GetMultiAccountUser(r)
90 if currentUser == nil || currentUser.Active == nil {
91 s.pages.HxRedirect(w, "/login")
92 return
93 }
94
95 currentDid := currentUser.Active.Did
96
97 var remainingAccounts []string
98 for _, acc := range currentUser.Accounts {
99 if acc.Did != currentDid {
100 remainingAccounts = append(remainingAccounts, acc.Did)
101 }
102 }
103
104 if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil {
105 l.Error("failed to remove account from registry", "err", err)
106 }
107
108 if err := s.oauth.DeleteSession(w, r); err != nil {
109 l.Error("failed to delete session", "err", err)
110 }
111
112 if len(remainingAccounts) > 0 {
113 nextDid := remainingAccounts[0]
114 if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil {
115 l.Error("failed to switch to next account", "err", err)
116 s.pages.HxRedirect(w, "/login")
117 return
118 }
119 l.Info("switched to next account after logout", "did", nextDid)
120 s.pages.HxRefresh(w)
121 return
122 }
123
124 l.Info("logged out last account")
125 s.pages.HxRedirect(w, "/login")
126}