Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

appview: refactor settings router

move settings router into a subpackage

also introduces a middleware package under appview, and turns TID() into
a global function that operates on a globally mutable TID clock.

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