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 addAccount := r.FormValue("add_account") == "true"
31
32 // remove spaces around the handle, handles can't have spaces around them
33 handle = strings.TrimSpace(handle)
34
35 // when users copy their handle from bsky.app, it tends to have these characters around it:
36 //
37 // @nelind.dk:
38 // \u202a ensures that the handle is always rendered left to right and
39 // \u202c reverts that so the rest of the page renders however it should
40 handle = strings.TrimPrefix(handle, "\u202a")
41 handle = strings.TrimSuffix(handle, "\u202c")
42
43 // `@` is harmless
44 handle = strings.TrimPrefix(handle, "@")
45
46 // basic handle validation
47 if !strings.Contains(handle, ".") {
48 l.Error("invalid handle format", "raw", handle)
49 s.pages.Notice(
50 w,
51 "login-msg",
52 fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social or %s.tngl.sh?", handle, handle, handle),
53 )
54 return
55 }
56
57 if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil {
58 l.Error("failed to set auth return", "err", err)
59 }
60
61 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
62 if err != nil {
63 l.Error("failed to start auth", "err", err)
64 s.pages.Notice(
65 w,
66 "login-msg",
67 fmt.Sprintf("Failed to start auth flow: %v", err),
68 )
69 return
70 }
71
72 s.pages.HxRedirect(w, redirectURL)
73 }
74}
75
76func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
77 l := s.logger.With("handler", "Logout")
78
79 currentUser := s.oauth.GetMultiAccountUser(r)
80 if currentUser == nil {
81 s.pages.HxRedirect(w, "/login")
82 return
83 }
84
85 currentDid := currentUser.Active.Did
86
87 var remainingAccounts []string
88 for _, acc := range currentUser.Accounts {
89 if acc.Did != currentDid {
90 remainingAccounts = append(remainingAccounts, acc.Did)
91 }
92 }
93
94 if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil {
95 l.Error("failed to remove account from registry", "err", err)
96 }
97
98 if err := s.oauth.DeleteSession(w, r); err != nil {
99 l.Error("failed to delete session", "err", err)
100 }
101
102 if len(remainingAccounts) > 0 {
103 nextDid := remainingAccounts[0]
104 if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil {
105 l.Error("failed to switch to next account", "err", err)
106 s.pages.HxRedirect(w, "/login")
107 return
108 }
109 l.Info("switched to next account after logout", "did", nextDid)
110 s.pages.HxRefresh(w)
111 return
112 }
113
114 l.Info("logged out last account")
115 s.pages.HxRedirect(w, "/login")
116}