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