Files
inp2p/internal/store/store.go

941 lines
27 KiB
Go

package store
import (
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
type Store struct {
DB *sql.DB
}
type Tenant struct {
ID int64
Name string
Status int
Subnet string
CreatedAt int64
}
type AuditLog struct {
ID int64 `json:"id"`
ActorType string `json:"actor_type"`
ActorID string `json:"actor_id"`
Action string `json:"action"`
TargetType string `json:"target_type"`
TargetID string `json:"target_id"`
Detail string `json:"detail"`
IP string `json:"ip"`
CreatedAt int64 `json:"created_at"`
}
type User struct {
ID int64
TenantID int64
Role string
Email string
PasswordHash string
Status int
CreatedAt int64
}
type APIKey struct {
ID int64
TenantID int64
Hash string
Scope string
ExpiresAt *int64
Status int
CreatedAt int64
Plain string
}
type NodeCredential struct {
NodeID int64
NodeUUID string
NodeName string
Alias string
Secret string
VirtualIP string
TenantID int64
Status int
CreatedAt int64
LastSeen *int64
}
type EnrollToken struct {
ID int64
TenantID int64
Hash string
ExpiresAt int64
UsedAt *int64
MaxAttempt int
Attempts int
Status int
CreatedAt int64
}
type SessionToken struct {
ID int64
UserID int64
TenantID int64
Role string
TokenHash string
ExpiresAt int64
Status int
CreatedAt int64
}
func Open(dbPath string) (*Store, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}
if _, err := db.Exec(`PRAGMA journal_mode=WAL;`); err != nil {
return nil, err
}
if _, err := db.Exec(`PRAGMA foreign_keys=ON;`); err != nil {
return nil, err
}
s := &Store{DB: db}
if err := s.migrate(); err != nil {
return nil, err
}
if err := s.ensureSubnetPool(); err != nil {
return nil, err
}
if err := s.ensureSettings(); err != nil {
return nil, err
}
if err := s.backfillNodeIdentity(); err != nil {
return nil, err
}
return s, nil
}
func (s *Store) migrate() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS tenants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
status INTEGER NOT NULL DEFAULT 1,
subnet TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL
);`,
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
role TEXT NOT NULL,
email TEXT,
password_hash TEXT,
status INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
);`,
`CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
scope TEXT,
expires_at INTEGER,
status INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
);`,
`CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
node_uuid TEXT,
node_name TEXT NOT NULL,
alias TEXT DEFAULT '',
node_pubkey TEXT,
node_secret_hash TEXT,
virtual_ip TEXT,
status INTEGER NOT NULL DEFAULT 1,
last_seen INTEGER,
created_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
);`,
`CREATE TABLE IF NOT EXISTS enroll_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
expires_at INTEGER NOT NULL,
used_at INTEGER,
max_attempt INTEGER NOT NULL DEFAULT 5,
attempts INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
);`,
`CREATE TABLE IF NOT EXISTS peering_policies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
src_tenant_id INTEGER NOT NULL,
dst_tenant_id INTEGER NOT NULL,
rules TEXT,
expires_at INTEGER,
status INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(src_tenant_id) REFERENCES tenants(id),
FOREIGN KEY(dst_tenant_id) REFERENCES tenants(id)
);`,
`CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_type TEXT,
actor_id TEXT,
action TEXT,
target_type TEXT,
target_id TEXT,
detail TEXT,
ip TEXT,
created_at INTEGER NOT NULL
);`,
`CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at INTEGER NOT NULL
);`,
`CREATE TABLE IF NOT EXISTS subnet_pool (
subnet TEXT PRIMARY KEY,
status INTEGER NOT NULL DEFAULT 0,
reserved INTEGER NOT NULL DEFAULT 0,
tenant_id INTEGER,
updated_at INTEGER NOT NULL
);`,
`CREATE TABLE IF NOT EXISTS session_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
tenant_id INTEGER NOT NULL,
role TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
expires_at INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(tenant_id) REFERENCES tenants(id)
);`,
}
for _, stmt := range stmts {
if _, err := s.DB.Exec(stmt); err != nil {
return err
}
}
if _, err := s.DB.Exec(`ALTER TABLE nodes ADD COLUMN node_uuid TEXT`); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
return err
}
if _, err := s.DB.Exec(`ALTER TABLE nodes ADD COLUMN alias TEXT DEFAULT ''`); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
return err
}
if _, err := s.DB.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_node_uuid ON nodes(node_uuid)`); err != nil {
return err
}
return nil
}
func (s *Store) ensureSubnetPool() error {
// pool: 10.10.1.0/24 .. 10.10.254.0/24
// reserve: 10.10.0.0/24 and 10.10.255.0/24
rows, err := s.DB.Query(`SELECT COUNT(1) FROM subnet_pool;`)
if err != nil {
return err
}
defer rows.Close()
var count int
if rows.Next() {
_ = rows.Scan(&count)
}
if count > 0 {
return nil
}
now := time.Now().Unix()
insert := `INSERT INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`
// reserved
_, _ = s.DB.Exec(insert, "10.10.0.0/24", 0, 1, nil, now)
_, _ = s.DB.Exec(insert, "10.10.255.0/24", 0, 1, nil, now)
for i := 1; i <= 254; i++ {
sn := fmt.Sprintf("10.10.%d.0/24", i)
_, _ = s.DB.Exec(insert, sn, 0, 0, nil, now)
}
return nil
}
func (s *Store) AllocateSubnet() (string, error) {
// find first available subnet
row := s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 ORDER BY subnet LIMIT 1`)
var subnet string
if err := row.Scan(&subnet); err != nil {
return "", err
}
if subnet == "" {
return "", errors.New("no subnet available")
}
return subnet, nil
}
func (s *Store) CreateTenant(name string) (*Tenant, error) {
sn, err := s.AllocateSubnet()
if err != nil {
return nil, err
}
now := time.Now().Unix()
res, err := s.DB.Exec(`INSERT INTO tenants(name,status,subnet,created_at) VALUES(?,?,?,?)`, name, 1, sn, now)
if err != nil {
return nil, err
}
id, _ := res.LastInsertId()
_, _ = s.DB.Exec(`UPDATE subnet_pool SET status=1, tenant_id=?, updated_at=? WHERE subnet=?`, id, now, sn)
return &Tenant{ID: id, Name: name, Status: 1, Subnet: sn, CreatedAt: now}, nil
}
func (s *Store) CreateTenantWithUsers(name, adminPassword, operatorPassword string) (*Tenant, *User, *User, error) {
if adminPassword == "" || operatorPassword == "" {
return nil, nil, nil, errors.New("password required")
}
ten, err := s.CreateTenant(name)
if err != nil {
return nil, nil, nil, err
}
admin, err := s.CreateUser(ten.ID, "admin", "admin@"+name, adminPassword, 1)
if err != nil {
return nil, nil, nil, err
}
op, err := s.CreateUser(ten.ID, "operator", "operator@"+name, operatorPassword, 1)
if err != nil {
return nil, nil, nil, err
}
return ten, admin, op, nil
}
func (s *Store) AllocateNodeIP(tenantID int64) (string, error) {
ten, err := s.GetTenantByID(tenantID)
if err != nil || ten == nil {
return "", errors.New("tenant not found")
}
ip, ipnet, err := net.ParseCIDR(ten.Subnet)
if err != nil {
return "", err
}
base := ip.To4()
if base == nil {
return "", errors.New("only ipv4 subnet supported")
}
rows, err := s.DB.Query(`SELECT virtual_ip FROM nodes WHERE tenant_id=? AND virtual_ip IS NOT NULL AND virtual_ip<>''`, tenantID)
if err != nil {
return "", err
}
defer rows.Close()
used := map[string]bool{}
for rows.Next() {
var v string
if rows.Scan(&v) == nil && v != "" {
used[v] = true
}
}
for host := 2; host <= 254; host++ {
cand := net.IPv4(base[0], base[1], base[2], byte(host)).String()
if !ipnet.Contains(net.ParseIP(cand)) {
continue
}
if used[cand] {
continue
}
return cand, nil
}
return "", errors.New("no available ip")
}
func (s *Store) CreateNodeCredential(tenantID int64, nodeName string) (*NodeCredential, error) {
secret := randToken()
h := hashTokenString(secret)
now := time.Now().Unix()
nodeUUID := randUUID()
vip, err := s.AllocateNodeIP(tenantID)
if err != nil {
return nil, err
}
res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_uuid,node_name,alias,node_secret_hash,virtual_ip,status,last_seen,created_at) VALUES(?,?,?,?,?,?,1,?,?)`, tenantID, nodeUUID, nodeName, "", h, vip, now, now)
if err != nil {
return nil, err
}
id, _ := res.LastInsertId()
return &NodeCredential{NodeID: id, NodeUUID: nodeUUID, NodeName: nodeName, Alias: "", Secret: secret, VirtualIP: vip, TenantID: tenantID, Status: 1, CreatedAt: now, LastSeen: &now}, nil
}
func (s *Store) VerifyNodeSecret(nodeName, secret string) (*Tenant, error) {
h := hashTokenString(secret)
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet,t.created_at FROM nodes n JOIN tenants t ON n.tenant_id=t.id WHERE n.node_name=? AND n.node_secret_hash=? AND n.status=1 AND t.status=1`, nodeName, h)
var t Tenant
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet, &t.CreatedAt); err != nil {
return nil, err
}
return &t, nil
}
func (s *Store) GetNodeCredentialByName(tenantID int64, nodeName string) (*NodeCredential, error) {
row := s.DB.QueryRow(`SELECT id,node_uuid,node_name,alias,virtual_ip,tenant_id,status,last_seen,created_at FROM nodes WHERE tenant_id=? AND node_name=? ORDER BY id DESC LIMIT 1`, tenantID, nodeName)
var n NodeCredential
var seen sql.NullInt64
if err := row.Scan(&n.NodeID, &n.NodeUUID, &n.NodeName, &n.Alias, &n.VirtualIP, &n.TenantID, &n.Status, &seen, &n.CreatedAt); err != nil {
return nil, err
}
if seen.Valid {
v := seen.Int64
n.LastSeen = &v
}
return &n, nil
}
func (s *Store) SetNodeAlias(tenantID int64, nodeUUID, alias string) error {
alias = strings.TrimSpace(alias)
res, err := s.DB.Exec(`UPDATE nodes SET alias=? WHERE tenant_id=? AND node_uuid=?`, alias, tenantID, nodeUUID)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return errors.New("node not found")
}
return nil
}
func (s *Store) SetNodeVirtualIP(tenantID int64, nodeUUID, ip string) error {
ten, err := s.GetTenantByID(tenantID)
if err != nil || ten == nil {
return errors.New("tenant not found")
}
if net.ParseIP(ip) == nil {
return errors.New("invalid ip")
}
_, ipnet, err := net.ParseCIDR(ten.Subnet)
if err != nil {
return err
}
if !ipnet.Contains(net.ParseIP(ip)) {
return errors.New("ip not in subnet")
}
var c int
if err := s.DB.QueryRow(`SELECT COUNT(1) FROM nodes WHERE tenant_id=? AND virtual_ip=? AND node_uuid<>?`, tenantID, ip, nodeUUID).Scan(&c); err != nil {
return err
}
if c > 0 {
return errors.New("ip conflict")
}
res, err := s.DB.Exec(`UPDATE nodes SET virtual_ip=? WHERE tenant_id=? AND node_uuid=?`, ip, tenantID, nodeUUID)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return errors.New("node not found")
}
return nil
}
func (s *Store) GetTenantByToken(token uint64) (*Tenant, error) {
h := hashToken(token)
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet,t.created_at FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1 AND t.status=1`, h)
var t Tenant
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet, &t.CreatedAt); err != nil {
return nil, err
}
return &t, nil
}
func (s *Store) GetTenantByID(id int64) (*Tenant, error) {
row := s.DB.QueryRow(`SELECT id,name,status,subnet,created_at FROM tenants WHERE id=?`, id)
var t Tenant
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet, &t.CreatedAt); err != nil {
return nil, err
}
return &t, nil
}
func (s *Store) CreateAPIKey(tenantID int64, scope string, ttl time.Duration) (string, error) {
token := randToken()
h := hashTokenString(token)
now := time.Now().Unix()
if ttl > 0 {
e := time.Now().Add(ttl)
_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, e.Unix(), now)
return token, err
}
_, err := s.DB.Exec(`INSERT INTO api_keys(tenant_id,key_hash,scope,expires_at,status,created_at) VALUES(?,?,?,?,1,?)`, tenantID, h, scope, nil, now)
return token, err
}
func (s *Store) CreateEnrollToken(tenantID int64, ttl time.Duration, maxAttempt int) (string, error) {
code := randToken()
h := hashTokenString(code)
exp := time.Now().Add(ttl).Unix()
now := time.Now().Unix()
_, err := s.DB.Exec(`INSERT INTO enroll_tokens(tenant_id,token_hash,expires_at,max_attempt,attempts,status,created_at) VALUES(?,?,?,?,0,1,?)`, tenantID, h, exp, maxAttempt, now)
return code, err
}
func (s *Store) ConsumeEnrollToken(code string) (*EnrollToken, error) {
h := hashTokenString(code)
now := time.Now().Unix()
row := s.DB.QueryRow(`SELECT id,tenant_id,expires_at,used_at,max_attempt,attempts,status,created_at FROM enroll_tokens WHERE token_hash=?`, h)
var et EnrollToken
var used sql.NullInt64
if err := row.Scan(&et.ID, &et.TenantID, &et.ExpiresAt, &used, &et.MaxAttempt, &et.Attempts, &et.Status, &et.CreatedAt); err != nil {
return nil, err
}
if used.Valid {
return nil, errors.New("token already used")
}
if et.Status != 1 {
return nil, errors.New("token disabled")
}
if et.Attempts >= et.MaxAttempt {
return nil, errors.New("token attempts exceeded")
}
if now > et.ExpiresAt {
return nil, errors.New("token expired")
}
// mark used
_, err := s.DB.Exec(`UPDATE enroll_tokens SET used_at=?, attempts=attempts+1 WHERE id=?`, now, et.ID)
if err != nil {
return nil, err
}
et.UsedAt = &now
return &et, nil
}
func (s *Store) IncEnrollAttempt(code string) {
h := hashTokenString(code)
_, _ = s.DB.Exec(`UPDATE enroll_tokens SET attempts=attempts+1 WHERE token_hash=?`, h)
}
func (s *Store) ensureSettings() error {
defaults := map[string]string{
"advanced_impersonate": "0",
"advanced_force_network": "0",
"advanced_cross_tenant": "0",
}
now := time.Now().Unix()
for k, v := range defaults {
_, _ = s.DB.Exec(`INSERT OR IGNORE INTO system_settings(key,value,updated_at) VALUES(?,?,?)`, k, v, now)
}
return nil
}
func (s *Store) GetSetting(key string) (string, bool, error) {
row := s.DB.QueryRow(`SELECT value FROM system_settings WHERE key=?`, key)
var v string
if err := row.Scan(&v); err != nil {
return "", false, err
}
return v, true, nil
}
func (s *Store) ListSettings() (map[string]string, error) {
rows, err := s.DB.Query(`SELECT key,value FROM system_settings`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var k, v string
if err := rows.Scan(&k, &v); err == nil {
out[k] = v
}
}
return out, nil
}
func (s *Store) SetSetting(key, value string) error {
now := time.Now().Unix()
_, err := s.DB.Exec(`INSERT INTO system_settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, key, value, now)
return err
}
func (s *Store) AddAuditLog(actorType, actorID, action, targetType, targetID, detail, ip string) error {
now := time.Now().Unix()
_, err := s.DB.Exec(`INSERT INTO audit_logs(actor_type,actor_id,action,target_type,target_id,detail,ip,created_at) VALUES(?,?,?,?,?,?,?,?)`, actorType, actorID, action, targetType, targetID, detail, ip, now)
return err
}
func (s *Store) ListAuditLogs(tenantID int64, limit, offset int) ([]AuditLog, error) {
q := `SELECT id,actor_type,actor_id,action,target_type,target_id,detail,ip,created_at FROM audit_logs`
args := []any{}
if tenantID > 0 {
// limit to logs related to this tenant
q += ` WHERE (target_type='tenant' AND target_id=?)`
args = append(args, fmt.Sprintf("%d", tenantID))
}
q += ` ORDER BY id DESC`
if limit > 0 {
q += ` LIMIT ?`
args = append(args, limit)
}
if offset > 0 {
q += ` OFFSET ?`
args = append(args, offset)
}
rows, err := s.DB.Query(q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := []AuditLog{}
for rows.Next() {
var a AuditLog
if err := rows.Scan(&a.ID, &a.ActorType, &a.ActorID, &a.Action, &a.TargetType, &a.TargetID, &a.Detail, &a.IP, &a.CreatedAt); err == nil {
out = append(out, a)
}
}
return out, nil
}
// ListTenants returns all tenants (admin)
func (s *Store) ListTenants() ([]Tenant, error) {
rows, err := s.DB.Query(`SELECT id,name,status,subnet,created_at FROM tenants ORDER BY id DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Tenant
for rows.Next() {
var t Tenant
if err := rows.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet, &t.CreatedAt); err == nil {
out = append(out, t)
}
}
return out, nil
}
func (s *Store) UpdateTenantStatus(id int64, status int) error {
_, err := s.DB.Exec(`UPDATE tenants SET status=? WHERE id=?`, status, id)
return err
}
func (s *Store) DeleteTenant(id int64) error {
_, err := s.DB.Exec(`DELETE FROM tenants WHERE id=?`, id)
return err
}
// ListAPIKeys returns api keys of a tenant (admin)
func (s *Store) ListAPIKeys(tenantID int64) ([]APIKey, error) {
rows, err := s.DB.Query(`SELECT id,tenant_id,key_hash,scope,expires_at,status,created_at FROM api_keys WHERE tenant_id=? ORDER BY id DESC`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []APIKey
for rows.Next() {
var k APIKey
var exp sql.NullInt64
if err := rows.Scan(&k.ID, &k.TenantID, &k.Hash, &k.Scope, &exp, &k.Status, &k.CreatedAt); err == nil {
if exp.Valid {
v := exp.Int64
k.ExpiresAt = &v
}
out = append(out, k)
}
}
return out, nil
}
func (s *Store) UpdateAPIKeyStatus(id int64, status int) error {
_, err := s.DB.Exec(`UPDATE api_keys SET status=? WHERE id=?`, status, id)
return err
}
func (s *Store) DeleteAPIKey(id int64) error {
_, err := s.DB.Exec(`DELETE FROM api_keys WHERE id=?`, id)
return err
}
// ListEnrollTokens returns enroll tokens for a tenant (admin)
func (s *Store) ListEnrollTokens(tenantID int64) ([]EnrollToken, error) {
rows, err := s.DB.Query(`SELECT id,tenant_id,token_hash,expires_at,used_at,max_attempt,attempts,status,created_at FROM enroll_tokens WHERE tenant_id=? ORDER BY id DESC`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []EnrollToken
for rows.Next() {
var et EnrollToken
var used sql.NullInt64
if err := rows.Scan(&et.ID, &et.TenantID, &et.Hash, &et.ExpiresAt, &used, &et.MaxAttempt, &et.Attempts, &et.Status, &et.CreatedAt); err == nil {
if used.Valid {
v := used.Int64
et.UsedAt = &v
}
out = append(out, et)
}
}
return out, nil
}
func (s *Store) UpdateEnrollStatus(id int64, status int) error {
_, err := s.DB.Exec(`UPDATE enroll_tokens SET status=? WHERE id=?`, status, id)
return err
}
func (s *Store) DeleteEnrollToken(id int64) error {
_, err := s.DB.Exec(`DELETE FROM enroll_tokens WHERE id=?`, id)
return err
}
func hashToken(token uint64) string {
b := make([]byte, 8)
for i := uint(0); i < 8; i++ {
b[7-i] = byte(token >> (i * 8))
}
return hashTokenBytes(b)
}
func hashTokenString(token string) string {
return hashTokenBytes([]byte(token))
}
func (s *Store) VerifyAPIKey(token string) (*Tenant, error) {
h := hashTokenString(token)
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet,t.created_at FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1 AND t.status=1`, h)
var t Tenant
if err := row.Scan(&t.ID, &t.Name, &t.Status, &t.Subnet, &t.CreatedAt); err != nil {
return nil, err
}
return &t, nil
}
func (s *Store) GetUserByTenant(tenantID int64) (*User, error) {
row := s.DB.QueryRow(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE tenant_id=? ORDER BY id LIMIT 1`, tenantID)
var u User
if err := row.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
return &u, nil
}
func (s *Store) GetUserByToken(token string) (*User, error) {
h := hashTokenString(token)
row := s.DB.QueryRow(`SELECT u.id,u.tenant_id,u.role,u.email,u.password_hash,u.status,u.created_at FROM api_keys k JOIN users u ON k.tenant_id=u.tenant_id WHERE k.key_hash=? AND k.status=1`, h)
var u User
if err := row.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
return &u, nil
}
func (s *Store) GetUserByEmail(tenantID int64, email string) (*User, error) {
row := s.DB.QueryRow(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE tenant_id=? AND email=? ORDER BY id LIMIT 1`, tenantID, email)
var u User
if err := row.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
return &u, nil
}
func (s *Store) CreateUser(tenantID int64, role, email, password string, status int) (*User, error) {
now := time.Now().Unix()
var hash string
if password != "" {
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
hash = string(b)
}
res, err := s.DB.Exec(`INSERT INTO users(tenant_id,role,email,password_hash,status,created_at) VALUES(?,?,?,?,?,?)`, tenantID, role, email, hash, status, now)
if err != nil {
return nil, err
}
id, _ := res.LastInsertId()
return &User{ID: id, TenantID: tenantID, Role: role, Email: email, PasswordHash: hash, Status: status, CreatedAt: now}, nil
}
func (s *Store) UserEmailExists(tenantID int64, email string) (bool, error) {
row := s.DB.QueryRow(`SELECT COUNT(1) FROM users WHERE tenant_id=? AND email=?`, tenantID, email)
var c int
if err := row.Scan(&c); err != nil {
return false, err
}
return c > 0, nil
}
func (s *Store) UserEmailExistsGlobal(email string) (bool, error) {
row := s.DB.QueryRow(`SELECT COUNT(1) FROM users WHERE email=?`, email)
var c int
if err := row.Scan(&c); err != nil {
return false, err
}
return c > 0, nil
}
func (s *Store) VerifyUserPassword(tenantID int64, email, password string) (*User, error) {
u, err := s.GetUserByEmail(tenantID, email)
if err != nil {
// compatibility: allow login by role name (admin/operator)
row := s.DB.QueryRow(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE tenant_id=? AND role=? ORDER BY id LIMIT 1`, tenantID, email)
var byRole User
if scanErr := row.Scan(&byRole.ID, &byRole.TenantID, &byRole.Role, &byRole.Email, &byRole.PasswordHash, &byRole.Status, &byRole.CreatedAt); scanErr != nil {
return nil, err
}
u = &byRole
}
if u.PasswordHash == "" {
return nil, errors.New("password not set")
}
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
return nil, errors.New("invalid password")
}
return u, nil
}
func (s *Store) VerifyUserPasswordGlobal(email, password string) (*User, error) {
row := s.DB.QueryRow(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE email=? ORDER BY id LIMIT 1`, email)
var u User
if err := row.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
if u.PasswordHash == "" {
return nil, errors.New("password not set")
}
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
return nil, errors.New("invalid password")
}
return &u, nil
}
func (s *Store) CreateSessionToken(userID, tenantID int64, role string, ttl time.Duration) (string, int64, error) {
tok := randToken()
h := hashTokenString(tok)
now := time.Now().Unix()
exp := time.Now().Add(ttl).Unix()
_, err := s.DB.Exec(`INSERT INTO session_tokens(user_id,tenant_id,role,token_hash,expires_at,status,created_at) VALUES(?,?,?,?,?,1,?)`, userID, tenantID, role, h, exp, now)
if err != nil {
return "", 0, err
}
return tok, exp, nil
}
func (s *Store) VerifySessionToken(token string) (*SessionToken, error) {
h := hashTokenString(token)
now := time.Now().Unix()
row := s.DB.QueryRow(`SELECT id,user_id,tenant_id,role,token_hash,expires_at,status,created_at FROM session_tokens WHERE token_hash=? AND status=1 AND expires_at>?`, h, now)
var st SessionToken
if err := row.Scan(&st.ID, &st.UserID, &st.TenantID, &st.Role, &st.TokenHash, &st.ExpiresAt, &st.Status, &st.CreatedAt); err != nil {
return nil, err
}
return &st, nil
}
func (s *Store) RevokeSessionToken(token string) error {
h := hashTokenString(token)
_, err := s.DB.Exec(`UPDATE session_tokens SET status=0 WHERE token_hash=?`, h)
return err
}
func (s *Store) ListUsers(tenantID int64) ([]User, error) {
rows, err := s.DB.Query(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE tenant_id=? ORDER BY id`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
out := []User{}
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
out = append(out, u)
}
return out, nil
}
func (s *Store) UpdateUserStatus(id int64, status int) error {
_, err := s.DB.Exec(`UPDATE users SET status=? WHERE id=?`, status, id)
return err
}
func (s *Store) UpdateUserPassword(id int64, password string) error {
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = s.DB.Exec(`UPDATE users SET password_hash=? WHERE id=?`, string(b), id)
return err
}
func (s *Store) UpdateUserEmail(id int64, email string) error {
_, err := s.DB.Exec(`UPDATE users SET email=? WHERE id=?`, email, id)
return err
}
func hashTokenBytes(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func randToken() string {
b := make([]byte, 24)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func randUUID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%s-%s-%s-%s-%s",
hex.EncodeToString(b[0:4]),
hex.EncodeToString(b[4:6]),
hex.EncodeToString(b[6:8]),
hex.EncodeToString(b[8:10]),
hex.EncodeToString(b[10:16]),
)
}
func (s *Store) backfillNodeIdentity() error {
rows, err := s.DB.Query(`SELECT id,tenant_id,node_uuid,virtual_ip FROM nodes ORDER BY id`)
if err != nil {
return err
}
defer rows.Close()
type rowNode struct {
id int64
tenantID int64
uuid string
vip string
}
var list []rowNode
for rows.Next() {
var r rowNode
if err := rows.Scan(&r.id, &r.tenantID, &r.uuid, &r.vip); err == nil {
list = append(list, r)
}
}
for _, n := range list {
if n.uuid == "" {
if _, err := s.DB.Exec(`UPDATE nodes SET node_uuid=? WHERE id=?`, randUUID(), n.id); err != nil {
return err
}
}
if strings.TrimSpace(n.vip) == "" {
vip, err := s.AllocateNodeIP(n.tenantID)
if err == nil && vip != "" {
_, _ = s.DB.Exec(`UPDATE nodes SET virtual_ip=? WHERE id=?`, vip, n.id)
}
}
}
return nil
}
// helper to avoid unused import (net)
var _ = net.IPv4len