Compare commits

...

5 Commits

Author SHA1 Message Date
hyung-hwan 6c4572d27f some db/transaction code refactored 2026-06-19 23:06:44 +09:00
hyung-hwan 858afe5371 refactored withStoreTransaction() by removing duplicate code 2026-06-19 19:00:03 +09:00
hyung-hwan 11d2d80d77 updated codit-data-browser to use the common routine for db access 2026-06-19 15:45:17 +09:00
hyung-hwan e1c056ebc4 checklist input enhancement 2026-06-19 08:00:16 +09:00
hyung-hwan 3a2d962472 enhanced CreateSession failure.
enhanced to handle unknown status or sprint in the boardchartview
2026-06-18 21:54:50 +09:00
24 changed files with 577 additions and 470 deletions
+5 -1
View File
@@ -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),
+6 -14
View File
@@ -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 = ?,
+2 -2
View File
@@ -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
+8 -8
View File
@@ -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
View File
@@ -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()
}
+29 -30
View File
@@ -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)
+3 -3
View File
@@ -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)
+2 -2
View File
@@ -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
+3 -4
View File
@@ -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(`
+8 -9
View File
@@ -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
}
+9 -13
View File
@@ -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)
}
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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"
}
+245
View File
@@ -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
}
+8 -3
View File
@@ -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) {
+5 -1
View File
@@ -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)
}
+5 -1
View File
@@ -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)
}
+50 -55
View File
@@ -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()) {
+23 -3
View File
@@ -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)
+22 -1
View File
@@ -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}