+94
appview/middleware/middleware.go
+94
appview/middleware/middleware.go
···
···
1
+
package middleware
2
+
3
+
import (
4
+
"log"
5
+
"net/http"
6
+
"time"
7
+
8
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/xrpc"
10
+
"tangled.sh/tangled.sh/core/appview"
11
+
"tangled.sh/tangled.sh/core/appview/auth"
12
+
)
13
+
14
+
type Middleware func(http.Handler) http.Handler
15
+
16
+
func AuthMiddleware(a *auth.Auth) Middleware {
17
+
return func(next http.Handler) http.Handler {
18
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19
+
redirectFunc := func(w http.ResponseWriter, r *http.Request) {
20
+
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
21
+
}
22
+
if r.Header.Get("HX-Request") == "true" {
23
+
redirectFunc = func(w http.ResponseWriter, _ *http.Request) {
24
+
w.Header().Set("HX-Redirect", "/login")
25
+
w.WriteHeader(http.StatusOK)
26
+
}
27
+
}
28
+
29
+
session, err := a.GetSession(r)
30
+
if session.IsNew || err != nil {
31
+
log.Printf("not logged in, redirecting")
32
+
redirectFunc(w, r)
33
+
return
34
+
}
35
+
36
+
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
37
+
if !ok || !authorized {
38
+
log.Printf("not logged in, redirecting")
39
+
redirectFunc(w, r)
40
+
return
41
+
}
42
+
43
+
// refresh if nearing expiry
44
+
// TODO: dedup with /login
45
+
expiryStr := session.Values[appview.SessionExpiry].(string)
46
+
expiry, err := time.Parse(time.RFC3339, expiryStr)
47
+
if err != nil {
48
+
log.Println("invalid expiry time", err)
49
+
redirectFunc(w, r)
50
+
return
51
+
}
52
+
pdsUrl, ok1 := session.Values[appview.SessionPds].(string)
53
+
did, ok2 := session.Values[appview.SessionDid].(string)
54
+
refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string)
55
+
56
+
if !ok1 || !ok2 || !ok3 {
57
+
log.Println("invalid expiry time", err)
58
+
redirectFunc(w, r)
59
+
return
60
+
}
61
+
62
+
if time.Now().After(expiry) {
63
+
log.Println("token expired, refreshing ...")
64
+
65
+
client := xrpc.Client{
66
+
Host: pdsUrl,
67
+
Auth: &xrpc.AuthInfo{
68
+
Did: did,
69
+
AccessJwt: refreshJwt,
70
+
RefreshJwt: refreshJwt,
71
+
},
72
+
}
73
+
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
74
+
if err != nil {
75
+
log.Println("failed to refresh session", err)
76
+
redirectFunc(w, r)
77
+
return
78
+
}
79
+
80
+
sessionish := auth.RefreshSessionWrapper{atSession}
81
+
82
+
err = a.StoreSession(r, w, &sessionish, pdsUrl)
83
+
if err != nil {
84
+
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
85
+
return
86
+
}
87
+
88
+
log.Println("successfully refreshed token")
89
+
}
90
+
91
+
next.ServeHTTP(w, r)
92
+
})
93
+
}
94
+
}
+2
-1
appview/state/follow.go
+2
-1
appview/state/follow.go
···
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
lexutil "github.com/bluesky-social/indigo/lex/util"
10
tangled "tangled.sh/tangled.sh/core/api/tangled"
11
"tangled.sh/tangled.sh/core/appview/db"
12
"tangled.sh/tangled.sh/core/appview/pages"
13
)
···
36
switch r.Method {
37
case http.MethodPost:
38
createdAt := time.Now().Format(time.RFC3339)
39
-
rkey := s.TID()
40
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
41
Collection: tangled.GraphFollowNSID,
42
Repo: currentUser.Did,
···
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
lexutil "github.com/bluesky-social/indigo/lex/util"
10
tangled "tangled.sh/tangled.sh/core/api/tangled"
11
+
"tangled.sh/tangled.sh/core/appview"
12
"tangled.sh/tangled.sh/core/appview/db"
13
"tangled.sh/tangled.sh/core/appview/pages"
14
)
···
37
switch r.Method {
38
case http.MethodPost:
39
createdAt := time.Now().Format(time.RFC3339)
40
+
rkey := appview.TID()
41
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
42
Collection: tangled.GraphFollowNSID,
43
Repo: currentUser.Did,
+7
-92
appview/state/middleware.go
+7
-92
appview/state/middleware.go
···
10
11
"slices"
12
13
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
14
"github.com/bluesky-social/indigo/atproto/identity"
15
-
"github.com/bluesky-social/indigo/xrpc"
16
"github.com/go-chi/chi/v5"
17
-
"tangled.sh/tangled.sh/core/appview"
18
-
"tangled.sh/tangled.sh/core/appview/auth"
19
"tangled.sh/tangled.sh/core/appview/db"
20
)
21
22
-
type Middleware func(http.Handler) http.Handler
23
-
24
-
func AuthMiddleware(s *State) Middleware {
25
-
return func(next http.Handler) http.Handler {
26
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27
-
redirectFunc := func(w http.ResponseWriter, r *http.Request) {
28
-
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
29
-
}
30
-
if r.Header.Get("HX-Request") == "true" {
31
-
redirectFunc = func(w http.ResponseWriter, _ *http.Request) {
32
-
w.Header().Set("HX-Redirect", "/login")
33
-
w.WriteHeader(http.StatusOK)
34
-
}
35
-
}
36
-
37
-
session, err := s.auth.GetSession(r)
38
-
if session.IsNew || err != nil {
39
-
log.Printf("not logged in, redirecting")
40
-
redirectFunc(w, r)
41
-
return
42
-
}
43
-
44
-
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
45
-
if !ok || !authorized {
46
-
log.Printf("not logged in, redirecting")
47
-
redirectFunc(w, r)
48
-
return
49
-
}
50
-
51
-
// refresh if nearing expiry
52
-
// TODO: dedup with /login
53
-
expiryStr := session.Values[appview.SessionExpiry].(string)
54
-
expiry, err := time.Parse(time.RFC3339, expiryStr)
55
-
if err != nil {
56
-
log.Println("invalid expiry time", err)
57
-
redirectFunc(w, r)
58
-
return
59
-
}
60
-
pdsUrl, ok1 := session.Values[appview.SessionPds].(string)
61
-
did, ok2 := session.Values[appview.SessionDid].(string)
62
-
refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string)
63
-
64
-
if !ok1 || !ok2 || !ok3 {
65
-
log.Println("invalid expiry time", err)
66
-
redirectFunc(w, r)
67
-
return
68
-
}
69
-
70
-
if time.Now().After(expiry) {
71
-
log.Println("token expired, refreshing ...")
72
-
73
-
client := xrpc.Client{
74
-
Host: pdsUrl,
75
-
Auth: &xrpc.AuthInfo{
76
-
Did: did,
77
-
AccessJwt: refreshJwt,
78
-
RefreshJwt: refreshJwt,
79
-
},
80
-
}
81
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
82
-
if err != nil {
83
-
log.Println("failed to refresh session", err)
84
-
redirectFunc(w, r)
85
-
return
86
-
}
87
-
88
-
sessionish := auth.RefreshSessionWrapper{atSession}
89
-
90
-
err = s.auth.StoreSession(r, w, &sessionish, pdsUrl)
91
-
if err != nil {
92
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
93
-
return
94
-
}
95
-
96
-
log.Println("successfully refreshed token")
97
-
}
98
-
99
-
next.ServeHTTP(w, r)
100
-
})
101
-
}
102
-
}
103
-
104
-
func knotRoleMiddleware(s *State, group string) Middleware {
105
return func(next http.Handler) http.Handler {
106
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107
// requires auth also
···
131
}
132
}
133
134
-
func KnotOwner(s *State) Middleware {
135
return knotRoleMiddleware(s, "server:owner")
136
}
137
138
-
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
139
return func(next http.Handler) http.Handler {
140
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141
// requires auth also
···
175
})
176
}
177
178
-
func ResolveIdent(s *State) Middleware {
179
excluded := []string{"favicon.ico"}
180
181
return func(next http.Handler) http.Handler {
···
201
}
202
}
203
204
-
func ResolveRepo(s *State) Middleware {
205
return func(next http.Handler) http.Handler {
206
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
207
repoName := chi.URLParam(req, "repo")
···
230
}
231
232
// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
233
-
func ResolvePull(s *State) Middleware {
234
return func(next http.Handler) http.Handler {
235
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
236
f, err := fullyResolvedRepo(r)
···
10
11
"slices"
12
13
"github.com/bluesky-social/indigo/atproto/identity"
14
"github.com/go-chi/chi/v5"
15
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/appview/middleware"
17
)
18
19
+
func knotRoleMiddleware(s *State, group string) middleware.Middleware {
20
return func(next http.Handler) http.Handler {
21
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
// requires auth also
···
46
}
47
}
48
49
+
func KnotOwner(s *State) middleware.Middleware {
50
return knotRoleMiddleware(s, "server:owner")
51
}
52
53
+
func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
54
return func(next http.Handler) http.Handler {
55
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56
// requires auth also
···
90
})
91
}
92
93
+
func ResolveIdent(s *State) middleware.Middleware {
94
excluded := []string{"favicon.ico"}
95
96
return func(next http.Handler) http.Handler {
···
116
}
117
}
118
119
+
func ResolveRepo(s *State) middleware.Middleware {
120
return func(next http.Handler) http.Handler {
121
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
122
repoName := chi.URLParam(req, "repo")
···
145
}
146
147
// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
148
+
func ResolvePull(s *State) middleware.Middleware {
149
return func(next http.Handler) http.Handler {
150
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151
f, err := fullyResolvedRepo(r)
+3
-2
appview/state/pull.go
+3
-2
appview/state/pull.go
···
13
"time"
14
15
"tangled.sh/tangled.sh/core/api/tangled"
16
"tangled.sh/tangled.sh/core/appview/auth"
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/pages"
···
521
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
522
Collection: tangled.RepoPullCommentNSID,
523
Repo: user.Did,
524
-
Rkey: s.TID(),
525
Record: &lexutil.LexiconTypeDecoder{
526
Val: &tangled.RepoPullComment{
527
Repo: &atUri,
···
846
body = formatPatches[0].Body
847
}
848
849
-
rkey := s.TID()
850
initialSubmission := db.PullSubmission{
851
Patch: patch,
852
SourceRev: sourceRev,
···
13
"time"
14
15
"tangled.sh/tangled.sh/core/api/tangled"
16
+
"tangled.sh/tangled.sh/core/appview"
17
"tangled.sh/tangled.sh/core/appview/auth"
18
"tangled.sh/tangled.sh/core/appview/db"
19
"tangled.sh/tangled.sh/core/appview/pages"
···
522
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
523
Collection: tangled.RepoPullCommentNSID,
524
Repo: user.Did,
525
+
Rkey: appview.TID(),
526
Record: &lexutil.LexiconTypeDecoder{
527
Val: &tangled.RepoPullComment{
528
Repo: &atUri,
···
847
body = formatPatches[0].Body
848
}
849
850
+
rkey := appview.TID()
851
initialSubmission := db.PullSubmission{
852
Patch: patch,
853
SourceRev: sourceRev,
+5
-4
appview/state/repo.go
+5
-4
appview/state/repo.go
···
23
"github.com/go-chi/chi/v5"
24
"github.com/go-git/go-git/v5/plumbing"
25
"tangled.sh/tangled.sh/core/api/tangled"
26
"tangled.sh/tangled.sh/core/appview/auth"
27
"tangled.sh/tangled.sh/core/appview/db"
28
"tangled.sh/tangled.sh/core/appview/pages"
···
1116
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1117
Collection: tangled.RepoIssueStateNSID,
1118
Repo: user.Did,
1119
-
Rkey: s.TID(),
1120
Record: &lexutil.LexiconTypeDecoder{
1121
Val: &tangled.RepoIssueState{
1122
Issue: issue.IssueAt,
···
1220
}
1221
1222
commentId := mathrand.IntN(1000000)
1223
-
rkey := s.TID()
1224
1225
err := db.NewIssueComment(s.db, &db.Comment{
1226
OwnerDid: user.Did,
···
1650
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1651
Collection: tangled.RepoIssueNSID,
1652
Repo: user.Did,
1653
-
Rkey: s.TID(),
1654
Record: &lexutil.LexiconTypeDecoder{
1655
Val: &tangled.RepoIssue{
1656
Repo: atUri,
···
1754
sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
1755
sourceAt := f.RepoAt.String()
1756
1757
-
rkey := s.TID()
1758
repo := &db.Repo{
1759
Did: user.Did,
1760
Name: forkName,
···
23
"github.com/go-chi/chi/v5"
24
"github.com/go-git/go-git/v5/plumbing"
25
"tangled.sh/tangled.sh/core/api/tangled"
26
+
"tangled.sh/tangled.sh/core/appview"
27
"tangled.sh/tangled.sh/core/appview/auth"
28
"tangled.sh/tangled.sh/core/appview/db"
29
"tangled.sh/tangled.sh/core/appview/pages"
···
1117
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1118
Collection: tangled.RepoIssueStateNSID,
1119
Repo: user.Did,
1120
+
Rkey: appview.TID(),
1121
Record: &lexutil.LexiconTypeDecoder{
1122
Val: &tangled.RepoIssueState{
1123
Issue: issue.IssueAt,
···
1221
}
1222
1223
commentId := mathrand.IntN(1000000)
1224
+
rkey := appview.TID()
1225
1226
err := db.NewIssueComment(s.db, &db.Comment{
1227
OwnerDid: user.Did,
···
1651
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1652
Collection: tangled.RepoIssueNSID,
1653
Repo: user.Did,
1654
+
Rkey: appview.TID(),
1655
Record: &lexutil.LexiconTypeDecoder{
1656
Val: &tangled.RepoIssue{
1657
Repo: atUri,
···
1755
sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
1756
sourceAt := f.RepoAt.String()
1757
1758
+
rkey := appview.TID()
1759
repo := &db.Repo{
1760
Did: user.Did,
1761
Name: forkName,
+25
-22
appview/state/router.go
+25
-22
appview/state/router.go
···
5
"strings"
6
7
"github.com/go-chi/chi/v5"
8
"tangled.sh/tangled.sh/core/appview/state/userutil"
9
)
10
···
70
r.Get("/{issue}", s.RepoSingleIssue)
71
72
r.Group(func(r chi.Router) {
73
-
r.Use(AuthMiddleware(s))
74
r.Get("/new", s.NewIssue)
75
r.Post("/new", s.NewIssue)
76
r.Post("/{issue}/comment", s.NewIssueComment)
···
86
})
87
88
r.Route("/fork", func(r chi.Router) {
89
-
r.Use(AuthMiddleware(s))
90
r.Get("/", s.ForkRepo)
91
r.Post("/", s.ForkRepo)
92
})
93
94
r.Route("/pulls", func(r chi.Router) {
95
r.Get("/", s.RepoPulls)
96
-
r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) {
97
r.Get("/", s.NewPull)
98
r.Get("/patch-upload", s.PatchUploadFragment)
99
r.Post("/validate-patch", s.ValidatePatch)
···
111
r.Get("/", s.RepoPullPatch)
112
r.Get("/interdiff", s.RepoPullInterdiff)
113
r.Get("/actions", s.PullActions)
114
-
r.With(AuthMiddleware(s)).Route("/comment", func(r chi.Router) {
115
r.Get("/", s.PullComment)
116
r.Post("/", s.PullComment)
117
})
···
122
})
123
124
r.Group(func(r chi.Router) {
125
-
r.Use(AuthMiddleware(s))
126
r.Route("/resubmit", func(r chi.Router) {
127
r.Get("/", s.ResubmitPull)
128
r.Post("/", s.ResubmitPull)
···
145
146
// settings routes, needs auth
147
r.Group(func(r chi.Router) {
148
-
r.Use(AuthMiddleware(s))
149
// repo description can only be edited by owner
150
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
151
r.Put("/", s.RepoDescription)
···
176
177
r.Get("/", s.Timeline)
178
179
-
r.With(AuthMiddleware(s)).Post("/logout", s.Logout)
180
181
r.Route("/login", func(r chi.Router) {
182
r.Get("/", s.Login)
···
184
})
185
186
r.Route("/knots", func(r chi.Router) {
187
-
r.Use(AuthMiddleware(s))
188
r.Get("/", s.Knots)
189
r.Post("/key", s.RegistrationKey)
190
···
202
203
r.Route("/repo", func(r chi.Router) {
204
r.Route("/new", func(r chi.Router) {
205
-
r.Use(AuthMiddleware(s))
206
r.Get("/", s.NewRepo)
207
r.Post("/", s.NewRepo)
208
})
209
// r.Post("/import", s.ImportRepo)
210
})
211
212
-
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
213
r.Post("/", s.Follow)
214
r.Delete("/", s.Follow)
215
})
216
217
-
r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
218
r.Post("/", s.Star)
219
r.Delete("/", s.Star)
220
})
221
222
-
r.Route("/settings", func(r chi.Router) {
223
-
r.Use(AuthMiddleware(s))
224
-
r.Get("/", s.Settings)
225
-
r.Put("/keys", s.SettingsKeys)
226
-
r.Delete("/keys", s.SettingsKeys)
227
-
r.Put("/emails", s.SettingsEmails)
228
-
r.Delete("/emails", s.SettingsEmails)
229
-
r.Get("/emails/verify", s.SettingsEmailsVerify)
230
-
r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend)
231
-
r.Post("/emails/primary", s.SettingsEmailsPrimary)
232
-
})
233
234
r.Get("/keys/{user}", s.Keys)
235
···
238
})
239
return r
240
}
···
5
"strings"
6
7
"github.com/go-chi/chi/v5"
8
+
"tangled.sh/tangled.sh/core/appview/middleware"
9
+
"tangled.sh/tangled.sh/core/appview/state/settings"
10
"tangled.sh/tangled.sh/core/appview/state/userutil"
11
)
12
···
72
r.Get("/{issue}", s.RepoSingleIssue)
73
74
r.Group(func(r chi.Router) {
75
+
r.Use(middleware.AuthMiddleware(s.auth))
76
r.Get("/new", s.NewIssue)
77
r.Post("/new", s.NewIssue)
78
r.Post("/{issue}/comment", s.NewIssueComment)
···
88
})
89
90
r.Route("/fork", func(r chi.Router) {
91
+
r.Use(middleware.AuthMiddleware(s.auth))
92
r.Get("/", s.ForkRepo)
93
r.Post("/", s.ForkRepo)
94
})
95
96
r.Route("/pulls", func(r chi.Router) {
97
r.Get("/", s.RepoPulls)
98
+
r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) {
99
r.Get("/", s.NewPull)
100
r.Get("/patch-upload", s.PatchUploadFragment)
101
r.Post("/validate-patch", s.ValidatePatch)
···
113
r.Get("/", s.RepoPullPatch)
114
r.Get("/interdiff", s.RepoPullInterdiff)
115
r.Get("/actions", s.PullActions)
116
+
r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) {
117
r.Get("/", s.PullComment)
118
r.Post("/", s.PullComment)
119
})
···
124
})
125
126
r.Group(func(r chi.Router) {
127
+
r.Use(middleware.AuthMiddleware(s.auth))
128
r.Route("/resubmit", func(r chi.Router) {
129
r.Get("/", s.ResubmitPull)
130
r.Post("/", s.ResubmitPull)
···
147
148
// settings routes, needs auth
149
r.Group(func(r chi.Router) {
150
+
r.Use(middleware.AuthMiddleware(s.auth))
151
// repo description can only be edited by owner
152
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
153
r.Put("/", s.RepoDescription)
···
178
179
r.Get("/", s.Timeline)
180
181
+
r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout)
182
183
r.Route("/login", func(r chi.Router) {
184
r.Get("/", s.Login)
···
186
})
187
188
r.Route("/knots", func(r chi.Router) {
189
+
r.Use(middleware.AuthMiddleware(s.auth))
190
r.Get("/", s.Knots)
191
r.Post("/key", s.RegistrationKey)
192
···
204
205
r.Route("/repo", func(r chi.Router) {
206
r.Route("/new", func(r chi.Router) {
207
+
r.Use(middleware.AuthMiddleware(s.auth))
208
r.Get("/", s.NewRepo)
209
r.Post("/", s.NewRepo)
210
})
211
// r.Post("/import", s.ImportRepo)
212
})
213
214
+
r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) {
215
r.Post("/", s.Follow)
216
r.Delete("/", s.Follow)
217
})
218
219
+
r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) {
220
r.Post("/", s.Star)
221
r.Delete("/", s.Star)
222
})
223
224
+
r.Route("/settings", s.SettingsRouter)
225
226
r.Get("/keys/{user}", s.Keys)
227
···
230
})
231
return r
232
}
233
+
234
+
func (s *State) SettingsRouter(r chi.Router) {
235
+
settings := &settings.Settings{
236
+
Db: s.db,
237
+
Auth: s.auth,
238
+
Pages: s.pages,
239
+
Config: s.config,
240
+
}
241
+
242
+
settings.Router(r)
243
+
}
+106
-80
appview/state/settings.go
appview/state/settings/settings.go
+106
-80
appview/state/settings.go
appview/state/settings/settings.go
···
1
-
package state
2
3
import (
4
"database/sql"
···
10
"strings"
11
"time"
12
13
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
14
-
lexutil "github.com/bluesky-social/indigo/lex/util"
15
-
"github.com/gliderlabs/ssh"
16
-
"github.com/google/uuid"
17
"tangled.sh/tangled.sh/core/api/tangled"
18
"tangled.sh/tangled.sh/core/appview/db"
19
"tangled.sh/tangled.sh/core/appview/email"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
)
22
23
-
func (s *State) Settings(w http.ResponseWriter, r *http.Request) {
24
-
user := s.auth.GetUser(r)
25
-
pubKeys, err := db.GetPublicKeys(s.db, user.Did)
26
if err != nil {
27
log.Println(err)
28
}
29
30
-
emails, err := db.GetAllEmails(s.db, user.Did)
31
if err != nil {
32
log.Println(err)
33
}
34
35
-
s.pages.Settings(w, pages.SettingsParams{
36
LoggedInUser: user,
37
PubKeys: pubKeys,
38
Emails: emails,
···
40
}
41
42
// buildVerificationEmail creates an email.Email struct for verification emails
43
-
func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email {
44
verifyURL := s.verifyUrl(did, emailAddr, code)
45
46
return email.Email{
47
-
APIKey: s.config.ResendApiKey,
48
From: "noreply@notifs.tangled.sh",
49
To: emailAddr,
50
Subject: "Verify your Tangled email",
···
56
}
57
58
// sendVerificationEmail handles the common logic for sending verification emails
59
-
func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error {
60
emailToSend := s.buildVerificationEmail(emailAddr, did, code)
61
62
err := email.SendEmail(emailToSend)
63
if err != nil {
64
log.Printf("sending email: %s", err)
65
-
s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext))
66
return err
67
}
68
69
return nil
70
}
71
72
-
func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) {
73
switch r.Method {
74
case http.MethodGet:
75
-
s.pages.Notice(w, "settings-emails", "Unimplemented.")
76
log.Println("unimplemented")
77
return
78
case http.MethodPut:
79
-
did := s.auth.GetDid(r)
80
emAddr := r.FormValue("email")
81
emAddr = strings.TrimSpace(emAddr)
82
83
if !email.IsValidEmail(emAddr) {
84
-
s.pages.Notice(w, "settings-emails-error", "Invalid email address.")
85
return
86
}
87
88
// check if email already exists in database
89
-
existingEmail, err := db.GetEmail(s.db, did, emAddr)
90
if err != nil && !errors.Is(err, sql.ErrNoRows) {
91
log.Printf("checking for existing email: %s", err)
92
-
s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
93
return
94
}
95
96
if err == nil {
97
if existingEmail.Verified {
98
-
s.pages.Notice(w, "settings-emails-error", "This email is already verified.")
99
return
100
}
101
102
-
s.pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.")
103
return
104
}
105
106
code := uuid.New().String()
107
108
// Begin transaction
109
-
tx, err := s.db.Begin()
110
if err != nil {
111
log.Printf("failed to start transaction: %s", err)
112
-
s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
113
return
114
}
115
defer tx.Rollback()
···
121
VerificationCode: code,
122
}); err != nil {
123
log.Printf("adding email: %s", err)
124
-
s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
125
return
126
}
127
···
132
// Commit transaction
133
if err := tx.Commit(); err != nil {
134
log.Printf("failed to commit transaction: %s", err)
135
-
s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
136
return
137
}
138
139
-
s.pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
140
return
141
case http.MethodDelete:
142
-
did := s.auth.GetDid(r)
143
emailAddr := r.FormValue("email")
144
emailAddr = strings.TrimSpace(emailAddr)
145
146
// Begin transaction
147
-
tx, err := s.db.Begin()
148
if err != nil {
149
log.Printf("failed to start transaction: %s", err)
150
-
s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
151
return
152
}
153
defer tx.Rollback()
154
155
if err := db.DeleteEmail(tx, did, emailAddr); err != nil {
156
log.Printf("deleting email: %s", err)
157
-
s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
158
return
159
}
160
161
// Commit transaction
162
if err := tx.Commit(); err != nil {
163
log.Printf("failed to commit transaction: %s", err)
164
-
s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
165
return
166
}
167
168
-
s.pages.HxLocation(w, "/settings")
169
return
170
}
171
}
172
173
-
func (s *State) verifyUrl(did string, email string, code string) string {
174
var appUrl string
175
-
if s.config.Dev {
176
-
appUrl = "http://" + s.config.ListenAddr
177
} else {
178
appUrl = "https://tangled.sh"
179
}
···
181
return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code))
182
}
183
184
-
func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) {
185
q := r.URL.Query()
186
187
// Get the parameters directly from the query
···
189
did := q.Get("did")
190
code := q.Get("code")
191
192
-
valid, err := db.CheckValidVerificationCode(s.db, did, emailAddr, code)
193
if err != nil {
194
log.Printf("checking email verification: %s", err)
195
-
s.pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.")
196
return
197
}
198
199
if !valid {
200
-
s.pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.")
201
return
202
}
203
204
// Mark email as verified in the database
205
-
if err := db.MarkEmailVerified(s.db, did, emailAddr); err != nil {
206
log.Printf("marking email as verified: %s", err)
207
-
s.pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.")
208
return
209
}
210
211
http.Redirect(w, r, "/settings", http.StatusSeeOther)
212
}
213
214
-
func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) {
215
if r.Method != http.MethodPost {
216
-
s.pages.Notice(w, "settings-emails-error", "Invalid request method.")
217
return
218
}
219
220
-
did := s.auth.GetDid(r)
221
emAddr := r.FormValue("email")
222
emAddr = strings.TrimSpace(emAddr)
223
224
if !email.IsValidEmail(emAddr) {
225
-
s.pages.Notice(w, "settings-emails-error", "Invalid email address.")
226
return
227
}
228
229
// Check if email exists and is unverified
230
-
existingEmail, err := db.GetEmail(s.db, did, emAddr)
231
if err != nil {
232
if errors.Is(err, sql.ErrNoRows) {
233
-
s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.")
234
} else {
235
log.Printf("checking for existing email: %s", err)
236
-
s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
237
}
238
return
239
}
240
241
if existingEmail.Verified {
242
-
s.pages.Notice(w, "settings-emails-error", "This email is already verified.")
243
return
244
}
245
···
248
timeSinceLastSent := time.Since(*existingEmail.LastSent)
249
if timeSinceLastSent < 10*time.Minute {
250
waitTime := 10*time.Minute - timeSinceLastSent
251
-
s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1)))
252
return
253
}
254
}
···
257
code := uuid.New().String()
258
259
// Begin transaction
260
-
tx, err := s.db.Begin()
261
if err != nil {
262
log.Printf("failed to start transaction: %s", err)
263
-
s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
264
return
265
}
266
defer tx.Rollback()
···
268
// Update the verification code and last sent time
269
if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil {
270
log.Printf("updating email verification: %s", err)
271
-
s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
272
return
273
}
274
···
280
// Commit transaction
281
if err := tx.Commit(); err != nil {
282
log.Printf("failed to commit transaction: %s", err)
283
-
s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
284
return
285
}
286
287
-
s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.")
288
}
289
290
-
func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) {
291
-
did := s.auth.GetDid(r)
292
emailAddr := r.FormValue("email")
293
emailAddr = strings.TrimSpace(emailAddr)
294
295
if emailAddr == "" {
296
-
s.pages.Notice(w, "settings-emails-error", "Email address cannot be empty.")
297
return
298
}
299
300
-
if err := db.MakeEmailPrimary(s.db, did, emailAddr); err != nil {
301
log.Printf("setting primary email: %s", err)
302
-
s.pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.")
303
return
304
}
305
306
-
s.pages.HxLocation(w, "/settings")
307
}
308
309
-
func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) {
310
switch r.Method {
311
case http.MethodGet:
312
-
s.pages.Notice(w, "settings-keys", "Unimplemented.")
313
log.Println("unimplemented")
314
return
315
case http.MethodPut:
316
-
did := s.auth.GetDid(r)
317
key := r.FormValue("key")
318
key = strings.TrimSpace(key)
319
name := r.FormValue("name")
320
-
client, _ := s.auth.AuthorizedClient(r)
321
322
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
323
if err != nil {
324
log.Printf("parsing public key: %s", err)
325
-
s.pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
326
return
327
}
328
329
-
rkey := s.TID()
330
331
-
tx, err := s.db.Begin()
332
if err != nil {
333
log.Printf("failed to start tx; adding public key: %s", err)
334
-
s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
335
return
336
}
337
defer tx.Rollback()
338
339
if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil {
340
log.Printf("adding public key: %s", err)
341
-
s.pages.Notice(w, "settings-keys", "Failed to add public key.")
342
return
343
}
344
···
357
// invalid record
358
if err != nil {
359
log.Printf("failed to create record: %s", err)
360
-
s.pages.Notice(w, "settings-keys", "Failed to create record.")
361
return
362
}
363
···
366
err = tx.Commit()
367
if err != nil {
368
log.Printf("failed to commit tx; adding public key: %s", err)
369
-
s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
370
return
371
}
372
373
-
s.pages.HxLocation(w, "/settings")
374
return
375
376
case http.MethodDelete:
377
-
did := s.auth.GetDid(r)
378
q := r.URL.Query()
379
380
name := q.Get("name")
···
385
log.Println(rkey)
386
log.Println(key)
387
388
-
client, _ := s.auth.AuthorizedClient(r)
389
390
-
if err := db.RemovePublicKey(s.db, did, name, key); err != nil {
391
log.Printf("removing public key: %s", err)
392
-
s.pages.Notice(w, "settings-keys", "Failed to remove public key.")
393
return
394
}
395
···
404
// invalid record
405
if err != nil {
406
log.Printf("failed to delete record from PDS: %s", err)
407
-
s.pages.Notice(w, "settings-keys", "Failed to remove key from PDS.")
408
return
409
}
410
}
411
log.Println("deleted successfully")
412
413
-
s.pages.HxLocation(w, "/settings")
414
return
415
}
416
}
···
1
+
package settings
2
3
import (
4
"database/sql"
···
10
"strings"
11
"time"
12
13
+
"github.com/go-chi/chi/v5"
14
"tangled.sh/tangled.sh/core/api/tangled"
15
+
"tangled.sh/tangled.sh/core/appview"
16
+
"tangled.sh/tangled.sh/core/appview/auth"
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/email"
19
+
"tangled.sh/tangled.sh/core/appview/middleware"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
+
22
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
23
+
lexutil "github.com/bluesky-social/indigo/lex/util"
24
+
"github.com/gliderlabs/ssh"
25
+
"github.com/google/uuid"
26
)
27
28
+
type Settings struct {
29
+
Db *db.DB
30
+
Auth *auth.Auth
31
+
Pages *pages.Pages
32
+
Config *appview.Config
33
+
}
34
+
35
+
func (s *Settings) Router(r chi.Router) {
36
+
r.Use(middleware.AuthMiddleware(s.Auth))
37
+
38
+
r.Get("/", s.settings)
39
+
r.Put("/keys", s.keys)
40
+
r.Delete("/keys", s.keys)
41
+
r.Put("/emails", s.emails)
42
+
r.Delete("/emails", s.emails)
43
+
r.Get("/emails/verify", s.emailsVerify)
44
+
r.Post("/emails/verify/resend", s.emailsVerifyResend)
45
+
r.Post("/emails/primary", s.emailsPrimary)
46
+
47
+
}
48
+
49
+
func (s *Settings) settings(w http.ResponseWriter, r *http.Request) {
50
+
user := s.Auth.GetUser(r)
51
+
pubKeys, err := db.GetPublicKeys(s.Db, user.Did)
52
if err != nil {
53
log.Println(err)
54
}
55
56
+
emails, err := db.GetAllEmails(s.Db, user.Did)
57
if err != nil {
58
log.Println(err)
59
}
60
61
+
s.Pages.Settings(w, pages.SettingsParams{
62
LoggedInUser: user,
63
PubKeys: pubKeys,
64
Emails: emails,
···
66
}
67
68
// buildVerificationEmail creates an email.Email struct for verification emails
69
+
func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Email {
70
verifyURL := s.verifyUrl(did, emailAddr, code)
71
72
return email.Email{
73
+
APIKey: s.Config.ResendApiKey,
74
From: "noreply@notifs.tangled.sh",
75
To: emailAddr,
76
Subject: "Verify your Tangled email",
···
82
}
83
84
// sendVerificationEmail handles the common logic for sending verification emails
85
+
func (s *Settings) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error {
86
emailToSend := s.buildVerificationEmail(emailAddr, did, code)
87
88
err := email.SendEmail(emailToSend)
89
if err != nil {
90
log.Printf("sending email: %s", err)
91
+
s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext))
92
return err
93
}
94
95
return nil
96
}
97
98
+
func (s *Settings) emails(w http.ResponseWriter, r *http.Request) {
99
switch r.Method {
100
case http.MethodGet:
101
+
s.Pages.Notice(w, "settings-emails", "Unimplemented.")
102
log.Println("unimplemented")
103
return
104
case http.MethodPut:
105
+
did := s.Auth.GetDid(r)
106
emAddr := r.FormValue("email")
107
emAddr = strings.TrimSpace(emAddr)
108
109
if !email.IsValidEmail(emAddr) {
110
+
s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
111
return
112
}
113
114
// check if email already exists in database
115
+
existingEmail, err := db.GetEmail(s.Db, did, emAddr)
116
if err != nil && !errors.Is(err, sql.ErrNoRows) {
117
log.Printf("checking for existing email: %s", err)
118
+
s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
119
return
120
}
121
122
if err == nil {
123
if existingEmail.Verified {
124
+
s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
125
return
126
}
127
128
+
s.Pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.")
129
return
130
}
131
132
code := uuid.New().String()
133
134
// Begin transaction
135
+
tx, err := s.Db.Begin()
136
if err != nil {
137
log.Printf("failed to start transaction: %s", err)
138
+
s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
139
return
140
}
141
defer tx.Rollback()
···
147
VerificationCode: code,
148
}); err != nil {
149
log.Printf("adding email: %s", err)
150
+
s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
151
return
152
}
153
···
158
// Commit transaction
159
if err := tx.Commit(); err != nil {
160
log.Printf("failed to commit transaction: %s", err)
161
+
s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
162
return
163
}
164
165
+
s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
166
return
167
case http.MethodDelete:
168
+
did := s.Auth.GetDid(r)
169
emailAddr := r.FormValue("email")
170
emailAddr = strings.TrimSpace(emailAddr)
171
172
// Begin transaction
173
+
tx, err := s.Db.Begin()
174
if err != nil {
175
log.Printf("failed to start transaction: %s", err)
176
+
s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
177
return
178
}
179
defer tx.Rollback()
180
181
if err := db.DeleteEmail(tx, did, emailAddr); err != nil {
182
log.Printf("deleting email: %s", err)
183
+
s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
184
return
185
}
186
187
// Commit transaction
188
if err := tx.Commit(); err != nil {
189
log.Printf("failed to commit transaction: %s", err)
190
+
s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
191
return
192
}
193
194
+
s.Pages.HxLocation(w, "/settings")
195
return
196
}
197
}
198
199
+
func (s *Settings) verifyUrl(did string, email string, code string) string {
200
var appUrl string
201
+
if s.Config.Dev {
202
+
appUrl = "http://" + s.Config.ListenAddr
203
} else {
204
appUrl = "https://tangled.sh"
205
}
···
207
return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code))
208
}
209
210
+
func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
211
q := r.URL.Query()
212
213
// Get the parameters directly from the query
···
215
did := q.Get("did")
216
code := q.Get("code")
217
218
+
valid, err := db.CheckValidVerificationCode(s.Db, did, emailAddr, code)
219
if err != nil {
220
log.Printf("checking email verification: %s", err)
221
+
s.Pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.")
222
return
223
}
224
225
if !valid {
226
+
s.Pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.")
227
return
228
}
229
230
// Mark email as verified in the database
231
+
if err := db.MarkEmailVerified(s.Db, did, emailAddr); err != nil {
232
log.Printf("marking email as verified: %s", err)
233
+
s.Pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.")
234
return
235
}
236
237
http.Redirect(w, r, "/settings", http.StatusSeeOther)
238
}
239
240
+
func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) {
241
if r.Method != http.MethodPost {
242
+
s.Pages.Notice(w, "settings-emails-error", "Invalid request method.")
243
return
244
}
245
246
+
did := s.Auth.GetDid(r)
247
emAddr := r.FormValue("email")
248
emAddr = strings.TrimSpace(emAddr)
249
250
if !email.IsValidEmail(emAddr) {
251
+
s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
252
return
253
}
254
255
// Check if email exists and is unverified
256
+
existingEmail, err := db.GetEmail(s.Db, did, emAddr)
257
if err != nil {
258
if errors.Is(err, sql.ErrNoRows) {
259
+
s.Pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.")
260
} else {
261
log.Printf("checking for existing email: %s", err)
262
+
s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
263
}
264
return
265
}
266
267
if existingEmail.Verified {
268
+
s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
269
return
270
}
271
···
274
timeSinceLastSent := time.Since(*existingEmail.LastSent)
275
if timeSinceLastSent < 10*time.Minute {
276
waitTime := 10*time.Minute - timeSinceLastSent
277
+
s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1)))
278
return
279
}
280
}
···
283
code := uuid.New().String()
284
285
// Begin transaction
286
+
tx, err := s.Db.Begin()
287
if err != nil {
288
log.Printf("failed to start transaction: %s", err)
289
+
s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
290
return
291
}
292
defer tx.Rollback()
···
294
// Update the verification code and last sent time
295
if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil {
296
log.Printf("updating email verification: %s", err)
297
+
s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
298
return
299
}
300
···
306
// Commit transaction
307
if err := tx.Commit(); err != nil {
308
log.Printf("failed to commit transaction: %s", err)
309
+
s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
310
return
311
}
312
313
+
s.Pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.")
314
}
315
316
+
func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
317
+
did := s.Auth.GetDid(r)
318
emailAddr := r.FormValue("email")
319
emailAddr = strings.TrimSpace(emailAddr)
320
321
if emailAddr == "" {
322
+
s.Pages.Notice(w, "settings-emails-error", "Email address cannot be empty.")
323
return
324
}
325
326
+
if err := db.MakeEmailPrimary(s.Db, did, emailAddr); err != nil {
327
log.Printf("setting primary email: %s", err)
328
+
s.Pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.")
329
return
330
}
331
332
+
s.Pages.HxLocation(w, "/settings")
333
}
334
335
+
func (s *Settings) keys(w http.ResponseWriter, r *http.Request) {
336
switch r.Method {
337
case http.MethodGet:
338
+
s.Pages.Notice(w, "settings-keys", "Unimplemented.")
339
log.Println("unimplemented")
340
return
341
case http.MethodPut:
342
+
did := s.Auth.GetDid(r)
343
key := r.FormValue("key")
344
key = strings.TrimSpace(key)
345
name := r.FormValue("name")
346
+
client, _ := s.Auth.AuthorizedClient(r)
347
348
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
349
if err != nil {
350
log.Printf("parsing public key: %s", err)
351
+
s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
352
return
353
}
354
355
+
rkey := appview.TID()
356
357
+
tx, err := s.Db.Begin()
358
if err != nil {
359
log.Printf("failed to start tx; adding public key: %s", err)
360
+
s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
361
return
362
}
363
defer tx.Rollback()
364
365
if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil {
366
log.Printf("adding public key: %s", err)
367
+
s.Pages.Notice(w, "settings-keys", "Failed to add public key.")
368
return
369
}
370
···
383
// invalid record
384
if err != nil {
385
log.Printf("failed to create record: %s", err)
386
+
s.Pages.Notice(w, "settings-keys", "Failed to create record.")
387
return
388
}
389
···
392
err = tx.Commit()
393
if err != nil {
394
log.Printf("failed to commit tx; adding public key: %s", err)
395
+
s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
396
return
397
}
398
399
+
s.Pages.HxLocation(w, "/settings")
400
return
401
402
case http.MethodDelete:
403
+
did := s.Auth.GetDid(r)
404
q := r.URL.Query()
405
406
name := q.Get("name")
···
411
log.Println(rkey)
412
log.Println(key)
413
414
+
client, _ := s.Auth.AuthorizedClient(r)
415
416
+
if err := db.RemovePublicKey(s.Db, did, name, key); err != nil {
417
log.Printf("removing public key: %s", err)
418
+
s.Pages.Notice(w, "settings-keys", "Failed to remove public key.")
419
return
420
}
421
···
430
// invalid record
431
if err != nil {
432
log.Printf("failed to delete record from PDS: %s", err)
433
+
s.Pages.Notice(w, "settings-keys", "Failed to remove key from PDS.")
434
return
435
}
436
}
437
log.Println("deleted successfully")
438
439
+
s.Pages.HxLocation(w, "/settings")
440
return
441
}
442
}
+2
-1
appview/state/star.go
+2
-1
appview/state/star.go
···
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
lexutil "github.com/bluesky-social/indigo/lex/util"
11
tangled "tangled.sh/tangled.sh/core/api/tangled"
12
"tangled.sh/tangled.sh/core/appview/db"
13
"tangled.sh/tangled.sh/core/appview/pages"
14
)
···
33
switch r.Method {
34
case http.MethodPost:
35
createdAt := time.Now().Format(time.RFC3339)
36
-
rkey := s.TID()
37
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
38
Collection: tangled.FeedStarNSID,
39
Repo: currentUser.Did,
···
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
lexutil "github.com/bluesky-social/indigo/lex/util"
11
tangled "tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/appview"
13
"tangled.sh/tangled.sh/core/appview/db"
14
"tangled.sh/tangled.sh/core/appview/pages"
15
)
···
34
switch r.Method {
35
case http.MethodPost:
36
createdAt := time.Now().Format(time.RFC3339)
37
+
rkey := appview.TID()
38
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
39
Collection: tangled.FeedStarNSID,
40
Repo: currentUser.Did,
+4
-4
appview/state/state.go
+4
-4
appview/state/state.go
···
91
return state, nil
92
}
93
94
-
func (s *State) TID() string {
95
-
return s.tidClock.Next().String()
96
}
97
98
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
···
522
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
523
Collection: tangled.KnotMemberNSID,
524
Repo: currentUser.Did,
525
-
Rkey: s.TID(),
526
Record: &lexutil.LexiconTypeDecoder{
527
Val: &tangled.KnotMember{
528
Member: memberIdent.DID.String(),
···
646
return
647
}
648
649
-
rkey := s.TID()
650
repo := &db.Repo{
651
Did: user.Did,
652
Name: repoName,
···
91
return state, nil
92
}
93
94
+
func TID(c *syntax.TIDClock) string {
95
+
return c.Next().String()
96
}
97
98
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
···
522
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
523
Collection: tangled.KnotMemberNSID,
524
Repo: currentUser.Did,
525
+
Rkey: appview.TID(),
526
Record: &lexutil.LexiconTypeDecoder{
527
Val: &tangled.KnotMember{
528
Member: memberIdent.DID.String(),
···
646
return
647
}
648
649
+
rkey := appview.TID()
650
repo := &db.Repo{
651
Did: user.Did,
652
Name: repoName,