feat: user roles - admins and users limited to the machines given to them, user editor with machines, password sign-in and its lock; direct address without Cloudflare as the agents' fallback and in the installer; version 0.1.2

This commit is contained in:
redtea committed 2026-10-15 10:56:19 +03:00
1 parent 36dc12b69d
commit 021da93229
29 files changed
+844 -97

No files matched your search

+129 -20
View File
@@ -120,6 +120,12 @@ CREATE TABLE IF NOT EXISTS forward_rules (
`ALTER TABLE admins ADD COLUMN password_off INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE agents ADD COLUMN transport TEXT NOT NULL DEFAULT 'auto'`,
`ALTER TABLE admins ADD COLUMN pin_hash TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE admins ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`,
`ALTER TABLE admins ADD COLUMN pw_locked INTEGER NOT NULL DEFAULT 0`,
`CREATE TABLE IF NOT EXISTS admin_agents (
admin_id INTEGER NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL,
PRIMARY KEY (admin_id, agent_id))`,
} {
if _, err := db.sql.Exec(alter); err != nil && !strings.Contains(err.Error(), "duplicate column") {
return err
@@ -155,24 +161,56 @@ func (db *DB) CountAdmins() (int, error) {
}
func (db *DB) CreateAdmin(username, passHash string) error {
_, err := db.sql.Exec(
`INSERT INTO admins(username, pass_hash, created_at) VALUES(?, ?, ?)`,
username, passHash, time.Now().Unix())
_, err := db.CreateUser(username, passHash, RoleAdmin)
return err
}
// Admin is one row of the admins table.
// CreateUser adds a panel user with the role given and returns its id.
func (db *DB) CreateUser(username, passHash, role string) (int64, error) {
res, err := db.sql.Exec(
`INSERT INTO admins(username, pass_hash, created_at, role) VALUES(?, ?, ?, ?)`,
username, passHash, time.Now().Unix(), role)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// Roles of panel users: an admin sees and runs everything; a user only the
// machines given to them, and none of the settings.
const (
RoleAdmin = "admin"
RoleUser = "user"
)
// Admin is one row of the admins table: a panel user, an admin or not.
type Admin struct {
ID int64
Username string
PassHash string
Role string
// PwLocked keeps the user from turning the password sign-in on or off
// themselves: an admin set it.
PwLocked bool
}
// IsAdmin reports whether the user has the run of the whole panel.
func (a *Admin) IsAdmin() bool { return a != nil && a.Role != RoleUser }
const adminCols = `id, username, pass_hash, role, pw_locked`
func scanAdmin(row interface{ Scan(...any) error }) (*Admin, error) {
a := &Admin{}
var locked int
if err := row.Scan(&a.ID, &a.Username, &a.PassHash, &a.Role, &locked); err != nil {
return nil, err
}
a.PwLocked = locked == 1
return a, nil
}
func (db *DB) AdminByUsername(username string) (*Admin, error) {
a := &Admin{}
err := db.sql.QueryRow(
`SELECT id, username, pass_hash FROM admins WHERE username = ?`, username).
Scan(&a.ID, &a.Username, &a.PassHash)
a, err := scanAdmin(db.sql.QueryRow(`SELECT `+adminCols+` FROM admins WHERE username = ?`, username))
if err == sql.ErrNoRows {
return nil, nil
}
@@ -180,22 +218,87 @@ func (db *DB) AdminByUsername(username string) (*Admin, error) {
}
func (db *DB) ListAdmins() ([]Admin, error) {
rows, err := db.sql.Query(`SELECT id, username FROM admins ORDER BY username`)
rows, err := db.sql.Query(`SELECT ` + adminCols + ` FROM admins ORDER BY role, username`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Admin
for rows.Next() {
var a Admin
if err := rows.Scan(&a.ID, &a.Username); err != nil {
a, err := scanAdmin(rows)
if err != nil {
return nil, err
}
out = append(out, a)
out = append(out, *a)
}
return out, rows.Err()
}
// CountRole is how many users have the role.
func (db *DB) CountRole(role string) int {
var n int
_ = db.sql.QueryRow(`SELECT COUNT(*) FROM admins WHERE role = ?`, role).Scan(&n)
return n
}
func (db *DB) SetUserRole(id int64, role string) error {
_, err := db.sql.Exec(`UPDATE admins SET role = ? WHERE id = ?`, role, id)
return err
}
func (db *DB) SetPwLocked(id int64, locked bool) error {
_, err := db.sql.Exec(`UPDATE admins SET pw_locked = ? WHERE id = ?`, boolInt(locked), id)
return err
}
// UserAgents are the machines a user may use.
func (db *DB) UserAgents(adminID int64) map[string]bool {
out := map[string]bool{}
rows, err := db.sql.Query(`SELECT agent_id FROM admin_agents WHERE admin_id = ?`, adminID)
if err != nil {
return out
}
defer rows.Close()
for rows.Next() {
var id string
if rows.Scan(&id) == nil {
out[id] = true
}
}
return out
}
// SetUserAgents gives the user exactly these machines.
func (db *DB) SetUserAgents(adminID int64, agentIDs []string) error {
tx, err := db.sql.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM admin_agents WHERE admin_id = ?`, adminID); err != nil {
return err
}
for _, id := range agentIDs {
if _, err := tx.Exec(`INSERT OR IGNORE INTO admin_agents(admin_id, agent_id) VALUES(?, ?)`, adminID, id); err != nil {
return err
}
}
return tx.Commit()
}
// CanUseAgent reports whether the user may use the machine.
func (db *DB) CanUseAgent(a *Admin, agentID string) bool {
if a == nil {
return false
}
if a.IsAdmin() {
return true
}
var n int
_ = db.sql.QueryRow(`SELECT COUNT(*) FROM admin_agents WHERE admin_id = ? AND agent_id = ?`, a.ID, agentID).Scan(&n)
return n > 0
}
func (db *DB) DeleteAdmin(id int64) error {
_, err := db.sql.Exec(`DELETE FROM admins WHERE id = ?`, id)
return err
@@ -214,9 +317,7 @@ type AdminKey struct {
}
func (db *DB) AdminByID(id int64) (*Admin, error) {
a := &Admin{}
err := db.sql.QueryRow(`SELECT id, username, pass_hash FROM admins WHERE id = ?`, id).
Scan(&a.ID, &a.Username, &a.PassHash)
a, err := scanAdmin(db.sql.QueryRow(`SELECT `+adminCols+` FROM admins WHERE id = ?`, id))
if err == sql.ErrNoRows {
return nil, nil
}
@@ -338,12 +439,10 @@ func (db *DB) CreateSession(id string, adminID int64, ttl time.Duration) error {
// SessionAdmin returns the admin for a live (non-expired) session, or nil.
func (db *DB) SessionAdmin(sessionID string) (*Admin, error) {
a := &Admin{}
err := db.sql.QueryRow(
`SELECT a.id, a.username, a.pass_hash
a, err := scanAdmin(db.sql.QueryRow(
`SELECT a.id, a.username, a.pass_hash, a.role, a.pw_locked
FROM admin_sessions s JOIN admins a ON a.id = s.admin_id
WHERE s.id = ? AND s.expires_at > ?`, sessionID, time.Now().Unix()).
Scan(&a.ID, &a.Username, &a.PassHash)
WHERE s.id = ? AND s.expires_at > ?`, sessionID, time.Now().Unix()))
if err == sql.ErrNoRows {
return nil, nil
}
@@ -455,6 +554,9 @@ func (db *DB) SetAgentLoginUser(id, user string) error {
func (db *DB) DeleteAgent(id string) error {
_, err := db.sql.Exec(`DELETE FROM agents WHERE id = ?`, id)
if err == nil {
_, _ = db.sql.Exec(`DELETE FROM admin_agents WHERE agent_id = ?`, id)
}
return err
}
@@ -631,3 +733,10 @@ func (db *DB) Audit(adminID int64, agentID, action string) {
`INSERT INTO audit(admin_id, agent_id, action, at) VALUES(?, ?, ?, ?)`,
adminID, agentID, action, time.Now().Unix())
}
func boolInt(b bool) int {
if b {
return 1
}
return 0
}