package store import ( "crypto/rand" "crypto/sha256" "database/sql" "encoding/hex" "errors" "fmt" "net" "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 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 NodeName 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 } 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_name TEXT NOT NULL, 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 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 } } 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@local", adminPassword, 1) if err != nil { return nil, nil, nil, err } op, err := s.CreateUser(ten.ID, "operator", "operator@local", operatorPassword, 1) if err != nil { return nil, nil, nil, err } return ten, admin, op, nil } func (s *Store) CreateNodeCredential(tenantID int64, nodeName string) (*NodeCredential, error) { secret := randToken() h := hashTokenString(secret) now := time.Now().Unix() res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_name,node_secret_hash,status,last_seen,created_at) VALUES(?,?,?,1,?,?)`, tenantID, nodeName, h, now, now) if err != nil { return nil, err } id, _ := res.LastInsertId() return &NodeCredential{NodeID: id, NodeName: nodeName, Secret: secret, 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) 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) } // 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) 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) 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 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) } // helper to avoid unused import (net) var _ = net.IPv4len