forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

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.

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