this repo has no description

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.

Akshay 3d9d9bfc 12ca48fc

Changed files
+259 -206
appview
+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 ) ··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 + }