package repository import ( "database/sql" "errors" "fmt" "log" "strconv" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/utils" "golang.org/x/crypto/bcrypt" ) // Repository wraps a SQLite database connection. type Repository struct { DB *sql.DB } // New creates a new Repository. func New(db *sql.DB) *Repository { return &Repository{DB: db} } var ErrNotFound = errors.New("not found") var ErrAccountInactive = errors.New("account is not activated") // Init creates the tokens table if it does not exist. func (r *Repository) Init() error { _, err := r.DB.Exec(storage.CreateSqlite) if err != nil { return err } // Migration: add is_active column if it doesn't exist (SQLite ignores errors for duplicate column) _, _ = r.DB.Exec(storage.AddIsActiveColumn) return nil } // CreateToken inserts a new user record with hashed password and generated token. // New users are created with is_active=false by default. func (r *Repository) CreateToken(tc TokenCreate) (string, error) { hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost) if err != nil { return "", err } token, err := utils.RandomToken() if err != nil { return "", err } result, err := r.DB.Exec( `INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tc.Name, tc.LastName, tc.Login, string(hashed), token, tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive, ) if err != nil { return "", err } id, err := result.LastInsertId() if err != nil { return "", err } return strconv.FormatInt(id, 10), nil } // RegisterUser inserts a new user with all permissions set to false and is_active=false. func (r *Repository) RegisterUser(ur UserRegister) (string, error) { hashed, err := bcrypt.GenerateFromPassword([]byte(ur.Password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("hash password: %w", err) } token, err := utils.RandomToken() if err != nil { return "", fmt.Errorf("generate token: %w", err) } result, err := r.DB.Exec( `INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active) VALUES (?, ?, ?, ?, ?, 0, 0, 0, 0)`, ur.Name, ur.LastName, ur.Login, string(hashed), token, ) if err != nil { return "", fmt.Errorf("insert user: %w", err) } id, err := result.LastInsertId() if err != nil { return "", fmt.Errorf("get last insert id: %w", err) } log.Printf("[register] user created: id=%s login=%s", strconv.FormatInt(id, 10), ur.Login) return strconv.FormatInt(id, 10), nil } // Login authenticates by login/password, generates a new token, and returns LoginResponse. func (r *Repository) Login(login, password string) (*LoginResponse, error) { var t Tokens var hashedPassword string err := r.DB.QueryRow( `SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens WHERE login = ?`, login, ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &t.Token, &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil { return nil, ErrNotFound } if !t.IsActive { return nil, ErrAccountInactive } // Generate new token on each login newToken, err := utils.RandomToken() if err != nil { return nil, err } _, err = r.DB.Exec(`UPDATE tokens SET token = ? WHERE id = ?`, newToken, t.ID) if err != nil { return nil, err } return &LoginResponse{ Token: newToken, Name: t.Name, LastName: t.LastName, Login: t.Login, PermissionView: t.PermissionView, PermissionManage: t.PermissionManage, PermissionAdmin: t.PermissionAdmin, IsActive: t.IsActive, }, nil } // GetTokenByToken retrieves a user record by token value. func (r *Repository) GetToken(token string) (*Tokens, error) { var t Tokens err := r.DB.QueryRow( `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens WHERE token = ?`, token, ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &t, nil } // ListTokens returns all users without password and token. func (r *Repository) ListTokens() ([]Tokens, error) { rows, err := r.DB.Query( `SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens`, ) if err != nil { return nil, err } defer rows.Close() var tokens []Tokens for rows.Next() { var t Tokens if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil { return nil, err } tokens = append(tokens, t) } return tokens, rows.Err() } // DeleteToken deletes a user by token value. func (r *Repository) DeleteToken(token string) error { result, err := r.DB.Exec(`DELETE FROM tokens WHERE token = ?`, token) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // DeleteTokenByLogin deletes a user by login. func (r *Repository) DeleteTokenByLogin(login string) error { result, err := r.DB.Exec(`DELETE FROM tokens WHERE login = ?`, login) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // ExistsByLogin checks if a user with given login exists. func (r *Repository) ExistsByLogin(login string) bool { var count int err := r.DB.QueryRow(`SELECT COUNT(*) FROM tokens WHERE login = ?`, login).Scan(&count) if err != nil { return false } return count > 0 } // InitRegistrationTokens creates the registration_tokens table if it does not exist. func (r *Repository) InitRegistrationTokens() error { _, err := r.DB.Exec(storage.CreateRegistrationTokensTable) return err } // CreateRegistrationToken inserts a new one-time registration token. func (r *Repository) CreateRegistrationToken(label string) (string, error) { token, err := utils.RandomToken() if err != nil { return "", err } _, err = r.DB.Exec( `INSERT INTO registration_tokens (token, label, used) VALUES (?, ?, 0)`, token, label, ) if err != nil { return "", err } return token, nil } // GetRegistrationToken retrieves a registration token if it exists and is not used. func (r *Repository) GetRegistrationToken(token string) (*RegistrationToken, error) { var rt RegistrationToken err := r.DB.QueryRow( `SELECT id, token, label, used, created_at, used_at FROM registration_tokens WHERE token = ?`, token, ).Scan(&rt.ID, &rt.Token, &rt.Label, &rt.Used, &rt.CreatedAt, &rt.UsedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &rt, nil } // MarkRegistrationTokenUsed marks a registration token as used. func (r *Repository) MarkRegistrationTokenUsed(token string) error { result, err := r.DB.Exec( `UPDATE registration_tokens SET used = 1, used_at = CURRENT_TIMESTAMP WHERE token = ? AND used = 0`, token, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // ActivateToken activates a user by token value. func (r *Repository) ActivateToken(token string) error { result, err := r.DB.Exec( `UPDATE tokens SET is_active = 1 WHERE token = ?`, token, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // DeactivateToken deactivates a user by token value. func (r *Repository) DeactivateToken(token string) error { result, err := r.DB.Exec( `UPDATE tokens SET is_active = 0 WHERE token = ?`, token, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // ActivateUserByLogin activates a user by login. func (r *Repository) ActivateUserByLogin(login string) error { result, err := r.DB.Exec( `UPDATE tokens SET is_active = 1 WHERE login = ?`, login, ) if err != nil { return fmt.Errorf("activate exec: %w", err) } affected, err := result.RowsAffected() if err != nil { return fmt.Errorf("rows affected: %w", err) } log.Printf("[activate] login=%s affected=%d", login, affected) if affected == 0 { return ErrNotFound } return nil } // DeactivateUserByLogin deactivates a user by login. func (r *Repository) DeactivateUserByLogin(login string) error { result, err := r.DB.Exec( `UPDATE tokens SET is_active = 0 WHERE login = ?`, login, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // ListInactiveTokens returns all users that are not activated. func (r *Repository) ListInactiveTokens() ([]Tokens, error) { rows, err := r.DB.Query( `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens WHERE is_active = 0`, ) if err != nil { return nil, err } defer rows.Close() var tokens []Tokens for rows.Next() { var t Tokens if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil { return nil, err } tokens = append(tokens, t) } return tokens, rows.Err() } // GetTokenByLogin retrieves a user by login. func (r *Repository) GetTokenByLogin(login string) (*Tokens, error) { var t Tokens err := r.DB.QueryRow( `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens WHERE login = ?`, login, ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &t, nil } // UpdateToken updates name and last_name for a user by login. func (r *Repository) UpdateToken(login string, update TokenUpdate) error { result, err := r.DB.Exec( `UPDATE tokens SET name = ?, last_name = ? WHERE login = ?`, update.Name, update.LastName, login, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // UpdatePermissions updates permissions and is_active for a user by login. func (r *Repository) UpdatePermissions(login string, update TokenUpdatePermissions) error { user, err := r.GetTokenByLogin(login) if err != nil { return err } // Use existing values if not provided newView := user.PermissionView newManage := user.PermissionManage newAdmin := user.PermissionAdmin newActive := user.IsActive if update.PermissionView != nil { newView = *update.PermissionView } if update.PermissionManage != nil { newManage = *update.PermissionManage } if update.PermissionAdmin != nil { newAdmin = *update.PermissionAdmin } if update.IsActive != nil { newActive = *update.IsActive } result, err := r.DB.Exec( `UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`, newView, newManage, newAdmin, newActive, login, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // UpdatePassword updates the password for a user by login. func (r *Repository) UpdatePassword(login string, newPassword string) error { hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { return err } result, err := r.DB.Exec( `UPDATE tokens SET password = ? WHERE login = ?`, string(hashed), login, ) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // CreateScript inserts a new script into the database. func (r *Repository) CreateScript(sc ScriptCreate) (*Script, error) { result, err := r.DB.Exec( `INSERT INTO scripts (path, content, interpreter_id) VALUES (?, ?, ?)`, sc.Path, sc.Content, sc.InterpreterID, ) if err != nil { return nil, fmt.Errorf("insert script: %w", err) } id, err := result.LastInsertId() if err != nil { return nil, fmt.Errorf("get last insert id: %w", err) } return &Script{ ID: id, Path: sc.Path, Content: sc.Content, InterpreterID: sc.InterpreterID, }, nil } // GetScript retrieves a script by ID. func (r *Repository) GetScript(id int64) (*Script, error) { var s Script err := r.DB.QueryRow( `SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE id = ?`, id, ).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &s, nil } // GetScriptByPath retrieves a script by its path. func (r *Repository) GetScriptByPath(path string) (*Script, error) { var s Script err := r.DB.QueryRow( `SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE path = ?`, path, ).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &s, nil } // ListScripts returns all scripts. func (r *Repository) ListScripts() ([]Script, error) { rows, err := r.DB.Query( `SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts`, ) if err != nil { return nil, err } defer rows.Close() var scripts []Script for rows.Next() { var s Script if err := rows.Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt); err != nil { return nil, err } scripts = append(scripts, s) } return scripts, rows.Err() } // UpdateScript updates a script by ID. func (r *Repository) UpdateScript(id int64, update ScriptUpdate) (*Script, error) { existing, err := r.GetScript(id) if err != nil { return nil, err } newPath := existing.Path newContent := existing.Content newInterpreterID := existing.InterpreterID if update.Path != nil { newPath = *update.Path } if update.Content != nil { newContent = *update.Content } if update.InterpreterID != nil { newInterpreterID = *update.InterpreterID } _, err = r.DB.Exec( `UPDATE scripts SET path = ?, content = ?, interpreter_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, newPath, newContent, newInterpreterID, id, ) if err != nil { return nil, fmt.Errorf("update script: %w", err) } return &Script{ ID: id, Path: newPath, Content: newContent, InterpreterID: newInterpreterID, }, nil } // DeleteScript deletes a script by ID. func (r *Repository) DeleteScript(id int64) error { result, err := r.DB.Exec(`DELETE FROM scripts WHERE id = ?`, id) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil }