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()
+}