You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

572 lines
13 KiB

package store
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"os"
"path/filepath"
"sort"
"strings"
"time"
_ "modernc.org/sqlite"
)
var ErrDependencyExists = errors.New("group is assigned to one or more users")
type SQLiteStore struct {
db *sql.DB
}
type ActivityRecord struct {
ID int64
Kind string
Path string
Title string
Action string
CreatedAt time.Time
}
type UserRecord struct {
ID int64
Username string
GroupName string
Enabled bool
PasswordHash string
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateUserParams struct {
Username string
Password string
GroupName string
Enabled bool
}
type UpdateUserParams struct {
ID int64
Username string
Password string
GroupName string
Enabled bool
}
type GroupRecord struct {
ID int64
Name string
Description string
Permissions []string
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateGroupParams struct {
Name string
Description string
Permissions []string
}
type UpdateGroupParams struct {
ID int64
Name string
Description string
Permissions []string
}
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}
store := &SQLiteStore{db: db}
if err := store.init(); err != nil {
_ = db.Close()
return nil, err
}
return store, nil
}
func (s *SQLiteStore) Close() error {
if s == nil || s.db == nil {
return nil
}
return s.db.Close()
}
func (s *SQLiteStore) RecordPageVisit(path string, title string) error {
_, err := s.db.Exec(`
INSERT INTO activity_logs (kind, path, title, action)
VALUES ('page_visit', ?, ?, '')
`, path, title)
return err
}
func (s *SQLiteStore) RecordMockAction(action string, path string) error {
_, err := s.db.Exec(`
INSERT INTO activity_logs (kind, path, title, action)
VALUES ('mock_action', ?, '', ?)
`, path, action)
return err
}
func (s *SQLiteStore) RecentActivity(limit int) ([]ActivityRecord, error) {
if limit <= 0 {
limit = 20
}
rows, err := s.db.Query(`
SELECT id, kind, path, title, action, created_at
FROM activity_logs
ORDER BY id DESC
LIMIT ?
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
records := make([]ActivityRecord, 0, limit)
for rows.Next() {
var record ActivityRecord
if err := rows.Scan(&record.ID, &record.Kind, &record.Path, &record.Title, &record.Action, &record.CreatedAt); err != nil {
return nil, err
}
records = append(records, record)
}
return records, rows.Err()
}
func (s *SQLiteStore) ListUsers() ([]UserRecord, error) {
rows, err := s.db.Query(`
SELECT id, username, group_name, enabled, created_at, updated_at
FROM users
ORDER BY username COLLATE NOCASE ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
users := make([]UserRecord, 0)
for rows.Next() {
var user UserRecord
if err := rows.Scan(&user.ID, &user.Username, &user.GroupName, &user.Enabled, &user.CreatedAt, &user.UpdatedAt); err != nil {
return nil, err
}
users = append(users, user)
}
return users, rows.Err()
}
func (s *SQLiteStore) GetUser(id int64) (UserRecord, error) {
var user UserRecord
err := s.db.QueryRow(`
SELECT id, username, group_name, enabled, created_at, updated_at
FROM users
WHERE id = ?
`, id).Scan(&user.ID, &user.Username, &user.GroupName, &user.Enabled, &user.CreatedAt, &user.UpdatedAt)
return user, err
}
func (s *SQLiteStore) GetUserByUsername(username string) (UserRecord, error) {
var user UserRecord
err := s.db.QueryRow(`
SELECT id, username, group_name, enabled, password_hash, created_at, updated_at
FROM users
WHERE username = ?
`, strings.TrimSpace(username)).Scan(
&user.ID,
&user.Username,
&user.GroupName,
&user.Enabled,
&user.PasswordHash,
&user.CreatedAt,
&user.UpdatedAt,
)
return user, err
}
func (s *SQLiteStore) ListGroups() ([]GroupRecord, error) {
rows, err := s.db.Query(`
SELECT id, name, description, permissions, created_at, updated_at
FROM groups
ORDER BY name COLLATE NOCASE ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
groups := make([]GroupRecord, 0)
for rows.Next() {
var group GroupRecord
var permissions string
if err := rows.Scan(&group.ID, &group.Name, &group.Description, &permissions, &group.CreatedAt, &group.UpdatedAt); err != nil {
return nil, err
}
group.Permissions = decodePermissions(permissions)
groups = append(groups, group)
}
return groups, rows.Err()
}
func (s *SQLiteStore) GetGroup(id int64) (GroupRecord, error) {
var group GroupRecord
var permissions string
err := s.db.QueryRow(`
SELECT id, name, description, permissions, created_at, updated_at
FROM groups
WHERE id = ?
`, id).Scan(&group.ID, &group.Name, &group.Description, &permissions, &group.CreatedAt, &group.UpdatedAt)
if err != nil {
return GroupRecord{}, err
}
group.Permissions = decodePermissions(permissions)
return group, nil
}
func (s *SQLiteStore) GetGroupByName(name string) (GroupRecord, error) {
var group GroupRecord
var permissions string
err := s.db.QueryRow(`
SELECT id, name, description, permissions, created_at, updated_at
FROM groups
WHERE name = ?
`, strings.TrimSpace(name)).Scan(&group.ID, &group.Name, &group.Description, &permissions, &group.CreatedAt, &group.UpdatedAt)
if err != nil {
return GroupRecord{}, err
}
group.Permissions = decodePermissions(permissions)
return group, nil
}
func (s *SQLiteStore) CreateGroup(params CreateGroupParams) (GroupRecord, error) {
result, err := s.db.Exec(`
INSERT INTO groups (name, description, permissions)
VALUES (?, ?, ?)
`, strings.TrimSpace(params.Name), strings.TrimSpace(params.Description), encodePermissions(params.Permissions))
if err != nil {
return GroupRecord{}, err
}
id, err := result.LastInsertId()
if err != nil {
return GroupRecord{}, err
}
return s.GetGroup(id)
}
func (s *SQLiteStore) UpdateGroup(params UpdateGroupParams) (GroupRecord, error) {
current, err := s.GetGroup(params.ID)
if err != nil {
return GroupRecord{}, err
}
tx, err := s.db.Begin()
if err != nil {
return GroupRecord{}, err
}
if _, err := tx.Exec(`
UPDATE groups
SET name = ?, description = ?, permissions = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, strings.TrimSpace(params.Name), strings.TrimSpace(params.Description), encodePermissions(params.Permissions), params.ID); err != nil {
_ = tx.Rollback()
return GroupRecord{}, err
}
if current.Name != strings.TrimSpace(params.Name) {
if _, err := tx.Exec(`
UPDATE users
SET group_name = ?, updated_at = CURRENT_TIMESTAMP
WHERE group_name = ?
`, strings.TrimSpace(params.Name), current.Name); err != nil {
_ = tx.Rollback()
return GroupRecord{}, err
}
}
if err := tx.Commit(); err != nil {
return GroupRecord{}, err
}
return s.GetGroup(params.ID)
}
func (s *SQLiteStore) DeleteGroup(id int64) error {
group, err := s.GetGroup(id)
if err != nil {
return err
}
var userCount int
if err := s.db.QueryRow(`
SELECT COUNT(*)
FROM users
WHERE group_name = ?
`, group.Name).Scan(&userCount); err != nil {
return err
}
if userCount > 0 {
return ErrDependencyExists
}
result, err := s.db.Exec(`DELETE FROM groups WHERE id = ?`, id)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *SQLiteStore) CreateUser(params CreateUserParams) (UserRecord, error) {
result, err := s.db.Exec(`
INSERT INTO users (username, password_hash, group_name, enabled)
VALUES (?, ?, ?, ?)
`, strings.TrimSpace(params.Username), hashPassword(params.Password), strings.TrimSpace(params.GroupName), params.Enabled)
if err != nil {
return UserRecord{}, err
}
id, err := result.LastInsertId()
if err != nil {
return UserRecord{}, err
}
return s.GetUser(id)
}
func (s *SQLiteStore) UpdateUser(params UpdateUserParams) (UserRecord, error) {
if strings.TrimSpace(params.Password) == "" {
result, err := s.db.Exec(`
UPDATE users
SET username = ?, group_name = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, strings.TrimSpace(params.Username), strings.TrimSpace(params.GroupName), params.Enabled, params.ID)
if err != nil {
return UserRecord{}, err
}
rows, err := result.RowsAffected()
if err != nil {
return UserRecord{}, err
}
if rows == 0 {
return UserRecord{}, sql.ErrNoRows
}
return s.GetUser(params.ID)
}
result, err := s.db.Exec(`
UPDATE users
SET username = ?, password_hash = ?, group_name = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, strings.TrimSpace(params.Username), hashPassword(params.Password), strings.TrimSpace(params.GroupName), params.Enabled, params.ID)
if err != nil {
return UserRecord{}, err
}
rows, err := result.RowsAffected()
if err != nil {
return UserRecord{}, err
}
if rows == 0 {
return UserRecord{}, sql.ErrNoRows
}
return s.GetUser(params.ID)
}
func (s *SQLiteStore) DeleteUser(id int64) error {
result, err := s.db.Exec(`DELETE FROM users WHERE id = ?`, id)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *SQLiteStore) init() error {
_, err := s.db.Exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS activity_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
path TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
action TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
group_name TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
permissions TEXT NOT NULL DEFAULT '[]',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`)
if err != nil {
return err
}
if err := s.ensureDefaultGroups(); err != nil {
return err
}
return s.ensureDefaultUsers()
}
func (s *SQLiteStore) ensureDefaultGroups() error {
defaultGroups := []CreateGroupParams{
{Name: "administrator", Description: "전체 접근 가능", Permissions: defaultPermissionKeys()},
{Name: "operator", Description: "운영 담당 권한 그룹", Permissions: []string{"network", "ports", "admin", "common", "status", "password"}},
{Name: "guest", Description: "조회 전용", Permissions: []string{"status"}},
}
tx, err := s.db.Begin()
if err != nil {
return err
}
for _, group := range defaultGroups {
if _, err := tx.Exec(`
INSERT OR IGNORE INTO groups (name, description, permissions)
VALUES (?, ?, ?)
`, group.Name, group.Description, encodePermissions(group.Permissions)); err != nil {
_ = tx.Rollback()
return err
}
}
return tx.Commit()
}
func (s *SQLiteStore) ensureDefaultUsers() error {
defaultUsers := []CreateUserParams{
{Username: "terauser", Password: "terauser", GroupName: "administrator", Enabled: true},
{Username: "guest", Password: "guest", GroupName: "guest", Enabled: true},
{Username: "admin", Password: "admin", GroupName: "administrator", Enabled: true},
}
tx, err := s.db.Begin()
if err != nil {
return err
}
for _, user := range defaultUsers {
if _, err := tx.Exec(`
INSERT OR IGNORE INTO users (username, password_hash, group_name, enabled)
VALUES (?, ?, ?, ?)
`, user.Username, hashPassword(user.Password), user.GroupName, user.Enabled); err != nil {
_ = tx.Rollback()
return err
}
}
return tx.Commit()
}
func hashPassword(password string) string {
sum := sha256.Sum256([]byte(password))
return hex.EncodeToString(sum[:])
}
func VerifyPassword(password string, encoded string) bool {
return hashPassword(password) == encoded
}
func IsUniqueConstraintError(err error) bool {
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
}
func IsNotFoundError(err error) bool {
return errors.Is(err, sql.ErrNoRows)
}
func IsDependencyError(err error) bool {
return errors.Is(err, ErrDependencyExists)
}
func encodePermissions(permissions []string) string {
if len(permissions) == 0 {
return "[]"
}
normalized := normalizePermissions(permissions)
payload, err := json.Marshal(normalized)
if err != nil {
return "[]"
}
return string(payload)
}
func decodePermissions(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
var permissions []string
if err := json.Unmarshal([]byte(raw), &permissions); err != nil {
return nil
}
return normalizePermissions(permissions)
}
func normalizePermissions(permissions []string) []string {
seen := make(map[string]struct{})
result := make([]string, 0, len(permissions))
for _, permission := range permissions {
permission = strings.TrimSpace(permission)
if permission == "" {
continue
}
if _, ok := seen[permission]; ok {
continue
}
seen[permission] = struct{}{}
result = append(result, permission)
}
sort.Strings(result)
return result
}
func defaultPermissionKeys() []string {
return []string{"admin", "common", "login", "network", "operate", "password", "ports", "status", "tools"}
}