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