Compare commits
5 Commits
a28b6cfa80
...
6c4572d27f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c4572d27f | |||
| 858afe5371 | |||
| 11d2d80d77 | |||
| e1c056ebc4 | |||
| 3a2d962472 |
@@ -9,6 +9,7 @@ import "sort"
|
||||
import "strconv"
|
||||
import "strings"
|
||||
|
||||
import storedb "codit/internal/db"
|
||||
import _ "modernc.org/sqlite"
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
import "github.com/rivo/tview"
|
||||
@@ -72,12 +73,15 @@ func main() {
|
||||
|
||||
func newBrowser(dbPath string, dataDir string) (*browser, error) {
|
||||
var err error
|
||||
var store *storedb.Store
|
||||
var db *sql.DB
|
||||
var br *browser
|
||||
db, err = sql.Open("sqlite", "file:"+dbPath+"?_pragma=foreign_keys(1)")
|
||||
|
||||
store, err = storedb.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db = store.DB
|
||||
br = &browser{
|
||||
DB: db,
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var err error
|
||||
var owned bool
|
||||
var now int64
|
||||
@@ -282,9 +282,7 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
|
||||
if strings.TrimSpace(item.ID) == "" {
|
||||
item.ID, err = util.NewID()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
}
|
||||
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
|
||||
item.LDAPUserFilter = "(uid={username})"
|
||||
@@ -293,14 +291,10 @@ func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
item.OIDCScopes = "openid profile email"
|
||||
}
|
||||
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO auth_providers (
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var err error
|
||||
var owned bool
|
||||
var now int64
|
||||
@@ -358,9 +352,7 @@ func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvide
|
||||
}
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
|
||||
_, err = tx.Exec(`UPDATE auth_providers SET
|
||||
name = ?, enabled = ?,
|
||||
|
||||
@@ -307,7 +307,7 @@ func (s *Store) ReorderBlockChecklistItems(boardID, blockID string, ids []string
|
||||
var internalBlockID int64
|
||||
var err error
|
||||
var now int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var i int
|
||||
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) {
|
||||
var internalBlockID int64
|
||||
var err error
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var assigneeIDs []string
|
||||
var i int
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *Store) CreateBoard(board models.Board) (models.Board, error) {
|
||||
var id string
|
||||
var err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
|
||||
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
|
||||
// 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.
|
||||
func resolveParentBlockID(tx *sql.Tx, boardPublicID, parentPublicID string) (int64, error) {
|
||||
func resolveParentBlockID(tx txExecutor, boardPublicID, parentPublicID string) (int64, error) {
|
||||
var parentID int64
|
||||
var row *sql.Row
|
||||
if parentPublicID == "" {
|
||||
@@ -338,7 +338,7 @@ func resolveParentBlockID(tx *sql.Tx, boardPublicID, parentPublicID string) (int
|
||||
|
||||
// hasCycleAfterReparent returns true when assigning newParentID as the parent
|
||||
// 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 row *sql.Row
|
||||
if newParentID == 0 {
|
||||
@@ -364,7 +364,7 @@ func hasCycleAfterReparent(tx *sql.Tx, blockID, newParentID int64) (bool, error)
|
||||
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
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO blocks_history
|
||||
@@ -381,7 +381,7 @@ func (s *Store) CreateBlock(block models.Block) (models.Block, error) {
|
||||
var id string
|
||||
var err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var result sql.Result
|
||||
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) {
|
||||
var err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var row *sql.Row
|
||||
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 {
|
||||
var err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var i int
|
||||
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 {
|
||||
var err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var blockID int64
|
||||
var row *sql.Row
|
||||
|
||||
+115
-49
@@ -9,51 +9,102 @@ import "path/filepath"
|
||||
import "sort"
|
||||
import "strings"
|
||||
|
||||
type Store struct {
|
||||
DB *sql.DB
|
||||
tx *sql.Tx
|
||||
}
|
||||
|
||||
type sqlExecutor interface {
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
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 drv string
|
||||
var imm bool
|
||||
var err error
|
||||
|
||||
drv = driverName(driver)
|
||||
if drv == "sqlite" {
|
||||
dsn = appendSQLitePragmas(dsn, map[string]string{
|
||||
"busy_timeout": "10000",
|
||||
"busy_timeout": "10000", // make this configurable too
|
||||
"journal_mode": "WAL",
|
||||
"foreign_keys": "1",
|
||||
})
|
||||
imm = true
|
||||
}
|
||||
db, err = sql.Open(drv, dsn)
|
||||
if err != nil {
|
||||
goto oops
|
||||
}
|
||||
if err != nil { goto oops }
|
||||
|
||||
//if drv == "sqlite" {
|
||||
// db.SetMaxOpenConns(10)
|
||||
// db.SetMaxIdleConns(1)
|
||||
//}
|
||||
if drv == "sqlite" {
|
||||
db.SetMaxOpenConns(64) // TODO: make this configurable
|
||||
// db.SetMaxIdleConns(8)
|
||||
// db.SetConnMaxIdleTime(1 * time.Minute)
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
goto oops
|
||||
}
|
||||
if err != nil { 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:
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
if db != nil { db.Close() }
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -149,57 +200,72 @@ func (s *Store) BeginStore(ctx context.Context) (*Store, error) {
|
||||
var tx *sql.Tx
|
||||
var err error
|
||||
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("nil store")
|
||||
}
|
||||
if s.tx != nil {
|
||||
return s, nil
|
||||
}
|
||||
if s == nil { return nil, fmt.Errorf("nil store") }
|
||||
if s.tx != nil { return s, nil } // more than one BeginStore calls, return the existing tx
|
||||
|
||||
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 {
|
||||
_ = conn.Close()
|
||||
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 {
|
||||
if s == nil || s.tx == nil {
|
||||
return nil
|
||||
}
|
||||
if s == nil || s.tx == nil { return nil }
|
||||
return s.tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Store) Rollback() error {
|
||||
if s == nil || s.tx == nil {
|
||||
return nil
|
||||
}
|
||||
if s == nil || s.tx == nil { return nil }
|
||||
return s.tx.Rollback()
|
||||
}
|
||||
|
||||
func (s *Store) begin() (*sql.Tx, bool, error) {
|
||||
func (s *Store) begin() (txExecutor, bool, error) {
|
||||
var tx *sql.Tx
|
||||
var err error
|
||||
|
||||
if s != nil && s.tx != nil {
|
||||
return s.tx, false, nil
|
||||
}
|
||||
// return an existing transaction object that's already started by
|
||||
// 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()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err != nil { return nil, false, err }
|
||||
|
||||
// the second return value is true to indicate that the transaction has been started here
|
||||
return tx, true, nil
|
||||
}
|
||||
|
||||
func rollbackIfOwned(tx *sql.Tx, owned bool) {
|
||||
if tx != nil && owned {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
func rollbackIfOwned(tx txExecutor, owned bool) {
|
||||
if tx != nil && owned { _ = tx.Rollback() }
|
||||
}
|
||||
|
||||
func commitIfOwned(tx *sql.Tx, owned bool) error {
|
||||
if !owned {
|
||||
return nil
|
||||
}
|
||||
func commitIfOwned(tx txExecutor, owned bool) error {
|
||||
if !owned { return nil }
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
|
||||
@@ -128,21 +128,21 @@ func (s *Store) UpdateUserGroup(item models.UserGroup) (models.UserGroup, error)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUserGroup(groupID string) error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var group models.UserGroup
|
||||
|
||||
group, err = s.GetUserGroup(groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
if NormalizeUserGroupScope(group.Scope) == UserGroupScopeAllUsers {
|
||||
return errors.New("all users group cannot be deleted")
|
||||
}
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { 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))
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
@@ -163,11 +163,8 @@ func (s *Store) DeleteUserGroup(groupID string) error {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
return commitIfOwned(tx, owned)
|
||||
}
|
||||
|
||||
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 err error
|
||||
var group models.UserGroup
|
||||
|
||||
group, err = s.GetUserGroup(groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
||||
return errors.New("virtual groups do not accept explicit members")
|
||||
}
|
||||
|
||||
now = time.Now().UTC().Unix()
|
||||
_, 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 = ?), ?)`,
|
||||
strings.TrimSpace(groupID), strings.TrimSpace(userID), now)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) RemoveUserGroupMember(groupID string, userID string) error {
|
||||
var err error
|
||||
var group models.UserGroup
|
||||
|
||||
group, err = s.GetUserGroup(groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
if NormalizeUserGroupScope(group.Scope) != UserGroupScopeExplicit {
|
||||
return errors.New("virtual groups do not have explicit members")
|
||||
}
|
||||
|
||||
_, err = s.Exec(`DELETE FROM user_group_members
|
||||
WHERE group_id = (SELECT id FROM user_groups 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 items []models.ProjectGroupRole
|
||||
var item models.ProjectGroupRole
|
||||
|
||||
rows, err = s.Query(`SELECT p.public_id, g.public_id, b.role, b.created_at
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
JOIN user_groups g ON g.id = b.subject_id
|
||||
WHERE p.public_id = ? AND b.subject_type = 'group'
|
||||
ORDER BY g.name`, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil { return nil, err }
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
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)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil { return nil, err }
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ func (s *Store) UpsertProjectGroupRole(projectID string, groupID string, role st
|
||||
var result sql.Result
|
||||
var rowsAffected int64
|
||||
var err error
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
rowsAffected, err = result.RowsAffected()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if rowsAffected <= 0 {
|
||||
return item, errors.New("group not found")
|
||||
}
|
||||
if err != nil { return item, err }
|
||||
|
||||
if rowsAffected <= 0 { return item, errors.New("group not found") }
|
||||
|
||||
item.ProjectID = strings.TrimSpace(projectID)
|
||||
item.GroupID = strings.TrimSpace(groupID)
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var now int64
|
||||
@@ -684,7 +684,7 @@ func (s *Store) listPKIClientProfileTargets(profileID string) ([]models.PKIClien
|
||||
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
|
||||
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
||||
targetID = strings.TrimSpace(targetID)
|
||||
|
||||
@@ -128,7 +128,7 @@ func (s *Store) DeletePKICA(id string) error {
|
||||
}
|
||||
|
||||
func (s *Store) DeletePKICASubtree(id string) error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
@@ -205,7 +205,7 @@ func (s *Store) DeletePKICASubtree(id string) error {
|
||||
}
|
||||
|
||||
func (s *Store) NextPKICASerial(caID string) (int64, error) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var principal models.ServicePrincipal
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var row *sql.Row
|
||||
var keyID int64
|
||||
@@ -86,9 +86,8 @@ func (s *Store) GetPrincipalByAPIKeyHash(tokenHash string) (models.ServicePrinci
|
||||
var err error
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return principal, err
|
||||
}
|
||||
if err != nil { return principal, err }
|
||||
|
||||
now = time.Now().UTC()
|
||||
currentUnix = now.Unix()
|
||||
row = tx.QueryRow(`
|
||||
|
||||
@@ -73,13 +73,13 @@ func (s *Store) UpdateServicePrincipal(item models.ServicePrincipal) error {
|
||||
}
|
||||
|
||||
func (s *Store) DeleteServicePrincipal(id string) error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { 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)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
@@ -95,8 +95,8 @@ func (s *Store) DeleteServicePrincipal(id string) error {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
err = commitIfOwned(tx, owned)
|
||||
return err
|
||||
|
||||
return commitIfOwned(tx, owned)
|
||||
}
|
||||
|
||||
func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, error) {
|
||||
@@ -119,10 +119,9 @@ func (s *Store) ListCertPrincipalBindings() ([]models.CertPrincipalBinding, erro
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil { return nil, err }
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var res sql.Result
|
||||
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 {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var now int64
|
||||
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 {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var interval int64
|
||||
var nextSync int64
|
||||
@@ -310,7 +310,7 @@ func (s *Store) SetRPMMirrorSyncEnabled(repoID string, path string, enabled bool
|
||||
}
|
||||
|
||||
func (s *Store) ResetRunningRPMMirrorTasks() error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var now int64
|
||||
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 {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var now int64
|
||||
var oldPrefix string
|
||||
var newPrefix string
|
||||
var err error
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
now = time.Now().UTC().Unix()
|
||||
oldPrefix = oldPath + "/"
|
||||
newPrefix = newPath + "/"
|
||||
@@ -569,9 +569,5 @@ func (s *Store) MoveRPMRepoDir(repoID string, oldPath string, newPath string) er
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return commitIfOwned(tx, owned)
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
}
|
||||
|
||||
func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var grantIDsJSON string
|
||||
@@ -911,7 +911,7 @@ func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSSHAccessProfile(id string) error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var row *sql.Row
|
||||
var secretID sql.NullString
|
||||
@@ -2169,7 +2169,7 @@ func (s *Store) CountSSHFileTransfersFiltered(query string, status string, sessi
|
||||
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 now int64
|
||||
|
||||
@@ -2212,7 +2212,7 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, 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
|
||||
|
||||
item.UpdatedAt = time.Now().UTC().Unix()
|
||||
@@ -2235,7 +2235,7 @@ func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, 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 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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var now int64
|
||||
@@ -538,7 +538,7 @@ func (s *Store) listSSHPrincipalGrantTargets(grantID string) ([]models.SSHPrinci
|
||||
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 result sql.Result
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
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) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var now int64
|
||||
|
||||
@@ -146,7 +146,7 @@ func (s *Store) GetSSHUserCAForUser(id string) (models.SSHUserCA, error) {
|
||||
}
|
||||
|
||||
func (s *Store) NextSSHUserCASerial(id string) (uint64, error) {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var row *sql.Row
|
||||
var serial int64
|
||||
|
||||
+16
-258
@@ -8,196 +8,6 @@ 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 *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) {
|
||||
var settings models.TLSSettings
|
||||
var rows *sql.Rows
|
||||
@@ -261,15 +71,15 @@ func (s *Store) GetTLSSettings() (models.TLSSettings, error) {
|
||||
}
|
||||
|
||||
func (s *Store) SetTLSSettings(settings models.TLSSettings) error {
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
var err error
|
||||
var now int64
|
||||
var endpointPoliciesJSON string
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
now = time.Now().UTC().Unix()
|
||||
_, 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`,
|
||||
@@ -376,12 +186,6 @@ func splitCSVValue(value string) []string {
|
||||
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) {
|
||||
var key models.APIKey
|
||||
var err error
|
||||
@@ -605,48 +409,6 @@ func (s *Store) ListAPIKeysAdmin(subjectType string, query string) ([]models.Adm
|
||||
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 {
|
||||
var err error
|
||||
_, 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 err error
|
||||
var nowUnix int64
|
||||
var tx *sql.Tx
|
||||
var tx txExecutor
|
||||
var owned bool
|
||||
|
||||
if project.ID == "" {
|
||||
id, err = util.NewID()
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
if err != nil { return project, err }
|
||||
project.ID = id
|
||||
}
|
||||
nowUnix = time.Now().UTC().Unix()
|
||||
project.CreatedAt = nowUnix
|
||||
project.UpdatedAt = nowUnix
|
||||
if project.UpdatedBy == "" {
|
||||
project.UpdatedBy = project.CreatedBy
|
||||
}
|
||||
if project.UpdatedBy == "" { project.UpdatedBy = project.CreatedBy }
|
||||
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
if project.HomePage == "" {
|
||||
project.HomePage = "settings"
|
||||
}
|
||||
if err != nil { return project, err }
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`,
|
||||
project.ID,
|
||||
@@ -760,15 +517,16 @@ func (s *Store) CreateProject(project models.Project) (models.Project, error) {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return project, err
|
||||
}
|
||||
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
if err != nil { return project, err }
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateProject(project models.Project) error {
|
||||
var err error
|
||||
|
||||
if project.HomePage == "" {
|
||||
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")
|
||||
}
|
||||
|
||||
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 err error
|
||||
var expires time.Time
|
||||
var cookie *http.Cookie
|
||||
|
||||
token, err = auth.NewSessionToken()
|
||||
if err != nil { return }
|
||||
if err != nil { return err }
|
||||
|
||||
expires = auth.SessionExpiry(api.Cfg)
|
||||
_ = 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{
|
||||
Name: api.sessionCookieName(),
|
||||
Value: token,
|
||||
@@ -558,6 +562,7 @@ func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models
|
||||
Expires: expires,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
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.TOTPSetupRequired = required && !user.TOTPEnabled
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,32 +110,47 @@ func WithStoreTransactionUnbuffered(store *db.Store, next http.Handler) http.Han
|
||||
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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var txStore *db.Store
|
||||
var recorder *bufferedResponseWriter
|
||||
var statusWriter *passThroughStatusWriter
|
||||
var err error
|
||||
|
||||
if isWebSocketUpgrade(r) {
|
||||
// no transaction on websocket upgrade
|
||||
next.ServeHTTP(w, r)
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// arrange to rollback even if the handler panics
|
||||
defer rollbackIfPanic(txStore)
|
||||
|
||||
r = WithStore(r, txStore)
|
||||
if isReadOnlyMethod(r.Method) {
|
||||
defer func() {
|
||||
var rec interface{}
|
||||
rec = recover()
|
||||
if rec != nil {
|
||||
_ = txStore.Rollback()
|
||||
panic(rec)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
err = txStore.Commit()
|
||||
if err != nil {
|
||||
@@ -143,18 +158,11 @@ func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool)
|
||||
return
|
||||
}
|
||||
runAfterCommit(r.Context())
|
||||
return
|
||||
}
|
||||
if !bufferWrites {
|
||||
} else if !bufferWrites {
|
||||
// read-write or write-only. unbufferred.
|
||||
var statusWriter *passThroughStatusWriter
|
||||
|
||||
statusWriter = newPassThroughStatusWriter(w)
|
||||
defer func() {
|
||||
var rec interface{}
|
||||
rec = recover()
|
||||
if rec != nil {
|
||||
_ = txStore.Rollback()
|
||||
panic(rec)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(statusWriter, r)
|
||||
if statusWriter.status >= 400 {
|
||||
_ = txStore.Rollback()
|
||||
@@ -169,40 +177,27 @@ func withStoreTransaction(store *db.Store, next http.Handler, bufferWrites bool)
|
||||
return
|
||||
}
|
||||
runAfterCommit(r.Context())
|
||||
return
|
||||
}
|
||||
recorder = newBufferedResponseWriter()
|
||||
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)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// read-write or write-only. bufferred.
|
||||
var recorder *bufferedResponseWriter
|
||||
|
||||
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")
|
||||
recorder = newBufferedResponseWriter()
|
||||
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 RegisterAfterCommit(ctx context.Context, fn func()) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Block, BlockProperties, BoardFieldValue } from '../api'
|
||||
// --- Constants ----------------------------------------------------------------
|
||||
|
||||
const FALLBACK_COLOR = '#9e9e9e'
|
||||
const OTHER_STATUS_VALUE = '__other__'
|
||||
const CHART_H = 280
|
||||
const M = { top: 16, right: 16, bottom: 56, left: 48 }
|
||||
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}>
|
||||
{getInitials(d.name)}
|
||||
</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}
|
||||
</text>
|
||||
<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 el = (
|
||||
<rect
|
||||
key={seg.statusValue || 'other'}
|
||||
key={seg.statusValue || OTHER_STATUS_VALUE}
|
||||
x={xLeft} y={y} width={bw} height={bh} fill={seg.color} rx={5}
|
||||
onMouseMove={(e) => {
|
||||
if (!containerRef.current) return
|
||||
@@ -601,7 +602,7 @@ function SprintStackedBarChart({ width, data }: { width: number; data: SprintDat
|
||||
<g key={d.sprintValue}>
|
||||
{stacked.map(({ seg, y, h }) => (
|
||||
<rect
|
||||
key={seg.statusValue}
|
||||
key={seg.statusValue || OTHER_STATUS_VALUE}
|
||||
x={x} y={y} width={bw} height={h} fill={seg.color} rx={2}
|
||||
onMouseMove={(e) => {
|
||||
if (!containerRef.current) return
|
||||
@@ -868,6 +869,7 @@ export default function BoardChartView({ blocks, properties, fieldValues }: Boar
|
||||
const [throughputPeriod, setThroughputPeriod] = useState<ThroughputPeriod>('weekly')
|
||||
const statusValues = fieldValues['status'] ?? []
|
||||
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 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,
|
||||
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 sprintData: SprintDatum[] = sprintValues
|
||||
@@ -887,6 +898,15 @@ export default function BoardChartView({ blocks, properties, fieldValues }: Boar
|
||||
count: inSprint.filter((p) => (p.status || '') === sv.value).length,
|
||||
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 }
|
||||
})
|
||||
.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
|
||||
const savedTitleRef = useRef('')
|
||||
const savedDescriptionRef = useRef('')
|
||||
|
||||
const checklistInputRef = useRef<HTMLInputElement>(null)
|
||||
const focusChecklistInputAfterSaveRef = useRef(false)
|
||||
|
||||
const checklistSensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (savingChecklist) return
|
||||
if (!focusChecklistInputAfterSaveRef.current) return
|
||||
|
||||
focusChecklistInputAfterSaveRef.current = false
|
||||
requestAnimationFrame(() => { checklistInputRef.current?.focus() })
|
||||
}, [savingChecklist])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !block) return
|
||||
setTitle(block.title)
|
||||
@@ -272,8 +284,11 @@ export default function CardDetailDialog(props: CardDetailDialogProps) {
|
||||
|
||||
function handleCreateChecklistItem() {
|
||||
if (!block || !newChecklistTitle.trim()) return
|
||||
|
||||
focusChecklistInputAfterSaveRef.current = true
|
||||
setSavingChecklist(true)
|
||||
setChecklistError(null)
|
||||
|
||||
api.createBlockChecklistItem(boardId, block.id, newChecklistTitle.trim())
|
||||
.then((item: BlockChecklistItem) => {
|
||||
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}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 0.5 }}>
|
||||
<TextField
|
||||
inputRef={checklistInputRef}
|
||||
placeholder="Add checklist item..."
|
||||
value={newChecklistTitle}
|
||||
onChange={(e) => setNewChecklistTitle(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleCreateChecklistItem() }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleCreateChecklistItem()
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
size="small"
|
||||
disabled={savingChecklist}
|
||||
|
||||
Reference in New Issue
Block a user