From 2ebe4e46bed062f522e92b80e5423e8d005be306 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 4 Mar 2025 22:25:09 +0000 Subject: [PATCH] implement transactions --- appview/db/db.go | 16 ++- appview/db/follow.go | 24 ++--- appview/db/issues.go | 54 +++++------ appview/db/jetstream.go | 16 +-- appview/db/pubkeys.go | 16 +-- appview/db/registration.go | 22 ++--- appview/db/repos.go | 28 +++--- appview/db/timeline.go | 6 +- appview/pages/templates/repo/new.html | 2 +- appview/state/follow.go | 7 +- appview/state/jetstream.go | 6 +- appview/state/middleware.go | 3 +- appview/state/repo.go | 58 ++++++++--- appview/state/settings.go | 5 +- appview/state/state.go | 135 +++++++++++++++++++------- rbac/rbac.go | 15 +-- 16 files changed, 263 insertions(+), 150 deletions(-) diff --git a/appview/db/db.go b/appview/db/db.go index 9f5d0f5a..ad0c957e 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -1,13 +1,25 @@ package db import ( + "context" "database/sql" _ "github.com/mattn/go-sqlite3" ) type DB struct { - db *sql.DB + *sql.DB +} + +type Execer interface { + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + Prepare(query string) (*sql.Stmt, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } func Make(dbPath string) (*DB, error) { @@ -104,5 +116,5 @@ func Make(dbPath string) (*DB, error) { if err != nil { return nil, err } - return &DB{db: db}, nil + return &DB{db}, nil } diff --git a/appview/db/follow.go b/appview/db/follow.go index 2a18bce3..795fc485 100644 --- a/appview/db/follow.go +++ b/appview/db/follow.go @@ -12,16 +12,16 @@ type Follow struct { RKey string } -func (d *DB) AddFollow(userDid, subjectDid, rkey string) error { +func AddFollow(e Execer, userDid, subjectDid, rkey string) error { query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)` - _, err := d.db.Exec(query, userDid, subjectDid, rkey) + _, err := e.Exec(query, userDid, subjectDid, rkey) return err } // Get a follow record -func (d *DB) GetFollow(userDid, subjectDid string) (*Follow, error) { +func GetFollow(e Execer, userDid, subjectDid string) (*Follow, error) { query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?` - row := d.db.QueryRow(query, userDid, subjectDid) + row := e.QueryRow(query, userDid, subjectDid) var follow Follow var followedAt string @@ -42,14 +42,14 @@ func (d *DB) GetFollow(userDid, subjectDid string) (*Follow, error) { } // Get a follow record -func (d *DB) DeleteFollow(userDid, subjectDid string) error { - _, err := d.db.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid) +func DeleteFollow(e Execer, userDid, subjectDid string) error { + _, err := e.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid) return err } -func (d *DB) GetFollowerFollowing(did string) (int, int, error) { +func GetFollowerFollowing(e Execer, did string) (int, int, error) { followers, following := 0, 0 - err := d.db.QueryRow( + err := e.QueryRow( `SELECT COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, COUNT(CASE WHEN user_did = ? THEN 1 END) AS following @@ -81,20 +81,20 @@ func (s FollowStatus) String() string { } } -func (d *DB) GetFollowStatus(userDid, subjectDid string) FollowStatus { +func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { if userDid == subjectDid { return IsSelf - } else if _, err := d.GetFollow(userDid, subjectDid); err != nil { + } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { return IsNotFollowing } else { return IsFollowing } } -func (d *DB) GetAllFollows() ([]Follow, error) { +func GetAllFollows(e Execer) ([]Follow, error) { var follows []Follow - rows, err := d.db.Query(`select user_did, subject_did, followed_at, rkey from follows`) + rows, err := e.Query(`select user_did, subject_did, followed_at, rkey from follows`) if err != nil { return nil, err } diff --git a/appview/db/issues.go b/appview/db/issues.go index 89c1c15f..122a9686 100644 --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -26,14 +26,10 @@ type Comment struct { Created *time.Time } -func (d *DB) NewIssue(issue *Issue) error { - tx, err := d.db.Begin() - if err != nil { - return err - } +func NewIssue(tx *sql.Tx, issue *Issue) error { defer tx.Rollback() - _, err = tx.Exec(` + _, err := tx.Exec(` insert or ignore into repo_issue_seqs (repo_at, next_issue_id) values (?, 1) `, issue.RepoAt) @@ -69,33 +65,33 @@ func (d *DB) NewIssue(issue *Issue) error { return nil } -func (d *DB) SetIssueAt(repoAt string, issueId int, issueAt string) error { - _, err := d.db.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) +func SetIssueAt(e Execer, repoAt string, issueId int, issueAt string) error { + _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) return err } -func (d *DB) GetIssueAt(repoAt string, issueId int) (string, error) { +func GetIssueAt(e Execer, repoAt string, issueId int) (string, error) { var issueAt string - err := d.db.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) + err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) return issueAt, err } -func (d *DB) GetIssueId(repoAt string) (int, error) { +func GetIssueId(e Execer, repoAt string) (int, error) { var issueId int - err := d.db.QueryRow(`select next_issue_id from repo_issue_seqs where repo_at = ?`, repoAt).Scan(&issueId) + err := e.QueryRow(`select next_issue_id from repo_issue_seqs where repo_at = ?`, repoAt).Scan(&issueId) return issueId - 1, err } -func (d *DB) GetIssueOwnerDid(repoAt string, issueId int) (string, error) { +func GetIssueOwnerDid(e Execer, repoAt string, issueId int) (string, error) { var ownerDid string - err := d.db.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) + err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) return ownerDid, err } -func (d *DB) GetIssues(repoAt string) ([]Issue, error) { +func GetIssues(e Execer, repoAt string) ([]Issue, error) { var issues []Issue - rows, err := d.db.Query(`select owner_did, issue_id, created, title, body, open from issues where repo_at = ? order by created desc`, repoAt) + rows, err := e.Query(`select owner_did, issue_id, created, title, body, open from issues where repo_at = ? order by created desc`, repoAt) if err != nil { return nil, err } @@ -125,9 +121,9 @@ func (d *DB) GetIssues(repoAt string) ([]Issue, error) { return issues, nil } -func (d *DB) GetIssue(repoAt string, issueId int) (*Issue, error) { +func GetIssue(e Execer, repoAt string, issueId int) (*Issue, error) { query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` - row := d.db.QueryRow(query, repoAt, issueId) + row := e.QueryRow(query, repoAt, issueId) var issue Issue var createdAt string @@ -145,9 +141,9 @@ func (d *DB) GetIssue(repoAt string, issueId int) (*Issue, error) { return &issue, nil } -func (d *DB) GetIssueWithComments(repoAt string, issueId int) (*Issue, []Comment, error) { +func GetIssueWithComments(e Execer, repoAt string, issueId int) (*Issue, []Comment, error) { query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` - row := d.db.QueryRow(query, repoAt, issueId) + row := e.QueryRow(query, repoAt, issueId) var issue Issue var createdAt string @@ -162,7 +158,7 @@ func (d *DB) GetIssueWithComments(repoAt string, issueId int) (*Issue, []Comment } issue.Created = &createdTime - comments, err := d.GetComments(repoAt, issueId) + comments, err := GetComments(e, repoAt, issueId) if err != nil { return nil, nil, err } @@ -170,9 +166,9 @@ func (d *DB) GetIssueWithComments(repoAt string, issueId int) (*Issue, []Comment return &issue, comments, nil } -func (d *DB) NewComment(comment *Comment) error { +func NewComment(e Execer, comment *Comment) error { query := `insert into comments (owner_did, repo_at, comment_at, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)` - _, err := d.db.Exec( + _, err := e.Exec( query, comment.OwnerDid, comment.RepoAt, @@ -184,10 +180,10 @@ func (d *DB) NewComment(comment *Comment) error { return err } -func (d *DB) GetComments(repoAt string, issueId int) ([]Comment, error) { +func GetComments(e Execer, repoAt string, issueId int) ([]Comment, error) { var comments []Comment - rows, err := d.db.Query(`select owner_did, issue_id, comment_id, comment_at, body, created from comments where repo_at = ? and issue_id = ? order by created asc`, repoAt, issueId) + rows, err := e.Query(`select owner_did, issue_id, comment_id, comment_at, body, created from comments where repo_at = ? and issue_id = ? order by created asc`, repoAt, issueId) if err == sql.ErrNoRows { return []Comment{}, nil } @@ -220,12 +216,12 @@ func (d *DB) GetComments(repoAt string, issueId int) ([]Comment, error) { return comments, nil } -func (d *DB) CloseIssue(repoAt string, issueId int) error { - _, err := d.db.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) +func CloseIssue(e Execer, repoAt string, issueId int) error { + _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) return err } -func (d *DB) ReopenIssue(repoAt string, issueId int) error { - _, err := d.db.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId) +func ReopenIssue(e Execer, repoAt string, issueId int) error { + _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId) return err } diff --git a/appview/db/jetstream.go b/appview/db/jetstream.go index f2c4c9d6..f4196387 100644 --- a/appview/db/jetstream.go +++ b/appview/db/jetstream.go @@ -1,21 +1,25 @@ package db -func (d *DB) SaveLastTimeUs(lastTimeUs int64) error { - _, err := d.db.Exec(`insert into _jetstream (last_time_us) values (?)`, lastTimeUs) +type DbWrapper struct { + Execer +} + +func (db DbWrapper) SaveLastTimeUs(lastTimeUs int64) error { + _, err := db.Exec(`insert into _jetstream (last_time_us) values (?)`, lastTimeUs) return err } -func (d *DB) UpdateLastTimeUs(lastTimeUs int64) error { - _, err := d.db.Exec(`update _jetstream set last_time_us = ? where rowid = 1`, lastTimeUs) +func (db DbWrapper) UpdateLastTimeUs(lastTimeUs int64) error { + _, err := db.Exec(`update _jetstream set last_time_us = ? where rowid = 1`, lastTimeUs) if err != nil { return err } return nil } -func (d *DB) GetLastTimeUs() (int64, error) { +func (db DbWrapper) GetLastTimeUs() (int64, error) { var lastTimeUs int64 - row := d.db.QueryRow(`select last_time_us from _jetstream`) + row := db.QueryRow(`select last_time_us from _jetstream`) err := row.Scan(&lastTimeUs) return lastTimeUs, err } diff --git a/appview/db/pubkeys.go b/appview/db/pubkeys.go index 408c2c40..552745cc 100644 --- a/appview/db/pubkeys.go +++ b/appview/db/pubkeys.go @@ -5,15 +5,15 @@ import ( "time" ) -func (d *DB) AddPublicKey(did, name, key string) error { +func AddPublicKey(e Execer, did, name, key string) error { query := `insert or ignore into public_keys (did, name, key) values (?, ?, ?)` - _, err := d.db.Exec(query, did, name, key) + _, err := e.Exec(query, did, name, key) return err } -func (d *DB) RemovePublicKey(did string) error { +func RemovePublicKey(e Execer, did string) error { query := `delete from public_keys where did = ?` - _, err := d.db.Exec(query, did) + _, err := e.Exec(query, did) return err } @@ -35,10 +35,10 @@ func (p PublicKey) MarshalJSON() ([]byte, error) { }) } -func (d *DB) GetAllPublicKeys() ([]PublicKey, error) { +func GetAllPublicKeys(e Execer) ([]PublicKey, error) { var keys []PublicKey - rows, err := d.db.Query(`select key, name, did, created from public_keys`) + rows, err := e.Query(`select key, name, did, created from public_keys`) if err != nil { return nil, err } @@ -62,10 +62,10 @@ func (d *DB) GetAllPublicKeys() ([]PublicKey, error) { return keys, nil } -func (d *DB) GetPublicKeys(did string) ([]PublicKey, error) { +func GetPublicKeys(e Execer, did string) ([]PublicKey, error) { var keys []PublicKey - rows, err := d.db.Query(`select did, key, name, created from public_keys where did = ?`, did) + rows, err := e.Query(`select did, key, name, created from public_keys where did = ?`, did) if err != nil { return nil, err } diff --git a/appview/db/registration.go b/appview/db/registration.go index c82e100c..b354f59d 100644 --- a/appview/db/registration.go +++ b/appview/db/registration.go @@ -32,10 +32,10 @@ const ( ) // returns registered status, did of owner, error -func (d *DB) RegistrationsByDid(did string) ([]Registration, error) { +func RegistrationsByDid(e Execer, did string) ([]Registration, error) { var registrations []Registration - rows, err := d.db.Query(` + rows, err := e.Query(` select domain, did, created, registered from registrations where did = ? `, did) @@ -69,12 +69,12 @@ func (d *DB) RegistrationsByDid(did string) ([]Registration, error) { } // returns registered status, did of owner, error -func (d *DB) RegistrationByDomain(domain string) (*Registration, error) { +func RegistrationByDomain(e Execer, domain string) (*Registration, error) { var createdAt *string var registeredAt *string var registration Registration - err := d.db.QueryRow(` + err := e.QueryRow(` select domain, did, created, registered from registrations where domain = ? `, domain).Scan(®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt) @@ -106,9 +106,9 @@ func genSecret() string { return hex.EncodeToString(key) } -func (d *DB) GenerateRegistrationKey(domain, did string) (string, error) { +func GenerateRegistrationKey(e Execer, domain, did string) (string, error) { // sanity check: does this domain already have a registration? - reg, err := d.RegistrationByDomain(domain) + reg, err := RegistrationByDomain(e, domain) if err != nil { return "", err } @@ -127,7 +127,7 @@ func (d *DB) GenerateRegistrationKey(domain, did string) (string, error) { secret := genSecret() - _, err = d.db.Exec(` + _, err = e.Exec(` insert into registrations (domain, did, secret) values (?, ?, ?) on conflict(domain) do update set did = excluded.did, secret = excluded.secret @@ -140,8 +140,8 @@ func (d *DB) GenerateRegistrationKey(domain, did string) (string, error) { return secret, nil } -func (d *DB) GetRegistrationKey(domain string) (string, error) { - res := d.db.QueryRow(`select secret from registrations where domain = ?`, domain) +func GetRegistrationKey(e Execer, domain string) (string, error) { + res := e.QueryRow(`select secret from registrations where domain = ?`, domain) var secret string err := res.Scan(&secret) @@ -152,8 +152,8 @@ func (d *DB) GetRegistrationKey(domain string) (string, error) { return secret, nil } -func (d *DB) Register(domain string) error { - _, err := d.db.Exec(` +func Register(e Execer, domain string) error { + _, err := e.Exec(` update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where domain = ?; diff --git a/appview/db/repos.go b/appview/db/repos.go index 90cdc7e6..6927614b 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -14,10 +14,10 @@ type Repo struct { AtUri string } -func (d *DB) GetAllRepos() ([]Repo, error) { +func GetAllRepos(e Execer) ([]Repo, error) { var repos []Repo - rows, err := d.db.Query(`select did, name, knot, rkey, created from repos`) + rows, err := e.Query(`select did, name, knot, rkey, created from repos`) if err != nil { return nil, err } @@ -39,10 +39,10 @@ func (d *DB) GetAllRepos() ([]Repo, error) { return repos, nil } -func (d *DB) GetAllReposByDid(did string) ([]Repo, error) { +func GetAllReposByDid(e Execer, did string) ([]Repo, error) { var repos []Repo - rows, err := d.db.Query(`select did, name, knot, rkey, created from repos where did = ?`, did) + rows, err := e.Query(`select did, name, knot, rkey, created from repos where did = ?`, did) if err != nil { return nil, err } @@ -64,10 +64,10 @@ func (d *DB) GetAllReposByDid(did string) ([]Repo, error) { return repos, nil } -func (d *DB) GetRepo(did, name string) (*Repo, error) { +func GetRepo(e Execer, did, name string) (*Repo, error) { var repo Repo - row := d.db.QueryRow(`select did, name, knot, created, at_uri from repos where did = ? and name = ?`, did, name) + row := e.QueryRow(`select did, name, knot, created, at_uri from repos where did = ? and name = ?`, did, name) var createdAt string if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { @@ -79,28 +79,28 @@ func (d *DB) GetRepo(did, name string) (*Repo, error) { return &repo, nil } -func (d *DB) AddRepo(repo *Repo) error { - _, err := d.db.Exec(`insert into repos (did, name, knot, rkey, at_uri) values (?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri) +func AddRepo(e Execer, repo *Repo) error { + _, err := e.Exec(`insert into repos (did, name, knot, rkey, at_uri) values (?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri) return err } -func (d *DB) RemoveRepo(did, name, rkey string) error { - _, err := d.db.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey) +func RemoveRepo(e Execer, did, name, rkey string) error { + _, err := e.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey) return err } -func (d *DB) AddCollaborator(collaborator, repoOwnerDid, repoName, repoKnot string) error { - _, err := d.db.Exec( +func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error { + _, err := e.Exec( `insert into collaborators (did, repo) values (?, (select id from repos where did = ? and name = ? and knot = ?));`, collaborator, repoOwnerDid, repoName, repoKnot) return err } -func (d *DB) CollaboratingIn(collaborator string) ([]Repo, error) { +func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) { var repos []Repo - rows, err := d.db.Query(`select r.did, r.name, r.knot, r.rkey, r.created from repos r join collaborators c on r.id = c.repo where c.did = ?;`, collaborator) + rows, err := e.Query(`select r.did, r.name, r.knot, r.rkey, r.created from repos r join collaborators c on r.id = c.repo where c.did = ?;`, collaborator) if err != nil { return nil, err } diff --git a/appview/db/timeline.go b/appview/db/timeline.go index e06322ff..d857b409 100644 --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -11,15 +11,15 @@ type TimelineEvent struct { EventAt time.Time } -func (d *DB) MakeTimeline() ([]TimelineEvent, error) { +func MakeTimeline(e Execer) ([]TimelineEvent, error) { var events []TimelineEvent - repos, err := d.GetAllRepos() + repos, err := GetAllRepos(e) if err != nil { return nil, err } - follows, err := d.GetAllFollows() + follows, err := GetAllFollows(e) if err != nil { return nil, err } diff --git a/appview/pages/templates/repo/new.html b/appview/pages/templates/repo/new.html index e71ee250..045043ac 100644 --- a/appview/pages/templates/repo/new.html +++ b/appview/pages/templates/repo/new.html @@ -22,10 +22,10 @@ type="text" id="branch" name="branch" + value="main" required class="w-full max-w-md" /> -

The default branch is main

diff --git a/appview/state/follow.go b/appview/state/follow.go index db67c3d8..3d25e44f 100644 --- a/appview/state/follow.go +++ b/appview/state/follow.go @@ -9,6 +9,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" tangled "github.com/sotangled/tangled/api/tangled" + "github.com/sotangled/tangled/appview/db" ) func (s *State) Follow(w http.ResponseWriter, r *http.Request) { @@ -51,7 +52,7 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { return } - err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey) + err = db.AddFollow(s.db, currentUser.Did, subjectIdent.DID.String(), rkey) if err != nil { log.Println("failed to follow", err) return @@ -73,7 +74,7 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { return case http.MethodDelete: // find the record in the db - follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String()) + follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String()) if err != nil { log.Println("failed to get follow relationship") return @@ -90,7 +91,7 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { return } - err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String()) + err = db.DeleteFollow(s.db, currentUser.Did, subjectIdent.DID.String()) if err != nil { log.Println("failed to delete follow from DB") // this is not an issue, the firehose event might have already done this diff --git a/appview/state/jetstream.go b/appview/state/jetstream.go index f7786916..65035a5b 100644 --- a/appview/state/jetstream.go +++ b/appview/state/jetstream.go @@ -13,13 +13,13 @@ import ( type Ingester func(ctx context.Context, e *models.Event) error -func jetstreamIngester(db *db.DB) Ingester { +func jetstreamIngester(d db.DbWrapper) Ingester { return func(ctx context.Context, e *models.Event) error { var err error defer func() { eventTime := e.TimeUS lastTimeUs := eventTime + 1 - if err := db.UpdateLastTimeUs(lastTimeUs); err != nil { + if err := d.UpdateLastTimeUs(lastTimeUs); err != nil { err = fmt.Errorf("(deferred) failed to save last time us: %w", err) } }() @@ -39,7 +39,7 @@ func jetstreamIngester(db *db.DB) Ingester { log.Println("invalid record") return err } - err = db.AddFollow(did, record.Subject, e.Commit.RKey) + err = db.AddFollow(d, did, record.Subject, e.Commit.RKey) if err != nil { return fmt.Errorf("failed to add follow to db: %w", err) } diff --git a/appview/state/middleware.go b/appview/state/middleware.go index 61187ffa..242d4b9a 100644 --- a/appview/state/middleware.go +++ b/appview/state/middleware.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/sotangled/tangled/appview" "github.com/sotangled/tangled/appview/auth" + "github.com/sotangled/tangled/appview/db" ) type Middleware func(http.Handler) http.Handler @@ -176,7 +177,7 @@ func ResolveRepoKnot(s *State) Middleware { return } - repo, err := s.db.GetRepo(id.DID.String(), repoName) + repo, err := db.GetRepo(s.db, id.DID.String(), repoName) if err != nil { // invalid did or handle log.Println("failed to resolve repo") diff --git a/appview/state/repo.go b/appview/state/repo.go index e01c44ef..c0a79800 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -389,7 +389,7 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { // TODO: create an atproto record for this - secret, err := s.db.GetRegistrationKey(f.Knot) + secret, err := db.GetRegistrationKey(s.db, f.Knot) if err != nil { log.Printf("no key found for domain %s: %s\n", f.Knot, err) return @@ -412,18 +412,46 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { return } + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + log.Println("failed to start tx") + w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) + return + } + defer func() { + tx.Rollback() + err = s.enforcer.E.LoadPolicy() + if err != nil { + log.Println("failed to rollback policies") + } + }() + err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo()) if err != nil { w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) return } - err = s.db.AddCollaborator(collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) + err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) if err != nil { w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) return } + err = tx.Commit() + if err != nil { + log.Println("failed to commit changes", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = s.enforcer.E.SavePolicy() + if err != nil { + log.Println("failed to update ACLs", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) } @@ -546,7 +574,7 @@ func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { return } - issue, comments, err := s.db.GetIssueWithComments(f.RepoAt, issueIdInt) + issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to get issue and comments", err) s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") @@ -605,7 +633,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { return } - issue, err := s.db.GetIssue(f.RepoAt, issueIdInt) + issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to get issue", err) s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -645,7 +673,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { return } - err := s.db.CloseIssue(f.RepoAt, issueIdInt) + err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to close issue", err) s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -678,7 +706,7 @@ func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { } if user.Did == f.OwnerDid() { - err := s.db.ReopenIssue(f.RepoAt, issueIdInt) + err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to reopen issue", err) s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") @@ -719,7 +747,7 @@ func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { commentId := rand.IntN(1000000) - err := s.db.NewComment(&db.Comment{ + err := db.NewComment(s.db, &db.Comment{ OwnerDid: user.Did, RepoAt: f.RepoAt, Issue: issueIdInt, @@ -735,7 +763,7 @@ func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { createdAt := time.Now().Format(time.RFC3339) commentIdInt64 := int64(commentId) ownerDid := user.Did - issueAt, err := s.db.GetIssueAt(f.RepoAt, issueIdInt) + issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to get issue at", err) s.pages.Notice(w, "issue-comment", "Failed to create comment.") @@ -777,7 +805,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { return } - issues, err := s.db.GetIssues(f.RepoAt) + issues, err := db.GetIssues(s.db, f.RepoAt) if err != nil { log.Println("failed to get issues", err) s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") @@ -841,7 +869,13 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } - err = s.db.NewIssue(&db.Issue{ + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + s.pages.Notice(w, "issues", "Failed to create issue, try again later") + return + } + + err = db.NewIssue(tx, &db.Issue{ RepoAt: f.RepoAt, Title: title, Body: body, @@ -853,7 +887,7 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } - issueId, err := s.db.GetIssueId(f.RepoAt) + issueId, err := db.GetIssueId(s.db, f.RepoAt) if err != nil { log.Println("failed to get issue id", err) s.pages.Notice(w, "issues", "Failed to create issue.") @@ -881,7 +915,7 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } - err = s.db.SetIssueAt(f.RepoAt, issueId, resp.Uri) + err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) if err != nil { log.Println("failed to set issue at", err) s.pages.Notice(w, "issues", "Failed to create issue.") diff --git a/appview/state/settings.go b/appview/state/settings.go index 09a99243..a05d5865 100644 --- a/appview/state/settings.go +++ b/appview/state/settings.go @@ -10,13 +10,14 @@ import ( lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/gliderlabs/ssh" "github.com/sotangled/tangled/api/tangled" + "github.com/sotangled/tangled/appview/db" "github.com/sotangled/tangled/appview/pages" ) func (s *State) Settings(w http.ResponseWriter, r *http.Request) { // for now, this is just pubkeys user := s.auth.GetUser(r) - pubKeys, err := s.db.GetPublicKeys(user.Did) + pubKeys, err := db.GetPublicKeys(s.db, user.Did) if err != nil { log.Println(err) } @@ -47,7 +48,7 @@ func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { return } - if err := s.db.AddPublicKey(did, name, key); err != nil { + if err := db.AddPublicKey(s.db, did, name, key); err != nil { log.Printf("adding public key: %s", err) s.pages.Notice(w, "settings-keys", "Failed to add public key.") return diff --git a/appview/state/state.go b/appview/state/state.go index fe2f05dc..b935e1f6 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -39,7 +39,7 @@ type State struct { } func Make(config *appview.Config) (*State, error) { - db, err := db.Make(config.DbPath) + d, err := db.Make(config.DbPath) if err != nil { return nil, err } @@ -60,17 +60,18 @@ func Make(config *appview.Config) (*State, error) { resolver := appview.NewResolver() - jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false) + wrapper := db.DbWrapper{d} + jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), wrapper, false) if err != nil { return nil, fmt.Errorf("failed to create jetstream client: %w", err) } - err = jc.StartJetstream(context.Background(), jetstreamIngester(db)) + err = jc.StartJetstream(context.Background(), jetstreamIngester(wrapper)) if err != nil { return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) } state := &State{ - db, + d, auth, enforcer, clock, @@ -135,7 +136,7 @@ func (s *State) Logout(w http.ResponseWriter, r *http.Request) { func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { user := s.auth.GetUser(r) - timeline, err := s.db.MakeTimeline() + timeline, err := db.MakeTimeline(s.db) if err != nil { log.Println(err) s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") @@ -195,7 +196,7 @@ func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { return } - key, err := s.db.GenerateRegistrationKey(domain, did) + key, err := db.GenerateRegistrationKey(s.db, domain, did) if err != nil { log.Println(err) @@ -222,7 +223,7 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { return } - pubKeys, err := s.db.GetPublicKeys(id.DID.String()) + pubKeys, err := db.GetPublicKeys(s.db, id.DID.String()) if err != nil { w.WriteHeader(http.StatusNotFound) return @@ -250,7 +251,7 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { } log.Println("checking ", domain) - secret, err := s.db.GetRegistrationKey(domain) + secret, err := db.GetRegistrationKey(s.db, domain) if err != nil { log.Printf("no key found for domain %s: %s\n", domain, err) return @@ -295,8 +296,22 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { return } + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + log.Println("failed to start tx", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer func() { + tx.Rollback() + err = s.enforcer.E.LoadPolicy() + if err != nil { + log.Println("failed to rollback policies") + } + }() + // mark as registered - err = s.db.Register(domain) + err = db.Register(tx, domain) if err != nil { log.Println("failed to register domain", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -304,7 +319,7 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { } // set permissions for this did as owner - reg, err := s.db.RegistrationByDomain(domain) + reg, err := db.RegistrationByDomain(tx, domain) if err != nil { log.Println("failed to register domain", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -327,6 +342,20 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { return } + err = tx.Commit() + if err != nil { + log.Println("failed to commit changes", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = s.enforcer.E.SavePolicy() + if err != nil { + log.Println("failed to update ACLs", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write([]byte("check success")) } @@ -338,7 +367,7 @@ func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { } user := s.auth.GetUser(r) - reg, err := s.db.RegistrationByDomain(domain) + reg, err := db.RegistrationByDomain(s.db, domain) if err != nil { w.Write([]byte("failed to pull up registration info")) return @@ -370,7 +399,7 @@ func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { func (s *State) Knots(w http.ResponseWriter, r *http.Request) { // for now, this is just pubkeys user := s.auth.GetUser(r) - registrations, err := s.db.RegistrationsByDid(user.Did) + registrations, err := db.RegistrationsByDid(s.db, user.Did) if err != nil { log.Println(err) } @@ -444,7 +473,7 @@ func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { } log.Println("created atproto record: ", resp.Uri) - secret, err := s.db.GetRegistrationKey(domain) + secret, err := db.GetRegistrationKey(s.db, domain) if err != nil { log.Printf("no key found for domain %s: %s\n", domain, err) return @@ -520,34 +549,24 @@ func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { return } - secret, err := s.db.GetRegistrationKey(domain) - if err != nil { - s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) + existingRepo, err := db.GetRepo(s.db, user.Did, repoName) + if err == nil && existingRepo != nil { + s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) return } - client, err := NewSignedClient(domain, secret, s.config.Dev) + secret, err := db.GetRegistrationKey(s.db, domain) if err != nil { - s.pages.Notice(w, "repo", "Failed to connect to knot server.") + s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) return } - resp, err := client.NewRepo(user.Did, repoName, defaultBranch) + client, err := NewSignedClient(domain, secret, s.config.Dev) if err != nil { - s.pages.Notice(w, "repo", "Failed to create repository on knot server.") + s.pages.Notice(w, "repo", "Failed to connect to knot server.") return } - switch resp.StatusCode { - case http.StatusConflict: - s.pages.Notice(w, "repo", "A repository with that name already exists.") - return - case http.StatusInternalServerError: - s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") - case http.StatusNoContent: - // continue - } - rkey := s.TID() repo := &db.Repo{ Did: user.Did, @@ -578,9 +597,38 @@ func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { } log.Println("created repo record: ", atresp.Uri) - repo.AtUri = atresp.Uri + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + log.Println(err) + s.pages.Notice(w, "repo", "Failed to save repository information.") + return + } + defer func() { + tx.Rollback() + err = s.enforcer.E.LoadPolicy() + if err != nil { + log.Println("failed to rollback policies") + } + }() - err = s.db.AddRepo(repo) + resp, err := client.NewRepo(user.Did, repoName, defaultBranch) + if err != nil { + s.pages.Notice(w, "repo", "Failed to create repository on knot server.") + return + } + + switch resp.StatusCode { + case http.StatusConflict: + s.pages.Notice(w, "repo", "A repository with that name already exists.") + return + case http.StatusInternalServerError: + s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") + case http.StatusNoContent: + // continue + } + + repo.AtUri = atresp.Uri + err = db.AddRepo(tx, repo) if err != nil { log.Println(err) s.pages.Notice(w, "repo", "Failed to save repository information.") @@ -596,6 +644,20 @@ func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { return } + err = tx.Commit() + if err != nil { + log.Println("failed to commit changes", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = s.enforcer.E.SavePolicy() + if err != nil { + log.Println("failed to update ACLs", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) return } @@ -615,12 +677,12 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { return } - repos, err := s.db.GetAllReposByDid(ident.DID.String()) + repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) if err != nil { log.Printf("getting repos for %s: %s", ident.DID.String(), err) } - collaboratingRepos, err := s.db.CollaboratingIn(ident.DID.String()) + collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) if err != nil { log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) } @@ -638,7 +700,7 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { } } - followers, following, err := s.db.GetFollowerFollowing(ident.DID.String()) + followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) if err != nil { log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) } @@ -646,7 +708,7 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { loggedInUser := s.auth.GetUser(r) followStatus := db.IsNotFollowing if loggedInUser != nil { - followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String()) + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) } profileAvatarUri, err := GetAvatarUri(ident.DID.String()) @@ -818,6 +880,7 @@ 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.Get("/", s.AddRepo) r.Post("/", s.AddRepo) }) diff --git a/rbac/rbac.go b/rbac/rbac.go index 54a0f85e..7d595647 100644 --- a/rbac/rbac.go +++ b/rbac/rbac.go @@ -6,7 +6,7 @@ import ( "path" "strings" - sqladapter "github.com/Blank-Xu/sql-adapter" + adapter "github.com/Blank-Xu/sql-adapter" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" ) @@ -31,7 +31,7 @@ m = r.act == p.act && r.dom == p.dom && keyMatch2(r.obj, p.obj) && g(r.sub, p.su ) type Enforcer struct { - E *casbin.SyncedEnforcer + E *casbin.Enforcer } func keyMatch2(key1 string, key2 string) bool { @@ -50,17 +50,18 @@ func NewEnforcer(path string) (*Enforcer, error) { return nil, err } - a, err := sqladapter.NewAdapter(db, "sqlite3", "acl") + a, err := adapter.NewAdapter(db, "sqlite3", "acl") if err != nil { return nil, err } - e, err := casbin.NewSyncedEnforcer(m, a) + e, err := casbin.NewEnforcer(m, a) if err != nil { return nil, err } - e.EnableAutoSave(true) + e.EnableAutoSave(false) + e.AddFunction("keyMatch2", keyMatch2Func) return &Enforcer{e}, nil @@ -82,7 +83,7 @@ func (e *Enforcer) AddDomain(domain string) error { } func (e *Enforcer) GetDomainsForUser(did string) ([]string, error) { - return e.E.Enforcer.GetDomainsForUser(did) + return e.E.GetDomainsForUser(did) } func (e *Enforcer) AddOwner(domain, owner string) error { @@ -131,7 +132,7 @@ func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) { // this includes roles too, casbin does not differentiate. // the filtering criteria is to remove strings not starting with `did:` - members, err := e.E.Enforcer.GetImplicitUsersForRole(role, domain) + members, err := e.E.GetImplicitUsersForRole(role, domain) for _, m := range members { if strings.HasPrefix(m, "did:") { membersWithoutRoles = append(membersWithoutRoles, m) -- 2.54.0