user management added
Test and publish / verify (push) Successful in 7m51s

This commit is contained in:
2026-08-04 22:31:24 +03:30
parent 9380f116d7
commit f6577a24e6
5 changed files with 438 additions and 110 deletions
+177 -30
View File
@@ -32,18 +32,18 @@ type User struct {
}
type Workspace struct {
ID int64
Name string
Slug string
ID int64
Name string
Slug string
CreatedAt string
}
type WorkspaceMember struct {
WorkspaceID int64
UserID int64
Role string
Workspace Workspace
User User
UserID int64
Role string
Workspace Workspace
User User
}
type Attendance struct {
@@ -317,7 +317,9 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
if err := s.ensureColumn("board_tasks", "importance", "importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent'))"); err != nil {
return err
}
if err := s.ensureColumn("board_tasks", "workspace_id", "workspace_id INTEGER NOT NULL DEFAULT 1"); err != nil { return err }
if err := s.ensureColumn("board_tasks", "workspace_id", "workspace_id INTEGER NOT NULL DEFAULT 1"); err != nil {
return err
}
if err := s.ensureBoardTaskTodoStatus(); err != nil {
return err
}
@@ -343,14 +345,22 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
}
_, err = s.db.Exec(`INSERT INTO users(username,password_hash,display_name,email,role) VALUES(?,?,?,?,?)`,
"admin", hash, "Workspace Admin", "admin@localhost", "admin")
if err != nil { return err }
if err != nil {
return err
}
}
var workspaceCount int
if err := s.db.QueryRow(`SELECT COUNT(*) FROM workspaces`).Scan(&workspaceCount); err != nil { return err }
if workspaceCount == 0 {
if _, err := s.db.Exec(`INSERT INTO workspaces(name,slug) VALUES(?,?)`, "Hamkar workspace", "hamkar"); err != nil { return err }
if err := s.db.QueryRow(`SELECT COUNT(*) FROM workspaces`).Scan(&workspaceCount); err != nil {
return err
}
if workspaceCount == 0 {
if _, err := s.db.Exec(`INSERT INTO workspaces(name,slug) VALUES(?,?)`, "Hamkar workspace", "hamkar"); err != nil {
return err
}
}
if _, err := s.db.Exec(`INSERT OR IGNORE INTO workspace_members(workspace_id,user_id,role) SELECT w.id,u.id,u.role FROM workspaces w CROSS JOIN users u`); err != nil {
return err
}
if _, err := s.db.Exec(`INSERT OR IGNORE INTO workspace_members(workspace_id,user_id,role) SELECT w.id,u.id,u.role FROM workspaces w CROSS JOIN users u`); err != nil { return err }
return nil
}
@@ -568,45 +578,89 @@ func (s *Store) Users() ([]User, error) {
func (s *Store) Workspaces(userID int64) ([]Workspace, error) {
rows, err := s.db.Query(`SELECT w.id,w.name,w.slug,w.created_at FROM workspaces w JOIN workspace_members m ON m.workspace_id=w.id WHERE m.user_id=? ORDER BY w.name`, userID)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
defer rows.Close()
var out []Workspace
for rows.Next() { var w Workspace; if err := rows.Scan(&w.ID,&w.Name,&w.Slug,&w.CreatedAt); err != nil { return nil, err }; out = append(out,w) }
for rows.Next() {
var w Workspace
if err := rows.Scan(&w.ID, &w.Name, &w.Slug, &w.CreatedAt); err != nil {
return nil, err
}
out = append(out, w)
}
return out, rows.Err()
}
func (s *Store) AllWorkspaces() ([]Workspace, error) {
rows, err := s.db.Query(`SELECT id,name,slug,created_at FROM workspaces ORDER BY name`); if err != nil { return nil,err }; defer rows.Close()
var out []Workspace; for rows.Next() { var w Workspace; if err:=rows.Scan(&w.ID,&w.Name,&w.Slug,&w.CreatedAt); err!=nil{return nil,err}; out=append(out,w) }; return out,rows.Err()
rows, err := s.db.Query(`SELECT id,name,slug,created_at FROM workspaces ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Workspace
for rows.Next() {
var w Workspace
if err := rows.Scan(&w.ID, &w.Name, &w.Slug, &w.CreatedAt); err != nil {
return nil, err
}
out = append(out, w)
}
return out, rows.Err()
}
func (s *Store) CreateWorkspace(name, slug string, ownerID int64) (int64, error) {
result, err := s.db.Exec(`INSERT INTO workspaces(name,slug) VALUES(?,?)`, strings.TrimSpace(name), strings.TrimSpace(strings.ToLower(slug)))
if err != nil { return 0, err }
id, err := result.LastInsertId(); if err != nil { return 0, err }
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
_, err = s.db.Exec(`INSERT INTO workspace_members(workspace_id,user_id,role) VALUES(?,?, 'admin')`, id, ownerID)
return id, err
}
func (s *Store) WorkspaceMember(workspaceID, userID int64) (WorkspaceMember, error) {
var m WorkspaceMember
err := s.db.QueryRow(`SELECT w.id,w.name,w.slug,w.created_at,u.id,u.username,u.display_name,u.email,u.role,u.avatar_url,u.active,m.role FROM workspaces w JOIN workspace_members m ON m.workspace_id=w.id JOIN users u ON u.id=m.user_id WHERE w.id=? AND u.id=?`, workspaceID,userID).Scan(&m.Workspace.ID,&m.Workspace.Name,&m.Workspace.Slug,&m.Workspace.CreatedAt,&m.User.ID,&m.User.Username,&m.User.DisplayName,&m.User.Email,&m.User.Role,&m.User.AvatarURL,&m.User.Active,&m.Role)
err := s.db.QueryRow(`SELECT w.id,w.name,w.slug,w.created_at,u.id,u.username,u.display_name,u.email,u.role,u.avatar_url,u.active,m.role FROM workspaces w JOIN workspace_members m ON m.workspace_id=w.id JOIN users u ON u.id=m.user_id WHERE w.id=? AND u.id=?`, workspaceID, userID).Scan(&m.Workspace.ID, &m.Workspace.Name, &m.Workspace.Slug, &m.Workspace.CreatedAt, &m.User.ID, &m.User.Username, &m.User.DisplayName, &m.User.Email, &m.User.Role, &m.User.AvatarURL, &m.User.Active, &m.Role)
return m, err
}
func (s *Store) SetWorkspaceMembers(workspaceID int64, userIDs []int64) error {
tx, err := s.db.Begin(); if err != nil { return err }
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err = tx.Exec(`DELETE FROM workspace_members WHERE workspace_id=?`, workspaceID); err != nil { return err }
if _, err = tx.Exec(`DELETE FROM workspace_members WHERE workspace_id=?`, workspaceID); err != nil {
return err
}
for _, userID := range userIDs {
if _, err = tx.Exec(`INSERT INTO workspace_members(workspace_id,user_id,role) SELECT ?,id,CASE WHEN role='admin' THEN 'admin' ELSE 'member' END FROM users WHERE id=?`, workspaceID,userID); err != nil { return err }
if _, err = tx.Exec(`INSERT INTO workspace_members(workspace_id,user_id,role) SELECT ?,id,CASE WHEN role='admin' THEN 'admin' ELSE 'member' END FROM users WHERE id=?`, workspaceID, userID); err != nil {
return err
}
}
return tx.Commit()
}
func (s *Store) WorkspaceMemberIDs(workspaceID int64) (map[int64]bool, error) {
rows, err := s.db.Query(`SELECT user_id FROM workspace_members WHERE workspace_id=?`, workspaceID); if err != nil { return nil, err }; defer rows.Close()
out := map[int64]bool{}; for rows.Next() { var id int64; if err:=rows.Scan(&id); err != nil { return nil,err }; out[id]=true }; return out, rows.Err()
rows, err := s.db.Query(`SELECT user_id FROM workspace_members WHERE workspace_id=?`, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[int64]bool{}
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
out[id] = true
}
return out, rows.Err()
}
func (s *Store) ActiveUsers() ([]User, error) {
@@ -615,8 +669,19 @@ func (s *Store) ActiveUsers() ([]User, error) {
func (s *Store) ActiveUsersInWorkspace(workspaceID int64) ([]User, error) {
rows, err := s.db.Query(`SELECT u.id,u.username,u.display_name,COALESCE(u.email,''),u.role,u.avatar_url,u.active FROM users u JOIN workspace_members m ON m.user_id=u.id WHERE m.workspace_id=? AND u.active=1 ORDER BY u.display_name`, workspaceID)
if err != nil { return nil, err }; defer rows.Close()
var out []User; for rows.Next() { var u User; if err:=rows.Scan(&u.ID,&u.Username,&u.DisplayName,&u.Email,&u.Role,&u.AvatarURL,&u.Active); err!=nil{return nil,err}; out=append(out,u) }; return out,rows.Err()
if err != nil {
return nil, err
}
defer rows.Close()
var out []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.Role, &u.AvatarURL, &u.Active); err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
func (s *Store) users(activeOnly bool) ([]User, error) {
@@ -796,6 +861,74 @@ func (s *Store) SetUserActive(userID, actorID int64, active bool) error {
return tx.Commit()
}
func (s *Store) SetUserPassword(userID int64, password string) error {
if userID < 1 {
return errors.New("account was not found")
}
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
hash, err := hashPassword(password)
if err != nil {
return err
}
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
result, err := tx.Exec(`UPDATE users SET password_hash=? WHERE id=?`, hash, userID)
if err != nil {
return err
}
if count, _ := result.RowsAffected(); count == 0 {
return errors.New("account was not found")
}
if _, err := tx.Exec(`DELETE FROM sessions WHERE user_id=?`, userID); err != nil {
return err
}
return tx.Commit()
}
func (s *Store) DeleteUser(userID, actorID int64) error {
if userID < 1 {
return errors.New("account was not found")
}
if userID == actorID {
return errors.New("you cannot delete your own account")
}
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var role string
if err := tx.QueryRow(`SELECT role FROM users WHERE id=?`, userID).Scan(&role); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.New("account was not found")
}
return err
}
if role == "admin" {
var admins int
if err := tx.QueryRow(`SELECT COUNT(*) FROM users WHERE role='admin'`).Scan(&admins); err != nil {
return err
}
if admins <= 1 {
return errors.New("the last administrator cannot be deleted")
}
}
// Keep historical request records when removing an administrator, while
// clearing the optional reviewer reference that has no cascade action.
if _, err := tx.Exec(`UPDATE requests SET reviewed_by=NULL WHERE reviewed_by=?`, userID); err != nil {
return err
}
if _, err := tx.Exec(`DELETE FROM users WHERE id=?`, userID); err != nil {
return err
}
return tx.Commit()
}
func (s *Store) SetUserAvatar(userID int64, avatarURL string) error {
_, err := s.db.Exec(`UPDATE users SET avatar_url=? WHERE id=?`, avatarURL, userID)
return err
@@ -1163,17 +1296,31 @@ func (s *Store) boardTasks(archived bool, workspaceID int64) ([]BoardTask, error
FROM board_tasks t
JOIN users c ON c.id=t.creator_id`
if archived {
query += ` WHERE t.archived_at IS NOT NULL` + func() string { if workspaceID > 0 { return ` AND t.workspace_id=?` }; return `` }() + `
query += ` WHERE t.archived_at IS NOT NULL` + func() string {
if workspaceID > 0 {
return ` AND t.workspace_id=?`
}
return ``
}() + `
ORDER BY t.archived_at DESC,t.id DESC`
} else {
query += ` WHERE t.archived_at IS NULL` + func() string { if workspaceID > 0 { return ` AND t.workspace_id=?` }; return `` }() + `
query += ` WHERE t.archived_at IS NULL` + func() string {
if workspaceID > 0 {
return ` AND t.workspace_id=?`
}
return ``
}() + `
ORDER BY CASE t.status
WHEN 'backlog' THEN 0 WHEN 'todo' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 ELSE 4 END,
t.created_at DESC`
}
var rows *sql.Rows
var err error
if workspaceID > 0 { rows, err = s.db.Query(query, workspaceID) } else { rows, err = s.db.Query(query) }
if workspaceID > 0 {
rows, err = s.db.Query(query, workspaceID)
} else {
rows, err = s.db.Query(query)
}
if err != nil {
return nil, err
}