forked from tangled.org/core
this repo has no description

appview: refactor settings router

move settings router into a subpackage

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

Changed files
+259 -206
appview
+94
appview/middleware/middleware.go
···
+
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)
+
})
+
}
+
}
+2 -1
appview/state/follow.go
···
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"
)
···
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,
+7 -92
appview/state/middleware.go
···
"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
···
}
}
-
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
···
})
}
-
func ResolveIdent(s *State) Middleware {
+
func ResolveIdent(s *State) middleware.Middleware {
excluded := []string{"favicon.ico"}
return func(next http.Handler) http.Handler {
···
}
}
-
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")
···
}
// 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)
+3 -2
appview/state/pull.go
···
"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"
···
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,
···
body = formatPatches[0].Body
}
-
rkey := s.TID()
+
rkey := appview.TID()
initialSubmission := db.PullSubmission{
Patch: patch,
SourceRev: sourceRev,
+5 -4
appview/state/repo.go
···
"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"
···
_, 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,
···
commentId := mathrand.IntN(1000000)
-
rkey := s.TID()
+
rkey := appview.TID()
err := db.NewIssueComment(s.db, &db.Comment{
OwnerDid: user.Did,
···
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,
···
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,
+25 -22
appview/state/router.go
···
"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"
)
···
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)
···
})
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)
···
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)
})
···
})
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)
···
// 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)
···
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)
···
})
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)
···
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)
···
})
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)
+
}
+106 -80
appview/state/settings.go appview/state/settings/settings.go
···
-
package state
+
package settings
import (
"database/sql"
···
"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,
···
}
// 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",
···
}
// 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()
···
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
}
···
// 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"
}
···
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
···
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
}
···
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
}
}
···
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()
···
// 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
}
···
// 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 <strong>public</strong> key.")
+
s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> 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
}
···
// 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
}
···
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")
···
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
}
···
// 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
}
}
+2 -1
appview/state/star.go
···
"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"
)
···
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,
+4 -4
appview/state/state.go
···
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) {
···
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(),
···
return
}
-
rkey := s.TID()
+
rkey := appview.TID()
repo := &db.Repo{
Did: user.Did,
Name: repoName,
+11
appview/tid.go
···
+
package appview
+
+
import (
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
)
+
+
var c *syntax.TIDClock = syntax.NewTIDClock(0)
+
+
func TID() string {
+
return c.Next().String()
+
}