Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c4572d27f | |||
| 858afe5371 | |||
| 11d2d80d77 | |||
| e1c056ebc4 | |||
| 3a2d962472 |
@@ -9,6 +9,7 @@ import "sort"
|
|||||||
import "strconv"
|
import "strconv"
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
import storedb "codit/internal/db"
|
||||||
import _ "modernc.org/sqlite"
|
import _ "modernc.org/sqlite"
|
||||||
import "github.com/gdamore/tcell/v2"
|
import "github.com/gdamore/tcell/v2"
|
||||||
import "github.com/rivo/tview"
|
import "github.com/rivo/tview"
|
||||||
@@ -72,12 +73,15 @@ func main() {
|
|||||||
|
|
||||||
func newBrowser(dbPath string, dataDir string) (*browser, error) {
|
func newBrowser(dbPath string, dataDir string) (*browser, error) {
|
||||||
var err error
|
var err error
|
||||||
|
var store *storedb.Store
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
var br *browser
|
var br *browser
|
||||||
db, err = sql.Open("sqlite", "file:"+dbPath+"?_pragma=foreign_keys(1)")
|
|
||||||
|
store, err = storedb.Open("sqlite", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
db = store.DB
|
||||||
br = &browser{
|
br = &browser{
|
||||||
DB: db,
|
DB: db,
|
||||||
DataDir: filepath.Clean(dataDir),
|
DataDir: filepath.Clean(dataDir),
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ func (s *Store) CountAuthProviderUsers(id string) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var err error
|
var err error
|
||||||
var owned bool
|
var owned bool
|
||||||
var now int64
|
var now int64
|
||||||
@@ -282,9 +282,7 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
|||||||
|
|
||||||
if strings.TrimSpace(item.ID) == "" {
|
if strings.TrimSpace(item.ID) == "" {
|
||||||
item.ID, err = util.NewID()
|
item.ID, err = util.NewID()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
|
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
|
||||||
item.LDAPUserFilter = "(uid={username})"
|
item.LDAPUserFilter = "(uid={username})"
|
||||||
@@ -293,14 +291,10 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
|||||||
item.OIDCScopes = "openid profile email"
|
item.OIDCScopes = "openid profile email"
|
||||||
}
|
}
|
||||||
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
|
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(`INSERT INTO auth_providers (
|
_, err = tx.Exec(`INSERT INTO auth_providers (
|
||||||
public_id, name, type, enabled,
|
public_id, name, type, enabled,
|
||||||
@@ -338,7 +332,7 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var err error
|
var err error
|
||||||
var owned bool
|
var owned bool
|
||||||
var now int64
|
var now int64
|
||||||
@@ -358,9 +352,7 @@ func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(`UPDATE auth_providers SET
|
_, err = tx.Exec(`UPDATE auth_providers SET
|
||||||
name = ?, enabled = ?,
|
name = ?, enabled = ?,
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ func (s *Store) ReorderBlockChecklistItems(boardID, blockID string, ids []string
|
|||||||
var internalBlockID int64
|
var internalBlockID int64
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var i int
|
var i int
|
||||||
var id string
|
var id string
|
||||||
@@ -704,7 +704,7 @@ func (s *Store) listBlockAssigneesForBoard(boardID string) (map[string][]models.
|
|||||||
func (s *Store) UpsertBlockProperties(boardID, blockID string, props models.BlockProperties) (models.BlockProperties, error) {
|
func (s *Store) UpsertBlockProperties(boardID, blockID string, props models.BlockProperties) (models.BlockProperties, error) {
|
||||||
var internalBlockID int64
|
var internalBlockID int64
|
||||||
var err error
|
var err error
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var assigneeIDs []string
|
var assigneeIDs []string
|
||||||
var i int
|
var i int
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (s *Store) CreateBoard(board models.Board) (models.Board, error) {
|
|||||||
var id string
|
var id string
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
|
|
||||||
if board.ID == "" {
|
if board.ID == "" {
|
||||||
@@ -322,7 +322,7 @@ func scanBlock(row interface{ Scan(...any) error }, bl *models.Block) error {
|
|||||||
// resolveParentBlockID returns the internal row ID for a parent block constrained
|
// resolveParentBlockID returns the internal row ID for a parent block constrained
|
||||||
// to the same board. Returns 0 when parentPublicID is "". Returns sql.ErrNoRows
|
// to the same board. Returns 0 when parentPublicID is "". Returns sql.ErrNoRows
|
||||||
// when the block doesn't exist, is deleted, or belongs to a different board.
|
// when the block doesn't exist, is deleted, or belongs to a different board.
|
||||||
func resolveParentBlockID(tx *sql.Tx, boardPublicID, parentPublicID string) (int64, error) {
|
func resolveParentBlockID(tx txExecutor, boardPublicID, parentPublicID string) (int64, error) {
|
||||||
var parentID int64
|
var parentID int64
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
if parentPublicID == "" {
|
if parentPublicID == "" {
|
||||||
@@ -338,7 +338,7 @@ func resolveParentBlockID(tx *sql.Tx, boardPublicID, parentPublicID string) (int
|
|||||||
|
|
||||||
// hasCycleAfterReparent returns true when assigning newParentID as the parent
|
// hasCycleAfterReparent returns true when assigning newParentID as the parent
|
||||||
// of blockID would create a cycle (including the self-parent case).
|
// of blockID would create a cycle (including the self-parent case).
|
||||||
func hasCycleAfterReparent(tx *sql.Tx, blockID, newParentID int64) (bool, error) {
|
func hasCycleAfterReparent(tx txExecutor, blockID, newParentID int64) (bool, error) {
|
||||||
var count int
|
var count int
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
if newParentID == 0 {
|
if newParentID == 0 {
|
||||||
@@ -364,7 +364,7 @@ func hasCycleAfterReparent(tx *sql.Tx, blockID, newParentID int64) (bool, error)
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) insertBlockHistory(tx *sql.Tx, blockID int64, insertAt int64) error {
|
func (s *Store) insertBlockHistory(tx txExecutor, blockID int64, insertAt int64) error {
|
||||||
var err error
|
var err error
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
INSERT INTO blocks_history
|
INSERT INTO blocks_history
|
||||||
@@ -381,7 +381,7 @@ func (s *Store) CreateBlock(block models.Block) (models.Block, error) {
|
|||||||
var id string
|
var id string
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var result sql.Result
|
var result sql.Result
|
||||||
var blockID int64
|
var blockID int64
|
||||||
@@ -515,7 +515,7 @@ func (s *Store) ListBlocksByType(boardID string, blockType string) ([]models.Blo
|
|||||||
func (s *Store) UpdateBlock(block models.Block) (models.Block, error) {
|
func (s *Store) UpdateBlock(block models.Block) (models.Block, error) {
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
var blockID int64
|
var blockID int64
|
||||||
@@ -594,7 +594,7 @@ func (s *Store) UpdateBlock(block models.Block) (models.Block, error) {
|
|||||||
func (s *Store) PatchBlocks(boardID string, patches []models.BlockPatch, updatedBy string) error {
|
func (s *Store) PatchBlocks(boardID string, patches []models.BlockPatch, updatedBy string) error {
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var i int
|
var i int
|
||||||
var p models.BlockPatch
|
var p models.BlockPatch
|
||||||
@@ -704,7 +704,7 @@ func (s *Store) PatchBlocks(boardID string, patches []models.BlockPatch, updated
|
|||||||
func (s *Store) DeleteBlock(boardID string, id string, updatedBy string) error {
|
func (s *Store) DeleteBlock(boardID string, id string, updatedBy string) error {
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var blockID int64
|
var blockID int64
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
|
|||||||
+115
-49
@@ -9,51 +9,102 @@ import "path/filepath"
|
|||||||
import "sort"
|
import "sort"
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
type Store struct {
|
|
||||||
DB *sql.DB
|
|
||||||
tx *sql.Tx
|
|
||||||
}
|
|
||||||
|
|
||||||
type sqlExecutor interface {
|
type sqlExecutor interface {
|
||||||
Exec(query string, args ...any) (sql.Result, error)
|
Exec(query string, args ...any) (sql.Result, error)
|
||||||
Query(query string, args ...any) (*sql.Rows, error)
|
Query(query string, args ...any) (*sql.Rows, error)
|
||||||
QueryRow(query string, args ...any) *sql.Row
|
QueryRow(query string, args ...any) *sql.Row
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(driver, dsn string) (*Store, error) {
|
type txExecutor interface {
|
||||||
|
sqlExecutor
|
||||||
|
Commit() error
|
||||||
|
Rollback() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
DB *sql.DB
|
||||||
|
tx txExecutor
|
||||||
|
imm bool // whether BEGIN IMMEDIATE is supported. For sqlite.
|
||||||
|
}
|
||||||
|
|
||||||
|
type manualTx struct {
|
||||||
|
conn *sql.Conn
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
func (tx *manualTx) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
|
return tx.conn.ExecContext(tx.ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *manualTx) Query(query string, args ...any) (*sql.Rows, error) {
|
||||||
|
return tx.conn.QueryContext(tx.ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *manualTx) QueryRow(query string, args ...any) *sql.Row {
|
||||||
|
return tx.conn.QueryRowContext(tx.ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *manualTx) Commit() error {
|
||||||
|
var err error
|
||||||
|
if tx.conn != nil {
|
||||||
|
// pass context.Background() to make COMMIT resist connection teardown/cancellation
|
||||||
|
//_, err = tx.conn.ExecContext(tx.ctx, "COMMIT")
|
||||||
|
_, err = tx.conn.ExecContext(context.Background(), "COMMIT")
|
||||||
|
_ = tx.conn.Close()
|
||||||
|
tx.conn = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *manualTx) Rollback() error {
|
||||||
|
var err error
|
||||||
|
if tx.conn != nil {
|
||||||
|
// pass context.Background() to make ROLLBACK resist connection teardown/cancellation
|
||||||
|
//_, err = tx.conn.ExecContext(tx.ctx, "ROLLBACK")
|
||||||
|
_, err = tx.conn.ExecContext(context.Background(), "ROLLBACK")
|
||||||
|
_ = tx.conn.Close()
|
||||||
|
tx.conn = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
func Open(driver string, dsn string) (*Store, error) {
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
var drv string
|
var drv string
|
||||||
|
var imm bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
drv = driverName(driver)
|
drv = driverName(driver)
|
||||||
if drv == "sqlite" {
|
if drv == "sqlite" {
|
||||||
dsn = appendSQLitePragmas(dsn, map[string]string{
|
dsn = appendSQLitePragmas(dsn, map[string]string{
|
||||||
"busy_timeout": "10000",
|
"busy_timeout": "10000", // make this configurable too
|
||||||
"journal_mode": "WAL",
|
"journal_mode": "WAL",
|
||||||
"foreign_keys": "1",
|
"foreign_keys": "1",
|
||||||
})
|
})
|
||||||
|
imm = true
|
||||||
}
|
}
|
||||||
db, err = sql.Open(drv, dsn)
|
db, err = sql.Open(drv, dsn)
|
||||||
if err != nil {
|
if err != nil { goto oops }
|
||||||
goto oops
|
|
||||||
}
|
|
||||||
|
|
||||||
//if drv == "sqlite" {
|
if drv == "sqlite" {
|
||||||
// db.SetMaxOpenConns(10)
|
db.SetMaxOpenConns(64) // TODO: make this configurable
|
||||||
// db.SetMaxIdleConns(1)
|
// db.SetMaxIdleConns(8)
|
||||||
//}
|
// db.SetConnMaxIdleTime(1 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil { goto oops }
|
||||||
goto oops
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Store{DB: db}, nil
|
// this is usually a base store without a transaction started.
|
||||||
|
// usually to be used as a base connection
|
||||||
|
return &Store{DB: db, tx: nil, imm: imm}, nil
|
||||||
|
|
||||||
oops:
|
oops:
|
||||||
if db != nil {
|
if db != nil { db.Close() }
|
||||||
db.Close()
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,57 +200,72 @@ func (s *Store) BeginStore(ctx context.Context) (*Store, error) {
|
|||||||
var tx *sql.Tx
|
var tx *sql.Tx
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if s == nil {
|
if s == nil { return nil, fmt.Errorf("nil store") }
|
||||||
return nil, fmt.Errorf("nil store")
|
if s.tx != nil { return s, nil } // more than one BeginStore calls, return the existing tx
|
||||||
}
|
|
||||||
if s.tx != nil {
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
tx, err = s.DB.BeginTx(ctx, nil)
|
tx, err = s.DB.BeginTx(ctx, nil)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
// return a new store off the current base db store
|
||||||
|
return &Store{DB: s.DB, tx: tx, imm: s.imm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) BeginImmediateStore(ctx context.Context) (*Store, error) {
|
||||||
|
var conn *sql.Conn
|
||||||
|
var tx *manualTx
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if s.tx != nil { return s, nil }
|
||||||
|
if !s.imm { return s.BeginStore(ctx) }
|
||||||
|
|
||||||
|
conn, err = s.DB.Conn(ctx)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
_, err = conn.ExecContext(ctx, "BEGIN IMMEDIATE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Store{DB: s.DB, tx: tx}, nil
|
|
||||||
|
tx = &manualTx{
|
||||||
|
conn: conn,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
return &Store{DB: s.DB, tx: tx, imm: s.imm}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Commit() error {
|
func (s *Store) Commit() error {
|
||||||
if s == nil || s.tx == nil {
|
if s == nil || s.tx == nil { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.tx.Commit()
|
return s.tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Rollback() error {
|
func (s *Store) Rollback() error {
|
||||||
if s == nil || s.tx == nil {
|
if s == nil || s.tx == nil { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.tx.Rollback()
|
return s.tx.Rollback()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) begin() (*sql.Tx, bool, error) {
|
func (s *Store) begin() (txExecutor, bool, error) {
|
||||||
var tx *sql.Tx
|
var tx *sql.Tx
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if s != nil && s.tx != nil {
|
// return an existing transaction object that's already started by
|
||||||
return s.tx, false, nil
|
// BeginStore() and BeginImmediateStore().
|
||||||
}
|
// the second return value is false because the transaction is not started here.
|
||||||
|
if s != nil && s.tx != nil { return s.tx, false, nil }
|
||||||
|
|
||||||
tx, err = s.DB.Begin()
|
tx, err = s.DB.Begin()
|
||||||
if err != nil {
|
if err != nil { return nil, false, err }
|
||||||
return nil, false, err
|
|
||||||
}
|
// the second return value is true to indicate that the transaction has been started here
|
||||||
return tx, true, nil
|
return tx, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rollbackIfOwned(tx *sql.Tx, owned bool) {
|
func rollbackIfOwned(tx txExecutor, owned bool) {
|
||||||
if tx != nil && owned {
|
if tx != nil && owned { _ = tx.Rollback() }
|
||||||
_ = tx.Rollback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func commitIfOwned(tx *sql.Tx, owned bool) error {
|
func commitIfOwned(tx txExecutor, owned bool) error {
|
||||||
if !owned {
|
if !owned { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,21 +128,21 @@ func (s *Store) UpdateUserGroup(item models.UserGroup) (models.UserGroup, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteUserGroup(groupID string) error {
|
func (s *Store) DeleteUserGroup(groupID string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var group models.UserGroup
|
var group models.UserGroup
|
||||||
|
|
||||||
group, err = s.GetUserGroup(groupID)
|
group, err = s.GetUserGroup(groupID)
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
if NormalizeUserGroupScope(group.Scope) == UserGroupScopeAllUsers {
|
if NormalizeUserGroupScope(group.Scope) == UserGroupScopeAllUsers {
|
||||||
return errors.New("all users group cannot be deleted")
|
return errors.New("all users group cannot be deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'group' AND subject_id = (SELECT id FROM user_groups WHERE public_id = ?)`, strings.TrimSpace(groupID))
|
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'group' AND subject_id = (SELECT id FROM user_groups WHERE public_id = ?)`, strings.TrimSpace(groupID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
@@ -163,11 +163,8 @@ func (s *Store) DeleteUserGroup(groupID string) error {
|
|||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = commitIfOwned(tx, owned)
|
|
||||||
if err != nil {
|
return commitIfOwned(tx, owned)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListUserGroupMembers(groupID string) ([]models.UserGroupMember, error) {
|
func (s *Store) ListUserGroupMembers(groupID string) ([]models.UserGroupMember, error) {
|
||||||
@@ -215,30 +212,33 @@ func (s *Store) AddUserGroupMember(groupID string, userID string) error {
|
|||||||
var now int64
|
var now int64
|
||||||
var err error
|
var err error
|
||||||
var group models.UserGroup
|
var group models.UserGroup
|
||||||
|
|
||||||
group, err = s.GetUserGroup(groupID)
|
group, err = s.GetUserGroup(groupID)
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
||||||
return errors.New("virtual groups do not accept explicit members")
|
return errors.New("virtual groups do not accept explicit members")
|
||||||
}
|
}
|
||||||
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
_, err = s.Exec(`INSERT OR IGNORE INTO user_group_members (group_id, user_id, created_at)
|
_, err = s.Exec(`INSERT OR IGNORE INTO user_group_members (group_id, user_id, created_at)
|
||||||
VALUES ((SELECT id FROM user_groups WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?)`,
|
VALUES ((SELECT id FROM user_groups WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?)`,
|
||||||
strings.TrimSpace(groupID), strings.TrimSpace(userID), now)
|
strings.TrimSpace(groupID), strings.TrimSpace(userID), now)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) RemoveUserGroupMember(groupID string, userID string) error {
|
func (s *Store) RemoveUserGroupMember(groupID string, userID string) error {
|
||||||
var err error
|
var err error
|
||||||
var group models.UserGroup
|
var group models.UserGroup
|
||||||
|
|
||||||
group, err = s.GetUserGroup(groupID)
|
group, err = s.GetUserGroup(groupID)
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
||||||
return errors.New("virtual groups do not have explicit members")
|
return errors.New("virtual groups do not have explicit members")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.Exec(`DELETE FROM user_group_members
|
_, err = s.Exec(`DELETE FROM user_group_members
|
||||||
WHERE group_id = (SELECT id FROM user_groups WHERE public_id = ?)
|
WHERE group_id = (SELECT id FROM user_groups WHERE public_id = ?)
|
||||||
AND user_id = (SELECT id FROM users WHERE public_id = ?)`,
|
AND user_id = (SELECT id FROM users WHERE public_id = ?)`,
|
||||||
@@ -253,15 +253,15 @@ func (s *Store) ListProjectGroupRoles(projectID string) ([]models.ProjectGroupRo
|
|||||||
var err error
|
var err error
|
||||||
var items []models.ProjectGroupRole
|
var items []models.ProjectGroupRole
|
||||||
var item models.ProjectGroupRole
|
var item models.ProjectGroupRole
|
||||||
|
|
||||||
rows, err = s.Query(`SELECT p.public_id, g.public_id, b.role, b.created_at
|
rows, err = s.Query(`SELECT p.public_id, g.public_id, b.role, b.created_at
|
||||||
FROM project_role_bindings b
|
FROM project_role_bindings b
|
||||||
JOIN projects p ON p.id = b.project_id
|
JOIN projects p ON p.id = b.project_id
|
||||||
JOIN user_groups g ON g.id = b.subject_id
|
JOIN user_groups g ON g.id = b.subject_id
|
||||||
WHERE p.public_id = ? AND b.subject_type = 'group'
|
WHERE p.public_id = ? AND b.subject_type = 'group'
|
||||||
ORDER BY g.name`, strings.TrimSpace(projectID))
|
ORDER BY g.name`, strings.TrimSpace(projectID))
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&item.ProjectID, &item.GroupID, &item.Role, &item.CreatedAt)
|
err = rows.Scan(&item.ProjectID, &item.GroupID, &item.Role, &item.CreatedAt)
|
||||||
@@ -270,10 +270,10 @@ func (s *Store) ListProjectGroupRoles(projectID string) ([]models.ProjectGroupRo
|
|||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +283,7 @@ func (s *Store) UpsertProjectGroupRole(projectID string, groupID string, role st
|
|||||||
var result sql.Result
|
var result sql.Result
|
||||||
var rowsAffected int64
|
var rowsAffected int64
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
result, err = s.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
|
||||||
SELECT (SELECT id FROM projects WHERE public_id = ?), 'group', g.id, ?, ?, ?
|
SELECT (SELECT id FROM projects WHERE public_id = ?), 'group', g.id, ?, ?, ?
|
||||||
@@ -294,12 +295,10 @@ func (s *Store) UpsertProjectGroupRole(projectID string, groupID string, role st
|
|||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
rowsAffected, err = result.RowsAffected()
|
rowsAffected, err = result.RowsAffected()
|
||||||
if err != nil {
|
if err != nil { return item, err }
|
||||||
return item, err
|
|
||||||
}
|
if rowsAffected <= 0 { return item, errors.New("group not found") }
|
||||||
if rowsAffected <= 0 {
|
|
||||||
return item, errors.New("group not found")
|
|
||||||
}
|
|
||||||
item.ProjectID = strings.TrimSpace(projectID)
|
item.ProjectID = strings.TrimSpace(projectID)
|
||||||
item.GroupID = strings.TrimSpace(groupID)
|
item.GroupID = strings.TrimSpace(groupID)
|
||||||
item.Role = strings.TrimSpace(role)
|
item.Role = strings.TrimSpace(role)
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ func (s *Store) GetActivePKIClientProfileForUser(profileID string, userID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
|
func (s *Store) CreatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -413,7 +413,7 @@ func (s *Store) CreatePKIClientProfile(item models.PKIClientProfile) (models.PKI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
|
func (s *Store) UpdatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -684,7 +684,7 @@ func (s *Store) listPKIClientProfileTargets(profileID string) ([]models.PKIClien
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertPKIClientProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error {
|
func insertPKIClientProfileTargetTx(tx txExecutor, profileID string, targetType string, targetID string, createdAt int64) error {
|
||||||
var err error
|
var err error
|
||||||
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
||||||
targetID = strings.TrimSpace(targetID)
|
targetID = strings.TrimSpace(targetID)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (s *Store) DeletePKICA(id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeletePKICASubtree(id string) error {
|
func (s *Store) DeletePKICASubtree(id string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
var err error
|
var err error
|
||||||
@@ -205,7 +205,7 @@ func (s *Store) DeletePKICASubtree(id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) NextPKICASerial(caID string) (int64, error) {
|
func (s *Store) NextPKICASerial(caID string) (int64, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (s *Store) SetPrincipalAPIKeyDisabled(principalID string, id string, disabl
|
|||||||
|
|
||||||
func (s *Store) GetPrincipalByAPIKeyHash(tokenHash string) (models.ServicePrincipal, error) {
|
func (s *Store) GetPrincipalByAPIKeyHash(tokenHash string) (models.ServicePrincipal, error) {
|
||||||
var principal models.ServicePrincipal
|
var principal models.ServicePrincipal
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
var keyID int64
|
var keyID int64
|
||||||
@@ -86,9 +86,8 @@ func (s *Store) GetPrincipalByAPIKeyHash(tokenHash string) (models.ServicePrinci
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return principal, err }
|
||||||
return principal, err
|
|
||||||
}
|
|
||||||
now = time.Now().UTC()
|
now = time.Now().UTC()
|
||||||
currentUnix = now.Unix()
|
currentUnix = now.Unix()
|
||||||
row = tx.QueryRow(`
|
row = tx.QueryRow(`
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ func (s *Store) UpdateServicePrincipal(item models.ServicePrincipal) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteServicePrincipal(id string) error {
|
func (s *Store) DeleteServicePrincipal(id string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'principal' AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`, id)
|
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'principal' AND subject_id = (SELECT id FROM service_principals WHERE public_id = ?)`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
@@ -95,8 +95,8 @@ func (s *Store) DeleteServicePrincipal(id string) error {
|
|||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = commitIfOwned(tx, owned)
|
|
||||||
return err
|
return commitIfOwned(tx, owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, error) {
|
func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, error) {
|
||||||
@@ -119,10 +119,9 @@ func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, erro
|
|||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil { return nil, err }
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (s *Store) TryStartRPMMirrorTask(repoID string, path string, now int64) (bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) TryStartRPMMirrorRun(repoID string, path string, startedAt int64) (string, bool, error) {
|
func (s *Store) TryStartRPMMirrorRun(repoID string, path string, startedAt int64) (string, bool, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var res sql.Result
|
var res sql.Result
|
||||||
var rows int64
|
var rows int64
|
||||||
@@ -192,7 +192,7 @@ func (s *Store) UpdateRPMMirrorTaskProgress(repoID string, path string, step str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) FinishRPMMirrorTask(repoID string, path string, success bool, revision string, errMsg string) error {
|
func (s *Store) FinishRPMMirrorTask(repoID string, path string, success bool, revision string, errMsg string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var now int64
|
var now int64
|
||||||
var status string
|
var status string
|
||||||
@@ -237,7 +237,7 @@ func (s *Store) FinishRPMMirrorTask(repoID string, path string, success bool, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) FinishRPMMirrorTaskRun(repoID string, path string, runID string, success bool, finishedAt int64, step string, total int64, done int64, failed int64, deleted int64, revision string, errMsg string) error {
|
func (s *Store) FinishRPMMirrorTaskRun(repoID string, path string, runID string, success bool, finishedAt int64, step string, total int64, done int64, failed int64, deleted int64, revision string, errMsg string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var interval int64
|
var interval int64
|
||||||
var nextSync int64
|
var nextSync int64
|
||||||
@@ -310,7 +310,7 @@ func (s *Store) SetRPMMirrorSyncEnabled(repoID string, path string, enabled bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ResetRunningRPMMirrorTasks() error {
|
func (s *Store) ResetRunningRPMMirrorTasks() error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var now int64
|
var now int64
|
||||||
var err error
|
var err error
|
||||||
@@ -541,16 +541,16 @@ func (s *Store) DeleteRPMRepoDirSubtree(repoID string, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) MoveRPMRepoDir(repoID string, oldPath string, newPath string) error {
|
func (s *Store) MoveRPMRepoDir(repoID string, oldPath string, newPath string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var now int64
|
var now int64
|
||||||
var oldPrefix string
|
var oldPrefix string
|
||||||
var newPrefix string
|
var newPrefix string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
oldPrefix = oldPath + "/"
|
oldPrefix = oldPath + "/"
|
||||||
newPrefix = newPath + "/"
|
newPrefix = newPath + "/"
|
||||||
@@ -569,9 +569,5 @@ func (s *Store) MoveRPMRepoDir(repoID string, oldPath string, newPath string) er
|
|||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = commitIfOwned(tx, owned)
|
return commitIfOwned(tx, owned)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) {
|
func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -777,7 +777,7 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) {
|
func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var grantIDsJSON string
|
var grantIDsJSON string
|
||||||
@@ -911,7 +911,7 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteSSHAccessProfile(id string) error {
|
func (s *Store) DeleteSSHAccessProfile(id string) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
var secretID sql.NullString
|
var secretID sql.NullString
|
||||||
@@ -2169,7 +2169,7 @@ func (s *Store) CountSSHFileTransfersFiltered(query string, status string, sessi
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) {
|
func createSSHSecretTx(tx txExecutor, item models.SSHSecret) (models.SSHSecret, error) {
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
|
|
||||||
@@ -2212,7 +2212,7 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
|||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) {
|
func updateSSHSecretTx(tx txExecutor, item models.SSHSecret) (models.SSHSecret, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
item.UpdatedAt = time.Now().UTC().Unix()
|
item.UpdatedAt = time.Now().UTC().Unix()
|
||||||
@@ -2235,7 +2235,7 @@ func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
|||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error {
|
func insertSSHAccessProfileTargetTx(tx txExecutor, profileID string, targetType string, targetID string, createdAt int64) error {
|
||||||
var err error
|
var err error
|
||||||
var id string
|
var id string
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (s *Store) GetSSHCredentialForUser(userID string, id string) (models.SSHCre
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSHSecret) (models.SSHCredential, error) {
|
func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSHSecret) (models.SSHCredential, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func (s *Store) GetSSHPrincipalGrant(id string) (models.SSHPrincipalGrant, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.SSHPrincipalGrant, error) {
|
func (s *Store) CreateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.SSHPrincipalGrant, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -336,7 +336,7 @@ func (s *Store) CreateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.SSHPrincipalGrant, error) {
|
func (s *Store) UpdateSSHPrincipalGrant(item models.SSHPrincipalGrant) (models.SSHPrincipalGrant, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -538,7 +538,7 @@ func (s *Store) listSSHPrincipalGrantTargets(grantID string) ([]models.SSHPrinci
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertSSHPrincipalGrantTargetTx(tx *sql.Tx, grantID string, targetType string, targetID string, createdAt int64) error {
|
func insertSSHPrincipalGrantTargetTx(tx txExecutor, grantID string, targetType string, targetID string, createdAt int64) error {
|
||||||
var err error
|
var err error
|
||||||
var result sql.Result
|
var result sql.Result
|
||||||
var rowsAffected int64
|
var rowsAffected int64
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (s *Store) ListSSHServersForGroup(groupID string) ([]models.SSHServer, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServerGroup, error) {
|
func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServerGroup, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
@@ -141,7 +141,7 @@ func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateSSHServerGroup(item models.SSHServerGroup) (models.SSHServerGroup, error) {
|
func (s *Store) UpdateSSHServerGroup(item models.SSHServerGroup) (models.SSHServerGroup, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (s *Store) GetSSHUserCAForUser(id string) (models.SSHUserCA, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) NextSSHUserCASerial(id string) (uint64, error) {
|
func (s *Store) NextSSHUserCASerial(id string) (uint64, error) {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var row *sql.Row
|
var row *sql.Row
|
||||||
var serial int64
|
var serial int64
|
||||||
|
|||||||
+16
-258
@@ -8,196 +8,6 @@ import "time"
|
|||||||
import "codit/internal/models"
|
import "codit/internal/models"
|
||||||
import "codit/internal/util"
|
import "codit/internal/util"
|
||||||
|
|
||||||
func (s *Store) CreateUser(user models.User, passwordHash string) (models.User, error) {
|
|
||||||
var id string
|
|
||||||
var err error
|
|
||||||
var nowUnix int64
|
|
||||||
if user.ID == "" {
|
|
||||||
id, err = util.NewID()
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
user.ID = id
|
|
||||||
}
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
user.CreatedAt = nowUnix
|
|
||||||
user.UpdatedAt = nowUnix
|
|
||||||
_, err = s.Exec(`
|
|
||||||
INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_provider_id, totp_required, external_subject, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM auth_providers WHERE public_id = ?) END, ?, ?, ?, ?)
|
|
||||||
`, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthProviderID, user.AuthProviderID, user.TOTPRequired, user.ExternalSubject, nowUnix, nowUnix)
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateUser(user models.User) error {
|
|
||||||
var err error
|
|
||||||
var nowUnix int64
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
user.UpdatedAt = nowUnix
|
|
||||||
_, err = s.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, totp_required = ?, updated_at = ? WHERE public_id = ?`,
|
|
||||||
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, user.TOTPRequired, nowUnix, user.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateUserWithPassword(user models.User, passwordHash string) error {
|
|
||||||
var err error
|
|
||||||
var nowUnix int64
|
|
||||||
var tx *sql.Tx
|
|
||||||
var owned bool
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
user.UpdatedAt = nowUnix
|
|
||||||
tx, owned, err = s.begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, totp_required = ?, password_hash = ?, updated_at = ? WHERE public_id = ?`,
|
|
||||||
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, user.TOTPRequired, passwordHash, nowUnix, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = commitIfOwned(tx, owned)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) SetUserPassword(userID, passwordHash string) error {
|
|
||||||
var err error
|
|
||||||
_, err = s.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE public_id = ?`, passwordHash, time.Now().UTC().Unix(), userID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetUserByID(id string) (models.User, error) {
|
|
||||||
var user models.User
|
|
||||||
var row *sql.Row
|
|
||||||
var err error
|
|
||||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.public_id = ?`, id)
|
|
||||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.TOTPEnabled, &user.TOTPRequired, &user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetUserByUsername(username string) (models.User, string, error) {
|
|
||||||
var user models.User
|
|
||||||
var passwordHash sql.NullString
|
|
||||||
var row *sql.Row
|
|
||||||
var err error
|
|
||||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.password_hash, COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.username = ?`, username)
|
|
||||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &passwordHash, &user.TOTPEnabled, &user.TOTPRequired, &user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return user, passwordHash.String, err
|
|
||||||
}
|
|
||||||
return user, passwordHash.String, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListUsers() ([]models.User, error) {
|
|
||||||
var rows *sql.Rows
|
|
||||||
var err error
|
|
||||||
var users []models.User
|
|
||||||
var u models.User
|
|
||||||
rows, err = s.Query(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id ORDER BY u.username`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
u = models.User{}
|
|
||||||
err = rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.IsAdmin, &u.Disabled, &u.AuthProviderID, &u.TOTPEnabled, &u.TOTPRequired, &u.AvatarURL, &u.AvatarUpdatedAt, &u.CreatedAt, &u.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
users = append(users, u)
|
|
||||||
}
|
|
||||||
return users, rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetUserAvatar(id string) (models.UserAvatar, error) {
|
|
||||||
var avatar models.UserAvatar
|
|
||||||
var row *sql.Row
|
|
||||||
var err error
|
|
||||||
row = s.QueryRow(`SELECT public_id, avatar_storage_path, avatar_content_type, avatar_updated_at FROM users WHERE public_id = ? AND avatar_storage_path != ''`, id)
|
|
||||||
err = row.Scan(&avatar.UserID, &avatar.StoragePath, &avatar.ContentType, &avatar.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return avatar, err
|
|
||||||
}
|
|
||||||
return avatar, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateUserAvatar(id string, storagePath string, contentType string) error {
|
|
||||||
var err error
|
|
||||||
var nowUnix int64
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
_, err = s.Exec(`UPDATE users SET avatar_storage_path = ?, avatar_content_type = ?, avatar_updated_at = ?, updated_at = ? WHERE public_id = ?`,
|
|
||||||
storagePath, contentType, nowUnix, nowUnix, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ClearUserAvatar(id string) error {
|
|
||||||
var err error
|
|
||||||
_, err = s.Exec(`UPDATE users SET avatar_storage_path = '', avatar_content_type = '', avatar_updated_at = 0, updated_at = ? WHERE public_id = ?`,
|
|
||||||
time.Now().UTC().Unix(), id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteUser(id string) error {
|
|
||||||
var tx *sql.Tx
|
|
||||||
var owned bool
|
|
||||||
var err error
|
|
||||||
tx, owned, err = s.begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'user' AND target_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`DELETE FROM users WHERE public_id = ?`, id)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = commitIfOwned(tx, owned)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetUserByProviderAndSub(providerPublicID string, sub string) (models.User, error) {
|
|
||||||
var user models.User
|
|
||||||
var row *sql.Row
|
|
||||||
var err error
|
|
||||||
row = s.QueryRow(`
|
|
||||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled,
|
|
||||||
COALESCE(ap.public_id, ''), u.external_subject, COALESCE(t.enabled, 0), u.totp_required,
|
|
||||||
CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END,
|
|
||||||
u.avatar_updated_at, u.created_at, u.updated_at
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
|
||||||
LEFT JOIN user_totp t ON t.user_id = u.id
|
|
||||||
WHERE ap.public_id = ? AND u.external_subject = ?
|
|
||||||
`, strings.TrimSpace(providerPublicID), strings.TrimSpace(sub))
|
|
||||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled,
|
|
||||||
&user.AuthProviderID, &user.ExternalSubject, &user.TOTPEnabled, &user.TOTPRequired,
|
|
||||||
&user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetTLSSettings() (models.TLSSettings, error) {
|
func (s *Store) GetTLSSettings() (models.TLSSettings, error) {
|
||||||
var settings models.TLSSettings
|
var settings models.TLSSettings
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
@@ -261,15 +71,15 @@ func (s *Store) GetTLSSettings() (models.TLSSettings, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) SetTLSSettings(settings models.TLSSettings) error {
|
func (s *Store) SetTLSSettings(settings models.TLSSettings) error {
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
var err error
|
var err error
|
||||||
var now int64
|
var now int64
|
||||||
var endpointPoliciesJSON string
|
var endpointPoliciesJSON string
|
||||||
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
now = time.Now().UTC().Unix()
|
now = time.Now().UTC().Unix()
|
||||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||||
@@ -376,12 +186,6 @@ func splitCSVValue(value string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) SetUserDisabled(id string, disabled bool) error {
|
|
||||||
var err error
|
|
||||||
_, err = s.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE public_id = ?`, disabled, time.Now().UTC().Unix(), id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateAPIKey(userID string, name string, tokenHash string, prefix string, expiresAt int64) (models.APIKey, error) {
|
func (s *Store) CreateAPIKey(userID string, name string, tokenHash string, prefix string, expiresAt int64) (models.APIKey, error) {
|
||||||
var key models.APIKey
|
var key models.APIKey
|
||||||
var err error
|
var err error
|
||||||
@@ -605,48 +409,6 @@ func (s *Store) ListAPIKeysAdmin(subjectType string, query string) ([]models.Adm
|
|||||||
return keys, rows.Err()
|
return keys, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
|
|
||||||
var user models.User
|
|
||||||
var tx *sql.Tx
|
|
||||||
var owned bool
|
|
||||||
var row *sql.Row
|
|
||||||
var keyID int64
|
|
||||||
var nowUnix int64
|
|
||||||
var err error
|
|
||||||
tx, owned, err = s.begin()
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
row = tx.QueryRow(`
|
|
||||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.created_at, u.updated_at, k.id
|
|
||||||
FROM api_keys k
|
|
||||||
JOIN users u ON u.id = k.user_id
|
|
||||||
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
|
||||||
WHERE k.token_hash = ?
|
|
||||||
AND u.disabled = 0
|
|
||||||
AND k.disabled = 0
|
|
||||||
AND (k.expires_at = 0 OR k.expires_at > ?)
|
|
||||||
LIMIT 1
|
|
||||||
`, tokenHash, nowUnix)
|
|
||||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.CreatedAt, &user.UpdatedAt, &keyID)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
nowUnix = time.Now().UTC().Unix()
|
|
||||||
_, err = tx.Exec(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`, nowUnix, keyID)
|
|
||||||
if err != nil {
|
|
||||||
rollbackIfOwned(tx, owned)
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
err = commitIfOwned(tx, owned)
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateSession(userID string, token string, expiresAt time.Time, totpVerified bool, oidcIDToken string) error {
|
func (s *Store) CreateSession(userID string, token string, expiresAt time.Time, totpVerified bool, oidcIDToken string) error {
|
||||||
var err error
|
var err error
|
||||||
_, err = s.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at, totp_verified, oidc_id_token)
|
_, err = s.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at, totp_verified, oidc_id_token)
|
||||||
@@ -715,28 +477,23 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
|
|||||||
var id string
|
var id string
|
||||||
var err error
|
var err error
|
||||||
var nowUnix int64
|
var nowUnix int64
|
||||||
var tx *sql.Tx
|
var tx txExecutor
|
||||||
var owned bool
|
var owned bool
|
||||||
|
|
||||||
if project.ID == "" {
|
if project.ID == "" {
|
||||||
id, err = util.NewID()
|
id, err = util.NewID()
|
||||||
if err != nil {
|
if err != nil { return project, err }
|
||||||
return project, err
|
|
||||||
}
|
|
||||||
project.ID = id
|
project.ID = id
|
||||||
}
|
}
|
||||||
nowUnix = time.Now().UTC().Unix()
|
nowUnix = time.Now().UTC().Unix()
|
||||||
project.CreatedAt = nowUnix
|
project.CreatedAt = nowUnix
|
||||||
project.UpdatedAt = nowUnix
|
project.UpdatedAt = nowUnix
|
||||||
if project.UpdatedBy == "" {
|
if project.UpdatedBy == "" { project.UpdatedBy = project.CreatedBy }
|
||||||
project.UpdatedBy = project.CreatedBy
|
|
||||||
}
|
|
||||||
tx, owned, err = s.begin()
|
tx, owned, err = s.begin()
|
||||||
if err != nil {
|
if err != nil { return project, err }
|
||||||
return project, err
|
if project.HomePage == "" { project.HomePage = "settings" }
|
||||||
}
|
|
||||||
if project.HomePage == "" {
|
|
||||||
project.HomePage = "settings"
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`INSERT INTO projects (public_id, slug, name, description, home_page, created_by, updated_by, created_at, updated_at)
|
_, err = tx.Exec(`INSERT INTO projects (public_id, slug, name, description, home_page, created_by, updated_by, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`,
|
||||||
project.ID,
|
project.ID,
|
||||||
@@ -760,15 +517,16 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
|
|||||||
rollbackIfOwned(tx, owned)
|
rollbackIfOwned(tx, owned)
|
||||||
return project, err
|
return project, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = commitIfOwned(tx, owned)
|
err = commitIfOwned(tx, owned)
|
||||||
if err != nil {
|
if err != nil { return project, err }
|
||||||
return project, err
|
|
||||||
}
|
|
||||||
return project, nil
|
return project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateProject(project models.Project) error {
|
func (s *Store) UpdateProject(project models.Project) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if project.HomePage == "" {
|
if project.HomePage == "" {
|
||||||
project.HomePage = "settings"
|
project.HomePage = "settings"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
import "strings"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
import "codit/internal/models"
|
||||||
|
import "codit/internal/util"
|
||||||
|
|
||||||
|
func (s *Store) CreateUser(user models.User, passwordHash string) (models.User, error) {
|
||||||
|
var id string
|
||||||
|
var err error
|
||||||
|
var nowUnix int64
|
||||||
|
if user.ID == "" {
|
||||||
|
id, err = util.NewID()
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
user.ID = id
|
||||||
|
}
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
user.CreatedAt = nowUnix
|
||||||
|
user.UpdatedAt = nowUnix
|
||||||
|
_, err = s.Exec(`
|
||||||
|
INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_provider_id, totp_required, external_subject, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM auth_providers WHERE public_id = ?) END, ?, ?, ?, ?)
|
||||||
|
`, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthProviderID, user.AuthProviderID, user.TOTPRequired, user.ExternalSubject, nowUnix, nowUnix)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateUser(user models.User) error {
|
||||||
|
var err error
|
||||||
|
var nowUnix int64
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
user.UpdatedAt = nowUnix
|
||||||
|
_, err = s.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, totp_required = ?, updated_at = ? WHERE public_id = ?`,
|
||||||
|
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, user.TOTPRequired, nowUnix, user.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateUserWithPassword(user models.User, passwordHash string) error {
|
||||||
|
var err error
|
||||||
|
var nowUnix int64
|
||||||
|
var tx txExecutor
|
||||||
|
var owned bool
|
||||||
|
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
user.UpdatedAt = nowUnix
|
||||||
|
tx, owned, err = s.begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, totp_required = ?, password_hash = ?, updated_at = ? WHERE public_id = ?`,
|
||||||
|
user.DisplayName, user.Email, user.IsAdmin, user.Disabled, user.TOTPRequired, passwordHash, nowUnix, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = commitIfOwned(tx, owned)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) SetUserPassword(userID, passwordHash string) error {
|
||||||
|
var err error
|
||||||
|
_, err = s.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE public_id = ?`, passwordHash, time.Now().UTC().Unix(), userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByID(id string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
var row *sql.Row
|
||||||
|
var err error
|
||||||
|
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.public_id = ?`, id)
|
||||||
|
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.TOTPEnabled, &user.TOTPRequired, &user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByUsername(username string) (models.User, string, error) {
|
||||||
|
var user models.User
|
||||||
|
var passwordHash sql.NullString
|
||||||
|
var row *sql.Row
|
||||||
|
var err error
|
||||||
|
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.password_hash, COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.username = ?`, username)
|
||||||
|
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &passwordHash, &user.TOTPEnabled, &user.TOTPRequired, &user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return user, passwordHash.String, err
|
||||||
|
}
|
||||||
|
return user, passwordHash.String, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListUsers() ([]models.User, error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
var users []models.User
|
||||||
|
var u models.User
|
||||||
|
rows, err = s.Query(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END, u.avatar_updated_at, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id ORDER BY u.username`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
u = models.User{}
|
||||||
|
err = rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.IsAdmin, &u.Disabled, &u.AuthProviderID, &u.TOTPEnabled, &u.TOTPRequired, &u.AvatarURL, &u.AvatarUpdatedAt, &u.CreatedAt, &u.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
return users, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserAvatar(id string) (models.UserAvatar, error) {
|
||||||
|
var avatar models.UserAvatar
|
||||||
|
var row *sql.Row
|
||||||
|
var err error
|
||||||
|
row = s.QueryRow(`SELECT public_id, avatar_storage_path, avatar_content_type, avatar_updated_at FROM users WHERE public_id = ? AND avatar_storage_path != ''`, id)
|
||||||
|
err = row.Scan(&avatar.UserID, &avatar.StoragePath, &avatar.ContentType, &avatar.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return avatar, err
|
||||||
|
}
|
||||||
|
return avatar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateUserAvatar(id string, storagePath string, contentType string) error {
|
||||||
|
var err error
|
||||||
|
var nowUnix int64
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
_, err = s.Exec(`UPDATE users SET avatar_storage_path = ?, avatar_content_type = ?, avatar_updated_at = ?, updated_at = ? WHERE public_id = ?`,
|
||||||
|
storagePath, contentType, nowUnix, nowUnix, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ClearUserAvatar(id string) error {
|
||||||
|
var err error
|
||||||
|
_, err = s.Exec(`UPDATE users SET avatar_storage_path = '', avatar_content_type = '', avatar_updated_at = 0, updated_at = ? WHERE public_id = ?`,
|
||||||
|
time.Now().UTC().Unix(), id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteUser(id string) error {
|
||||||
|
var tx txExecutor
|
||||||
|
var owned bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tx, owned, err = s.begin()
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
|
_, err = tx.Exec(`DELETE FROM project_role_bindings WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`DELETE FROM ssh_principal_grant_targets WHERE target_type = 'user' AND target_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`DELETE FROM subject_permissions WHERE subject_type = 'user' AND subject_id = (SELECT id FROM users WHERE public_id = ?)`, id)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`DELETE FROM users WHERE public_id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = commitIfOwned(tx, owned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByProviderAndSub(providerPublicID string, sub string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
var row *sql.Row
|
||||||
|
var err error
|
||||||
|
row = s.QueryRow(`
|
||||||
|
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled,
|
||||||
|
COALESCE(ap.public_id, ''), u.external_subject, COALESCE(t.enabled, 0), u.totp_required,
|
||||||
|
CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END,
|
||||||
|
u.avatar_updated_at, u.created_at, u.updated_at
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
||||||
|
LEFT JOIN user_totp t ON t.user_id = u.id
|
||||||
|
WHERE ap.public_id = ? AND u.external_subject = ?
|
||||||
|
`, strings.TrimSpace(providerPublicID), strings.TrimSpace(sub))
|
||||||
|
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled,
|
||||||
|
&user.AuthProviderID, &user.ExternalSubject, &user.TOTPEnabled, &user.TOTPRequired,
|
||||||
|
&user.AvatarURL, &user.AvatarUpdatedAt, &user.CreatedAt, &user.UpdatedAt)
|
||||||
|
if err != nil { return user, err }
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) SetUserDisabled(id string, disabled bool) error {
|
||||||
|
var err error
|
||||||
|
_, err = s.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE public_id = ?`, disabled, time.Now().UTC().Unix(), id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
var tx txExecutor
|
||||||
|
var owned bool
|
||||||
|
var row *sql.Row
|
||||||
|
var keyID int64
|
||||||
|
var nowUnix int64
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tx, owned, err = s.begin()
|
||||||
|
if err != nil { return user, err }
|
||||||
|
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
row = tx.QueryRow(`
|
||||||
|
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.created_at, u.updated_at, k.id
|
||||||
|
FROM api_keys k
|
||||||
|
JOIN users u ON u.id = k.user_id
|
||||||
|
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
||||||
|
WHERE k.token_hash = ?
|
||||||
|
AND u.disabled = 0
|
||||||
|
AND k.disabled = 0
|
||||||
|
AND (k.expires_at = 0 OR k.expires_at > ?)
|
||||||
|
LIMIT 1
|
||||||
|
`, tokenHash, nowUnix)
|
||||||
|
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.CreatedAt, &user.UpdatedAt, &keyID)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
nowUnix = time.Now().UTC().Unix()
|
||||||
|
_, err = tx.Exec(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`, nowUnix, keyID)
|
||||||
|
if err != nil {
|
||||||
|
rollbackIfOwned(tx, owned)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
err = commitIfOwned(tx, owned)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
@@ -538,18 +538,22 @@ func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]strin
|
|||||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "invalid credentials")
|
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models.User, totpVerified bool, oidcIDToken string) {
|
func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models.User, totpVerified bool, oidcIDToken string) error {
|
||||||
var token string
|
var token string
|
||||||
var err error
|
var err error
|
||||||
var expires time.Time
|
var expires time.Time
|
||||||
var cookie *http.Cookie
|
var cookie *http.Cookie
|
||||||
|
|
||||||
token, err = auth.NewSessionToken()
|
token, err = auth.NewSessionToken()
|
||||||
if err != nil { return }
|
if err != nil { return err }
|
||||||
|
|
||||||
expires = auth.SessionExpiry(api.Cfg)
|
expires = auth.SessionExpiry(api.Cfg)
|
||||||
_ = api.store(r).DeleteExpiredSessions(time.Now().UTC())
|
_ = api.store(r).DeleteExpiredSessions(time.Now().UTC())
|
||||||
_ = api.store(r).CreateSession(user.ID, token, expires, totpVerified, oidcIDToken)
|
err = api.store(r).CreateSession(user.ID, token, expires, totpVerified, oidcIDToken)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "session create failed user=%s err=%v", user.Username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
cookie = &http.Cookie{
|
cookie = &http.Cookie{
|
||||||
Name: api.sessionCookieName(),
|
Name: api.sessionCookieName(),
|
||||||
Value: token,
|
Value: token,
|
||||||
@@ -558,6 +562,7 @@ func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models
|
|||||||
Expires: expires,
|
Expires: expires,
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
func (api *API) Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
|
|||||||
user.TOTPEffectiveRequired = required
|
user.TOTPEffectiveRequired = required
|
||||||
user.TOTPSetupRequired = required && !user.TOTPEnabled
|
user.TOTPSetupRequired = required && !user.TOTPEnabled
|
||||||
sessionVerified = !required
|
sessionVerified = !required
|
||||||
api.issueSession(w, r, user, sessionVerified, token.IDToken)
|
err = api.issueSession(w, r, user, sessionVerified, token.IDToken)
|
||||||
|
if err != nil {
|
||||||
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
|
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
|
|||||||
if required && !user.TOTPEnabled {
|
if required && !user.TOTPEnabled {
|
||||||
sessionVerified = false
|
sessionVerified = false
|
||||||
}
|
}
|
||||||
api.issueSession(w, r, user, sessionVerified, "")
|
err = api.issueSession(w, r, user, sessionVerified, "")
|
||||||
|
if err != nil {
|
||||||
|
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
WriteJSON(w, http.StatusOK, user)
|
WriteJSON(w, http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,32 +110,47 @@ func WithStoreTransactionUnbuffered(store *db.Store, next http.Handler) http.Han
|
|||||||
return withStoreTransaction(store, next, false)
|
return withStoreTransaction(store, next, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isWebSocketUpgrade(r *http.Request) bool {
|
||||||
|
if r == nil { return false }
|
||||||
|
return strings.Contains(strings.ToLower(strings.TrimSpace(r.Header.Get("Connection"))), "upgrade") &&
|
||||||
|
strings.EqualFold(strings.TrimSpace(r.Header.Get("Upgrade")), "websocket")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rollbackIfPanic(txStore *db.Store) {
|
||||||
|
var rec interface{}
|
||||||
|
rec = recover()
|
||||||
|
if rec != nil { // if the goroutne panicked
|
||||||
|
_ = txStore.Rollback() // rollback the transaction
|
||||||
|
panic(rec) // resume panicking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool) http.Handler {
|
func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var txStore *db.Store
|
var txStore *db.Store
|
||||||
var recorder *bufferedResponseWriter
|
|
||||||
var statusWriter *passThroughStatusWriter
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if isWebSocketUpgrade(r) {
|
if isWebSocketUpgrade(r) {
|
||||||
|
// no transaction on websocket upgrade
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
txStore, err = store.BeginStore(r.Context())
|
|
||||||
|
if isReadOnlyMethod(r.Method) {
|
||||||
|
txStore, err = store.BeginStore(r.Context())
|
||||||
|
} else {
|
||||||
|
txStore, err = store.BeginImmediateStore(r.Context())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arrange to rollback even if the handler panics
|
||||||
|
defer rollbackIfPanic(txStore)
|
||||||
|
|
||||||
r = WithStore(r, txStore)
|
r = WithStore(r, txStore)
|
||||||
if isReadOnlyMethod(r.Method) {
|
if isReadOnlyMethod(r.Method) {
|
||||||
defer func() {
|
|
||||||
var rec interface{}
|
|
||||||
rec = recover()
|
|
||||||
if rec != nil {
|
|
||||||
_ = txStore.Rollback()
|
|
||||||
panic(rec)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
err = txStore.Commit()
|
err = txStore.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -143,18 +158,11 @@ func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runAfterCommit(r.Context())
|
runAfterCommit(r.Context())
|
||||||
return
|
} else if !bufferWrites {
|
||||||
}
|
// read-write or write-only. unbufferred.
|
||||||
if !bufferWrites {
|
var statusWriter *passThroughStatusWriter
|
||||||
|
|
||||||
statusWriter = newPassThroughStatusWriter(w)
|
statusWriter = newPassThroughStatusWriter(w)
|
||||||
defer func() {
|
|
||||||
var rec interface{}
|
|
||||||
rec = recover()
|
|
||||||
if rec != nil {
|
|
||||||
_ = txStore.Rollback()
|
|
||||||
panic(rec)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
next.ServeHTTP(statusWriter, r)
|
next.ServeHTTP(statusWriter, r)
|
||||||
if statusWriter.status >= 400 {
|
if statusWriter.status >= 400 {
|
||||||
_ = txStore.Rollback()
|
_ = txStore.Rollback()
|
||||||
@@ -169,40 +177,27 @@ func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runAfterCommit(r.Context())
|
runAfterCommit(r.Context())
|
||||||
return
|
} else {
|
||||||
}
|
// read-write or write-only. bufferred.
|
||||||
recorder = newBufferedResponseWriter()
|
var recorder *bufferedResponseWriter
|
||||||
defer func() {
|
|
||||||
var rec interface{}
|
|
||||||
rec = recover()
|
|
||||||
if rec != nil {
|
|
||||||
_ = txStore.Rollback()
|
|
||||||
panic(rec)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
next.ServeHTTP(recorder, r)
|
|
||||||
if recorder.status >= 400 {
|
|
||||||
_ = txStore.Rollback()
|
|
||||||
recorder.FlushTo(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = txStore.Commit()
|
|
||||||
if err != nil {
|
|
||||||
_ = txStore.Rollback()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runAfterCommit(r.Context())
|
|
||||||
recorder.FlushTo(w)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWebSocketUpgrade(r *http.Request) bool {
|
recorder = newBufferedResponseWriter()
|
||||||
if r == nil {
|
next.ServeHTTP(recorder, r)
|
||||||
return false
|
if recorder.status >= 400 {
|
||||||
}
|
_ = txStore.Rollback()
|
||||||
return strings.Contains(strings.ToLower(strings.TrimSpace(r.Header.Get("Connection"))), "upgrade") &&
|
recorder.FlushTo(w)
|
||||||
strings.EqualFold(strings.TrimSpace(r.Header.Get("Upgrade")), "websocket")
|
return
|
||||||
|
}
|
||||||
|
err = txStore.Commit()
|
||||||
|
if err != nil {
|
||||||
|
_ = txStore.Rollback()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runAfterCommit(r.Context())
|
||||||
|
recorder.FlushTo(w)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAfterCommit(ctx context.Context, fn func()) {
|
func RegisterAfterCommit(ctx context.Context, fn func()) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Block, BlockProperties, BoardFieldValue } from '../api'
|
|||||||
// --- Constants ----------------------------------------------------------------
|
// --- Constants ----------------------------------------------------------------
|
||||||
|
|
||||||
const FALLBACK_COLOR = '#9e9e9e'
|
const FALLBACK_COLOR = '#9e9e9e'
|
||||||
|
const OTHER_STATUS_VALUE = '__other__'
|
||||||
const CHART_H = 280
|
const CHART_H = 280
|
||||||
const M = { top: 16, right: 16, bottom: 56, left: 48 }
|
const M = { top: 16, right: 16, bottom: 56, left: 48 }
|
||||||
const M_H = { top: 10, right: 48, bottom: 28, left: 90 }
|
const M_H = { top: 10, right: 48, bottom: 28, left: 90 }
|
||||||
@@ -516,7 +517,7 @@ function AssigneeWorkloadChart({ width, data }: { width: number; data: AssigneeD
|
|||||||
<text x={avatarCx} y={cy + 4} textAnchor="middle" fontSize={avatarR > 10 ? 10 : 9} fill="#fff" fontWeight={700}>
|
<text x={avatarCx} y={cy + 4} textAnchor="middle" fontSize={avatarR > 10 ? 10 : 9} fill="#fff" fontWeight={700}>
|
||||||
{getInitials(d.name)}
|
{getInitials(d.name)}
|
||||||
</text>
|
</text>
|
||||||
<text x={avatarCx + avatarR + 6} y={cy + 4} textAnchor="start" fontSize={11} fill="#444">
|
<text x={avatarCx + avatarR + 6} y={cy + 4} textAnchor="start" fontSize={11} fill={labelColor}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</text>
|
</text>
|
||||||
<rect x={0} y={y} width={iw} height={bh} fill="#f0f0f0" rx={5} />
|
<rect x={0} y={y} width={iw} height={bh} fill="#f0f0f0" rx={5} />
|
||||||
@@ -525,7 +526,7 @@ function AssigneeWorkloadChart({ width, data }: { width: number; data: AssigneeD
|
|||||||
const bw = xScale(seg.count) ?? 0
|
const bw = xScale(seg.count) ?? 0
|
||||||
const el = (
|
const el = (
|
||||||
<rect
|
<rect
|
||||||
key={seg.statusValue || 'other'}
|
key={seg.statusValue || OTHER_STATUS_VALUE}
|
||||||
x={xLeft} y={y} width={bw} height={bh} fill={seg.color} rx={5}
|
x={xLeft} y={y} width={bw} height={bh} fill={seg.color} rx={5}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={(e) => {
|
||||||
if (!containerRef.current) return
|
if (!containerRef.current) return
|
||||||
@@ -601,7 +602,7 @@ function SprintStackedBarChart({ width, data }: { width: number; data: SprintDat
|
|||||||
<g key={d.sprintValue}>
|
<g key={d.sprintValue}>
|
||||||
{stacked.map(({ seg, y, h }) => (
|
{stacked.map(({ seg, y, h }) => (
|
||||||
<rect
|
<rect
|
||||||
key={seg.statusValue}
|
key={seg.statusValue || OTHER_STATUS_VALUE}
|
||||||
x={x} y={y} width={bw} height={h} fill={seg.color} rx={2}
|
x={x} y={y} width={bw} height={h} fill={seg.color} rx={2}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={(e) => {
|
||||||
if (!containerRef.current) return
|
if (!containerRef.current) return
|
||||||
@@ -868,6 +869,7 @@ export default function BoardChartView({ blocks, properties, fieldValues }: Boar
|
|||||||
const [throughputPeriod, setThroughputPeriod] = useState<ThroughputPeriod>('weekly')
|
const [throughputPeriod, setThroughputPeriod] = useState<ThroughputPeriod>('weekly')
|
||||||
const statusValues = fieldValues['status'] ?? []
|
const statusValues = fieldValues['status'] ?? []
|
||||||
const sprintValues = fieldValues['sprint'] ?? []
|
const sprintValues = fieldValues['sprint'] ?? []
|
||||||
|
const knownStatusValues: Set<string> = new Set(statusValues.map((sv) => sv.value))
|
||||||
|
|
||||||
const statusCountMap = properties.reduce<Record<string, number>>((acc, p) => {
|
const statusCountMap = properties.reduce<Record<string, number>>((acc, p) => {
|
||||||
const k = p.status || ''; acc[k] = (acc[k] ?? 0) + 1; return acc
|
const k = p.status || ''; acc[k] = (acc[k] ?? 0) + 1; return acc
|
||||||
@@ -877,6 +879,15 @@ export default function BoardChartView({ blocks, properties, fieldValues }: Boar
|
|||||||
count: statusCountMap[sv.value] ?? 0,
|
count: statusCountMap[sv.value] ?? 0,
|
||||||
color: sv.color || FALLBACK_COLOR,
|
color: sv.color || FALLBACK_COLOR,
|
||||||
}))
|
}))
|
||||||
|
const statusOtherCount: number = properties.filter((p) => !knownStatusValues.has(p.status || '')).length
|
||||||
|
if (statusOtherCount > 0) {
|
||||||
|
statusData.push({
|
||||||
|
value: OTHER_STATUS_VALUE,
|
||||||
|
label: 'Other',
|
||||||
|
count: statusOtherCount,
|
||||||
|
color: FALLBACK_COLOR,
|
||||||
|
})
|
||||||
|
}
|
||||||
const totalCards = properties.length
|
const totalCards = properties.length
|
||||||
|
|
||||||
const sprintData: SprintDatum[] = sprintValues
|
const sprintData: SprintDatum[] = sprintValues
|
||||||
@@ -887,6 +898,15 @@ export default function BoardChartView({ blocks, properties, fieldValues }: Boar
|
|||||||
count: inSprint.filter((p) => (p.status || '') === sv.value).length,
|
count: inSprint.filter((p) => (p.status || '') === sv.value).length,
|
||||||
color: sv.color || FALLBACK_COLOR,
|
color: sv.color || FALLBACK_COLOR,
|
||||||
}))
|
}))
|
||||||
|
const sprintOtherCount: number = inSprint.filter((p) => !knownStatusValues.has(p.status || '')).length
|
||||||
|
if (sprintOtherCount > 0) {
|
||||||
|
segments.push({
|
||||||
|
statusValue: OTHER_STATUS_VALUE,
|
||||||
|
label: 'Other',
|
||||||
|
count: sprintOtherCount,
|
||||||
|
color: FALLBACK_COLOR,
|
||||||
|
})
|
||||||
|
}
|
||||||
return { sprintValue: sprint.value, label: sprint.label, total: inSprint.length, segments }
|
return { sprintValue: sprint.value, label: sprint.label, total: inSprint.length, segments }
|
||||||
})
|
})
|
||||||
.filter((d) => d.total > 0)
|
.filter((d) => d.total > 0)
|
||||||
|
|||||||
@@ -100,10 +100,22 @@ export default function CardDetailDialog(props: CardDetailDialogProps) {
|
|||||||
// Track last-saved values for text fields so blur/cancel can revert correctly
|
// Track last-saved values for text fields so blur/cancel can revert correctly
|
||||||
const savedTitleRef = useRef('')
|
const savedTitleRef = useRef('')
|
||||||
const savedDescriptionRef = useRef('')
|
const savedDescriptionRef = useRef('')
|
||||||
|
|
||||||
|
const checklistInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const focusChecklistInputAfterSaveRef = useRef(false)
|
||||||
|
|
||||||
const checklistSensors = useSensors(
|
const checklistSensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savingChecklist) return
|
||||||
|
if (!focusChecklistInputAfterSaveRef.current) return
|
||||||
|
|
||||||
|
focusChecklistInputAfterSaveRef.current = false
|
||||||
|
requestAnimationFrame(() => { checklistInputRef.current?.focus() })
|
||||||
|
}, [savingChecklist])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !block) return
|
if (!open || !block) return
|
||||||
setTitle(block.title)
|
setTitle(block.title)
|
||||||
@@ -272,8 +284,11 @@ export default function CardDetailDialog(props: CardDetailDialogProps) {
|
|||||||
|
|
||||||
function handleCreateChecklistItem() {
|
function handleCreateChecklistItem() {
|
||||||
if (!block || !newChecklistTitle.trim()) return
|
if (!block || !newChecklistTitle.trim()) return
|
||||||
|
|
||||||
|
focusChecklistInputAfterSaveRef.current = true
|
||||||
setSavingChecklist(true)
|
setSavingChecklist(true)
|
||||||
setChecklistError(null)
|
setChecklistError(null)
|
||||||
|
|
||||||
api.createBlockChecklistItem(boardId, block.id, newChecklistTitle.trim())
|
api.createBlockChecklistItem(boardId, block.id, newChecklistTitle.trim())
|
||||||
.then((item: BlockChecklistItem) => {
|
.then((item: BlockChecklistItem) => {
|
||||||
setChecklistItems((prev: BlockChecklistItem[]) => [...prev, item])
|
setChecklistItems((prev: BlockChecklistItem[]) => [...prev, item])
|
||||||
@@ -647,10 +662,16 @@ export default function CardDetailDialog(props: CardDetailDialogProps) {
|
|||||||
{checklistError ? <Alert severity="error" sx={{ mt: 1 }}>{checklistError}</Alert> : null}
|
{checklistError ? <Alert severity="error" sx={{ mt: 1 }}>{checklistError}</Alert> : null}
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 0.5 }}>
|
||||||
<TextField
|
<TextField
|
||||||
|
inputRef={checklistInputRef}
|
||||||
placeholder="Add checklist item..."
|
placeholder="Add checklist item..."
|
||||||
value={newChecklistTitle}
|
value={newChecklistTitle}
|
||||||
onChange={(e) => setNewChecklistTitle(e.target.value)}
|
onChange={(e) => setNewChecklistTitle(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') handleCreateChecklistItem() }}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCreateChecklistItem()
|
||||||
|
}
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
disabled={savingChecklist}
|
disabled={savingChecklist}
|
||||||
|
|||||||
Reference in New Issue
Block a user