From 3d9d9bfcab83eff01ba37cc375a55d37acf67197 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 22 Apr 2025 15:14:57 +0100 Subject: [PATCH] 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. --- appview/middleware/middleware.go | 94 ++++++++++++ appview/state/follow.go | 3 +- appview/state/middleware.go | 99 +----------- appview/state/pull.go | 5 +- appview/state/repo.go | 9 +- appview/state/router.go | 47 +++--- appview/state/{ => settings}/settings.go | 186 +++++++++++++---------- appview/state/star.go | 3 +- appview/state/state.go | 8 +- appview/tid.go | 11 ++ 10 files changed, 259 insertions(+), 206 deletions(-) create mode 100644 appview/middleware/middleware.go rename appview/state/{ => settings}/settings.go (63%) create mode 100644 appview/tid.go diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go new file mode 100644 index 00000000..077bee45 --- /dev/null +++ b/appview/middleware/middleware.go @@ -0,0 +1,94 @@ +package middleware + +import ( + "log" + "net/http" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/xrpc" + "tangled.sh/tangled.sh/core/appview" + "tangled.sh/tangled.sh/core/appview/auth" +) + +type Middleware func(http.Handler) http.Handler + +func AuthMiddleware(a *auth.Auth) Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + redirectFunc := func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + } + if r.Header.Get("HX-Request") == "true" { + redirectFunc = func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("HX-Redirect", "/login") + w.WriteHeader(http.StatusOK) + } + } + + session, err := a.GetSession(r) + if session.IsNew || err != nil { + log.Printf("not logged in, redirecting") + redirectFunc(w, r) + return + } + + authorized, ok := session.Values[appview.SessionAuthenticated].(bool) + if !ok || !authorized { + log.Printf("not logged in, redirecting") + redirectFunc(w, r) + return + } + + // refresh if nearing expiry + // TODO: dedup with /login + expiryStr := session.Values[appview.SessionExpiry].(string) + expiry, err := time.Parse(time.RFC3339, expiryStr) + if err != nil { + log.Println("invalid expiry time", err) + redirectFunc(w, r) + return + } + pdsUrl, ok1 := session.Values[appview.SessionPds].(string) + did, ok2 := session.Values[appview.SessionDid].(string) + refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string) + + if !ok1 || !ok2 || !ok3 { + log.Println("invalid expiry time", err) + redirectFunc(w, r) + return + } + + if time.Now().After(expiry) { + log.Println("token expired, refreshing ...") + + client := xrpc.Client{ + Host: pdsUrl, + Auth: &xrpc.AuthInfo{ + Did: did, + AccessJwt: refreshJwt, + RefreshJwt: refreshJwt, + }, + } + atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) + if err != nil { + log.Println("failed to refresh session", err) + redirectFunc(w, r) + return + } + + sessionish := auth.RefreshSessionWrapper{atSession} + + err = a.StoreSession(r, w, &sessionish, pdsUrl) + if err != nil { + log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) + return + } + + log.Println("successfully refreshed token") + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/appview/state/follow.go b/appview/state/follow.go index 6f168020..0688675b 100644 --- a/appview/state/follow.go +++ b/appview/state/follow.go @@ -8,6 +8,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" tangled "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages" ) @@ -36,7 +37,7 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: createdAt := time.Now().Format(time.RFC3339) - rkey := s.TID() + rkey := appview.TID() resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.GraphFollowNSID, Repo: currentUser.Did, diff --git a/appview/state/middleware.go b/appview/state/middleware.go index ce1c3cab..6bafbe1d 100644 --- a/appview/state/middleware.go +++ b/appview/state/middleware.go @@ -10,98 +10,13 @@ import ( "slices" - comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/xrpc" "github.com/go-chi/chi/v5" - "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/middleware" ) -type Middleware func(http.Handler) http.Handler - -func AuthMiddleware(s *State) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - redirectFunc := func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) - } - if r.Header.Get("HX-Request") == "true" { - redirectFunc = func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("HX-Redirect", "/login") - w.WriteHeader(http.StatusOK) - } - } - - session, err := s.auth.GetSession(r) - if session.IsNew || err != nil { - log.Printf("not logged in, redirecting") - redirectFunc(w, r) - return - } - - authorized, ok := session.Values[appview.SessionAuthenticated].(bool) - if !ok || !authorized { - log.Printf("not logged in, redirecting") - redirectFunc(w, r) - return - } - - // refresh if nearing expiry - // TODO: dedup with /login - expiryStr := session.Values[appview.SessionExpiry].(string) - expiry, err := time.Parse(time.RFC3339, expiryStr) - if err != nil { - log.Println("invalid expiry time", err) - redirectFunc(w, r) - return - } - pdsUrl, ok1 := session.Values[appview.SessionPds].(string) - did, ok2 := session.Values[appview.SessionDid].(string) - refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string) - - if !ok1 || !ok2 || !ok3 { - log.Println("invalid expiry time", err) - redirectFunc(w, r) - return - } - - if time.Now().After(expiry) { - log.Println("token expired, refreshing ...") - - client := xrpc.Client{ - Host: pdsUrl, - Auth: &xrpc.AuthInfo{ - Did: did, - AccessJwt: refreshJwt, - RefreshJwt: refreshJwt, - }, - } - atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) - if err != nil { - log.Println("failed to refresh session", err) - redirectFunc(w, r) - return - } - - sessionish := auth.RefreshSessionWrapper{atSession} - - err = s.auth.StoreSession(r, w, &sessionish, pdsUrl) - if err != nil { - log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) - return - } - - log.Println("successfully refreshed token") - } - - next.ServeHTTP(w, r) - }) - } -} - -func knotRoleMiddleware(s *State, group string) Middleware { +func knotRoleMiddleware(s *State, group string) middleware.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // requires auth also @@ -131,11 +46,11 @@ func knotRoleMiddleware(s *State, group string) Middleware { } } -func KnotOwner(s *State) Middleware { +func KnotOwner(s *State) middleware.Middleware { return knotRoleMiddleware(s, "server:owner") } -func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware { +func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // requires auth also @@ -175,7 +90,7 @@ func StripLeadingAt(next http.Handler) http.Handler { }) } -func ResolveIdent(s *State) Middleware { +func ResolveIdent(s *State) middleware.Middleware { excluded := []string{"favicon.ico"} return func(next http.Handler) http.Handler { @@ -201,7 +116,7 @@ func ResolveIdent(s *State) Middleware { } } -func ResolveRepo(s *State) Middleware { +func ResolveRepo(s *State) middleware.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { repoName := chi.URLParam(req, "repo") @@ -230,7 +145,7 @@ func ResolveRepo(s *State) Middleware { } // middleware that is tacked on top of /{user}/{repo}/pulls/{pull} -func ResolvePull(s *State) Middleware { +func ResolvePull(s *State) middleware.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { f, err := fullyResolvedRepo(r) diff --git a/appview/state/pull.go b/appview/state/pull.go index fc1e07df..064f707f 100644 --- a/appview/state/pull.go +++ b/appview/state/pull.go @@ -13,6 +13,7 @@ import ( "time" "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages" @@ -521,7 +522,7 @@ func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullCommentNSID, Repo: user.Did, - Rkey: s.TID(), + Rkey: appview.TID(), Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoPullComment{ Repo: &atUri, @@ -846,7 +847,7 @@ func (s *State) createPullRequest( body = formatPatches[0].Body } - rkey := s.TID() + rkey := appview.TID() initialSubmission := db.PullSubmission{ Patch: patch, SourceRev: sourceRev, diff --git a/appview/state/repo.go b/appview/state/repo.go index 67509c94..7c4b2121 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -23,6 +23,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing" "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages" @@ -1116,7 +1117,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueStateNSID, Repo: user.Did, - Rkey: s.TID(), + Rkey: appview.TID(), Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoIssueState{ Issue: issue.IssueAt, @@ -1220,7 +1221,7 @@ func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { } commentId := mathrand.IntN(1000000) - rkey := s.TID() + rkey := appview.TID() err := db.NewIssueComment(s.db, &db.Comment{ OwnerDid: user.Did, @@ -1650,7 +1651,7 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueNSID, Repo: user.Did, - Rkey: s.TID(), + Rkey: appview.TID(), Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.RepoIssue{ Repo: atUri, @@ -1754,7 +1755,7 @@ func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) sourceAt := f.RepoAt.String() - rkey := s.TID() + rkey := appview.TID() repo := &db.Repo{ Did: user.Did, Name: forkName, diff --git a/appview/state/router.go b/appview/state/router.go index 92e87b83..f45898c0 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/go-chi/chi/v5" + "tangled.sh/tangled.sh/core/appview/middleware" + "tangled.sh/tangled.sh/core/appview/state/settings" "tangled.sh/tangled.sh/core/appview/state/userutil" ) @@ -70,7 +72,7 @@ func (s *State) UserRouter() http.Handler { r.Get("/{issue}", s.RepoSingleIssue) r.Group(func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) r.Get("/new", s.NewIssue) r.Post("/new", s.NewIssue) r.Post("/{issue}/comment", s.NewIssueComment) @@ -86,14 +88,14 @@ func (s *State) UserRouter() http.Handler { }) r.Route("/fork", func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) r.Get("/", s.ForkRepo) r.Post("/", s.ForkRepo) }) r.Route("/pulls", func(r chi.Router) { r.Get("/", s.RepoPulls) - r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) { r.Get("/", s.NewPull) r.Get("/patch-upload", s.PatchUploadFragment) r.Post("/validate-patch", s.ValidatePatch) @@ -111,7 +113,7 @@ func (s *State) UserRouter() http.Handler { r.Get("/", s.RepoPullPatch) r.Get("/interdiff", s.RepoPullInterdiff) r.Get("/actions", s.PullActions) - r.With(AuthMiddleware(s)).Route("/comment", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) { r.Get("/", s.PullComment) r.Post("/", s.PullComment) }) @@ -122,7 +124,7 @@ func (s *State) UserRouter() http.Handler { }) r.Group(func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) r.Route("/resubmit", func(r chi.Router) { r.Get("/", s.ResubmitPull) r.Post("/", s.ResubmitPull) @@ -145,7 +147,7 @@ func (s *State) UserRouter() http.Handler { // settings routes, needs auth r.Group(func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) // repo description can only be edited by owner r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { r.Put("/", s.RepoDescription) @@ -176,7 +178,7 @@ func (s *State) StandardRouter() http.Handler { r.Get("/", s.Timeline) - r.With(AuthMiddleware(s)).Post("/logout", s.Logout) + r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout) r.Route("/login", func(r chi.Router) { r.Get("/", s.Login) @@ -184,7 +186,7 @@ func (s *State) StandardRouter() http.Handler { }) r.Route("/knots", func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) r.Get("/", s.Knots) r.Post("/key", s.RegistrationKey) @@ -202,34 +204,24 @@ func (s *State) StandardRouter() http.Handler { r.Route("/repo", func(r chi.Router) { r.Route("/new", func(r chi.Router) { - r.Use(AuthMiddleware(s)) + r.Use(middleware.AuthMiddleware(s.auth)) r.Get("/", s.NewRepo) r.Post("/", s.NewRepo) }) // r.Post("/import", s.ImportRepo) }) - r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) { r.Post("/", s.Follow) r.Delete("/", s.Follow) }) - r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) { r.Post("/", s.Star) r.Delete("/", s.Star) }) - r.Route("/settings", func(r chi.Router) { - r.Use(AuthMiddleware(s)) - r.Get("/", s.Settings) - r.Put("/keys", s.SettingsKeys) - r.Delete("/keys", s.SettingsKeys) - r.Put("/emails", s.SettingsEmails) - r.Delete("/emails", s.SettingsEmails) - r.Get("/emails/verify", s.SettingsEmailsVerify) - r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend) - r.Post("/emails/primary", s.SettingsEmailsPrimary) - }) + r.Route("/settings", s.SettingsRouter) r.Get("/keys/{user}", s.Keys) @@ -238,3 +230,14 @@ func (s *State) StandardRouter() http.Handler { }) return r } + +func (s *State) SettingsRouter(r chi.Router) { + settings := &settings.Settings{ + Db: s.db, + Auth: s.auth, + Pages: s.pages, + Config: s.config, + } + + settings.Router(r) +} diff --git a/appview/state/settings.go b/appview/state/settings/settings.go similarity index 63% rename from appview/state/settings.go rename to appview/state/settings/settings.go index f1b8c5b2..056ade6b 100644 --- a/appview/state/settings.go +++ b/appview/state/settings/settings.go @@ -1,4 +1,4 @@ -package state +package settings import ( "database/sql" @@ -10,29 +10,55 @@ import ( "strings" "time" - comatproto "github.com/bluesky-social/indigo/api/atproto" - lexutil "github.com/bluesky-social/indigo/lex/util" - "github.com/gliderlabs/ssh" - "github.com/google/uuid" + "github.com/go-chi/chi/v5" "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" + "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/email" + "tangled.sh/tangled.sh/core/appview/middleware" "tangled.sh/tangled.sh/core/appview/pages" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/gliderlabs/ssh" + "github.com/google/uuid" ) -func (s *State) Settings(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) - pubKeys, err := db.GetPublicKeys(s.db, user.Did) +type Settings struct { + Db *db.DB + Auth *auth.Auth + Pages *pages.Pages + Config *appview.Config +} + +func (s *Settings) Router(r chi.Router) { + r.Use(middleware.AuthMiddleware(s.Auth)) + + r.Get("/", s.settings) + r.Put("/keys", s.keys) + r.Delete("/keys", s.keys) + r.Put("/emails", s.emails) + r.Delete("/emails", s.emails) + r.Get("/emails/verify", s.emailsVerify) + r.Post("/emails/verify/resend", s.emailsVerifyResend) + r.Post("/emails/primary", s.emailsPrimary) + +} + +func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { + user := s.Auth.GetUser(r) + pubKeys, err := db.GetPublicKeys(s.Db, user.Did) if err != nil { log.Println(err) } - emails, err := db.GetAllEmails(s.db, user.Did) + emails, err := db.GetAllEmails(s.Db, user.Did) if err != nil { log.Println(err) } - s.pages.Settings(w, pages.SettingsParams{ + s.Pages.Settings(w, pages.SettingsParams{ LoggedInUser: user, PubKeys: pubKeys, Emails: emails, @@ -40,11 +66,11 @@ func (s *State) Settings(w http.ResponseWriter, r *http.Request) { } // buildVerificationEmail creates an email.Email struct for verification emails -func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email { +func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Email { verifyURL := s.verifyUrl(did, emailAddr, code) return email.Email{ - APIKey: s.config.ResendApiKey, + APIKey: s.Config.ResendApiKey, From: "noreply@notifs.tangled.sh", To: emailAddr, Subject: "Verify your Tangled email", @@ -56,60 +82,60 @@ func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email } // sendVerificationEmail handles the common logic for sending verification emails -func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { +func (s *Settings) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { emailToSend := s.buildVerificationEmail(emailAddr, did, code) err := email.SendEmail(emailToSend) if err != nil { log.Printf("sending email: %s", err) - s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) + s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) return err } return nil } -func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { +func (s *Settings) emails(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.pages.Notice(w, "settings-emails", "Unimplemented.") + s.Pages.Notice(w, "settings-emails", "Unimplemented.") log.Println("unimplemented") return case http.MethodPut: - did := s.auth.GetDid(r) + did := s.Auth.GetDid(r) emAddr := r.FormValue("email") emAddr = strings.TrimSpace(emAddr) if !email.IsValidEmail(emAddr) { - s.pages.Notice(w, "settings-emails-error", "Invalid email address.") + s.Pages.Notice(w, "settings-emails-error", "Invalid email address.") return } // check if email already exists in database - existingEmail, err := db.GetEmail(s.db, did, emAddr) + existingEmail, err := db.GetEmail(s.Db, did, emAddr) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Printf("checking for existing email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") return } if err == nil { if existingEmail.Verified { - s.pages.Notice(w, "settings-emails-error", "This email is already verified.") + s.Pages.Notice(w, "settings-emails-error", "This email is already verified.") return } - s.pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.") + s.Pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.") return } code := uuid.New().String() // Begin transaction - tx, err := s.db.Begin() + tx, err := s.Db.Begin() if err != nil { log.Printf("failed to start transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") return } defer tx.Rollback() @@ -121,7 +147,7 @@ func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { VerificationCode: code, }); err != nil { log.Printf("adding email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") return } @@ -132,48 +158,48 @@ func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { // Commit transaction if err := tx.Commit(); err != nil { log.Printf("failed to commit transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") return } - s.pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") + s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") return case http.MethodDelete: - did := s.auth.GetDid(r) + did := s.Auth.GetDid(r) emailAddr := r.FormValue("email") emailAddr = strings.TrimSpace(emailAddr) // Begin transaction - tx, err := s.db.Begin() + tx, err := s.Db.Begin() if err != nil { log.Printf("failed to start transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") return } defer tx.Rollback() if err := db.DeleteEmail(tx, did, emailAddr); err != nil { log.Printf("deleting email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") return } // Commit transaction if err := tx.Commit(); err != nil { log.Printf("failed to commit transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") return } - s.pages.HxLocation(w, "/settings") + s.Pages.HxLocation(w, "/settings") return } } -func (s *State) verifyUrl(did string, email string, code string) string { +func (s *Settings) verifyUrl(did string, email string, code string) string { var appUrl string - if s.config.Dev { - appUrl = "http://" + s.config.ListenAddr + if s.Config.Dev { + appUrl = "http://" + s.Config.ListenAddr } else { appUrl = "https://tangled.sh" } @@ -181,7 +207,7 @@ func (s *State) verifyUrl(did string, email string, code string) string { return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) } -func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) { +func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() // Get the parameters directly from the query @@ -189,57 +215,57 @@ func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) { did := q.Get("did") code := q.Get("code") - valid, err := db.CheckValidVerificationCode(s.db, did, emailAddr, code) + valid, err := db.CheckValidVerificationCode(s.Db, did, emailAddr, code) if err != nil { log.Printf("checking email verification: %s", err) - s.pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.") + s.Pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.") return } if !valid { - s.pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.") + s.Pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.") return } // Mark email as verified in the database - if err := db.MarkEmailVerified(s.db, did, emailAddr); err != nil { + if err := db.MarkEmailVerified(s.Db, did, emailAddr); err != nil { log.Printf("marking email as verified: %s", err) - s.pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.") + s.Pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.") return } http.Redirect(w, r, "/settings", http.StatusSeeOther) } -func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) { +func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - s.pages.Notice(w, "settings-emails-error", "Invalid request method.") + s.Pages.Notice(w, "settings-emails-error", "Invalid request method.") return } - did := s.auth.GetDid(r) + did := s.Auth.GetDid(r) emAddr := r.FormValue("email") emAddr = strings.TrimSpace(emAddr) if !email.IsValidEmail(emAddr) { - s.pages.Notice(w, "settings-emails-error", "Invalid email address.") + s.Pages.Notice(w, "settings-emails-error", "Invalid email address.") return } // Check if email exists and is unverified - existingEmail, err := db.GetEmail(s.db, did, emAddr) + existingEmail, err := db.GetEmail(s.Db, did, emAddr) if err != nil { if errors.Is(err, sql.ErrNoRows) { - s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") + s.Pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") } else { log.Printf("checking for existing email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") } return } if existingEmail.Verified { - s.pages.Notice(w, "settings-emails-error", "This email is already verified.") + s.Pages.Notice(w, "settings-emails-error", "This email is already verified.") return } @@ -248,7 +274,7 @@ func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Reques timeSinceLastSent := time.Since(*existingEmail.LastSent) if timeSinceLastSent < 10*time.Minute { waitTime := 10*time.Minute - timeSinceLastSent - s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) + s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) return } } @@ -257,10 +283,10 @@ func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Reques code := uuid.New().String() // Begin transaction - tx, err := s.db.Begin() + tx, err := s.Db.Begin() if err != nil { log.Printf("failed to start transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") return } defer tx.Rollback() @@ -268,7 +294,7 @@ func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Reques // Update the verification code and last sent time if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { log.Printf("updating email verification: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") return } @@ -280,65 +306,65 @@ func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Reques // Commit transaction if err := tx.Commit(); err != nil { log.Printf("failed to commit transaction: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") return } - s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") + s.Pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") } -func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) { - did := s.auth.GetDid(r) +func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { + did := s.Auth.GetDid(r) emailAddr := r.FormValue("email") emailAddr = strings.TrimSpace(emailAddr) if emailAddr == "" { - s.pages.Notice(w, "settings-emails-error", "Email address cannot be empty.") + s.Pages.Notice(w, "settings-emails-error", "Email address cannot be empty.") return } - if err := db.MakeEmailPrimary(s.db, did, emailAddr); err != nil { + if err := db.MakeEmailPrimary(s.Db, did, emailAddr); err != nil { log.Printf("setting primary email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.") + s.Pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.") return } - s.pages.HxLocation(w, "/settings") + s.Pages.HxLocation(w, "/settings") } -func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { +func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - s.pages.Notice(w, "settings-keys", "Unimplemented.") + s.Pages.Notice(w, "settings-keys", "Unimplemented.") log.Println("unimplemented") return case http.MethodPut: - did := s.auth.GetDid(r) + did := s.Auth.GetDid(r) key := r.FormValue("key") key = strings.TrimSpace(key) name := r.FormValue("name") - client, _ := s.auth.AuthorizedClient(r) + client, _ := s.Auth.AuthorizedClient(r) _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) if err != nil { log.Printf("parsing public key: %s", err) - s.pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a public key.") + s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a public key.") return } - rkey := s.TID() + rkey := appview.TID() - tx, err := s.db.Begin() + tx, err := s.Db.Begin() if err != nil { log.Printf("failed to start tx; adding public key: %s", err) - s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") + s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") return } defer tx.Rollback() if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil { log.Printf("adding public key: %s", err) - s.pages.Notice(w, "settings-keys", "Failed to add public key.") + s.Pages.Notice(w, "settings-keys", "Failed to add public key.") return } @@ -357,7 +383,7 @@ func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { // invalid record if err != nil { log.Printf("failed to create record: %s", err) - s.pages.Notice(w, "settings-keys", "Failed to create record.") + s.Pages.Notice(w, "settings-keys", "Failed to create record.") return } @@ -366,15 +392,15 @@ func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { err = tx.Commit() if err != nil { log.Printf("failed to commit tx; adding public key: %s", err) - s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") + s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") return } - s.pages.HxLocation(w, "/settings") + s.Pages.HxLocation(w, "/settings") return case http.MethodDelete: - did := s.auth.GetDid(r) + did := s.Auth.GetDid(r) q := r.URL.Query() name := q.Get("name") @@ -385,11 +411,11 @@ func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { log.Println(rkey) log.Println(key) - client, _ := s.auth.AuthorizedClient(r) + client, _ := s.Auth.AuthorizedClient(r) - if err := db.RemovePublicKey(s.db, did, name, key); err != nil { + if err := db.RemovePublicKey(s.Db, did, name, key); err != nil { log.Printf("removing public key: %s", err) - s.pages.Notice(w, "settings-keys", "Failed to remove public key.") + s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") return } @@ -404,13 +430,13 @@ func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { // invalid record if err != nil { log.Printf("failed to delete record from PDS: %s", err) - s.pages.Notice(w, "settings-keys", "Failed to remove key from PDS.") + s.Pages.Notice(w, "settings-keys", "Failed to remove key from PDS.") return } } log.Println("deleted successfully") - s.pages.HxLocation(w, "/settings") + s.Pages.HxLocation(w, "/settings") return } } diff --git a/appview/state/star.go b/appview/state/star.go index 1fb4ddbc..a11217d6 100644 --- a/appview/state/star.go +++ b/appview/state/star.go @@ -9,6 +9,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" tangled "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/pages" ) @@ -33,7 +34,7 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: createdAt := time.Now().Format(time.RFC3339) - rkey := s.TID() + rkey := appview.TID() resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.FeedStarNSID, Repo: currentUser.Did, diff --git a/appview/state/state.go b/appview/state/state.go index 1893edc8..29563fa3 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -91,8 +91,8 @@ func Make(config *appview.Config) (*State, error) { return state, nil } -func (s *State) TID() string { - return s.tidClock.Next().String() +func TID(c *syntax.TIDClock) string { + return c.Next().String() } func (s *State) Login(w http.ResponseWriter, r *http.Request) { @@ -522,7 +522,7 @@ func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ Collection: tangled.KnotMemberNSID, Repo: currentUser.Did, - Rkey: s.TID(), + Rkey: appview.TID(), Record: &lexutil.LexiconTypeDecoder{ Val: &tangled.KnotMember{ Member: memberIdent.DID.String(), @@ -646,7 +646,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } - rkey := s.TID() + rkey := appview.TID() repo := &db.Repo{ Did: user.Did, Name: repoName, diff --git a/appview/tid.go b/appview/tid.go new file mode 100644 index 00000000..46a4da67 --- /dev/null +++ b/appview/tid.go @@ -0,0 +1,11 @@ +package appview + +import ( + "github.com/bluesky-social/indigo/atproto/syntax" +) + +var c *syntax.TIDClock = syntax.NewTIDClock(0) + +func TID() string { + return c.Next().String() +} -- 2.51.2