feat: first working version

This commit is contained in:
nihonium 2026-01-14 12:55:21 +03:00
parent a67e208d6e
commit 1029563cb1
11 changed files with 585 additions and 0 deletions

54
internal/auth/auth.go Normal file
View file

@ -0,0 +1,54 @@
package auth
import (
"errors"
"linux-auth/internal/db"
"linux-auth/internal/utils"
)
/**
* @brief Проверка аутентификации пользователя
*
* Сравнивает введённый пароль с хешем из БД.
* Учитывает блокировку пользователя и количество неудачных попыток.
*
* @param username Логин пользователя
* @param password Введённый пароль
* @return true если доступ разрешён, false если логин/пароль неверны
* @return ошибка если произошла системная ошибка (например, БД)
*/
func Authenticate(username, password string) (bool, error) {
user, err := db.GetUser(username)
if err != nil {
// Пользователь не найден
return false, nil
}
// Проверка блокировки
if user.Locked {
return false, errors.New("пользователь заблокирован")
}
// Проверка пароля
if !utils.CheckPassword(password, user.PasswordHash) {
// Инкрементируем счётчик неудачных попыток
err := db.IncrementFail(username)
if err != nil {
return false, err
}
// Если достигнут максимум — блокируем
const MaxAttempts = 3 // Можно передавать через config.toml
user.FailedAttempts++
if user.FailedAttempts >= MaxAttempts {
_ = db.LockUser(username)
}
return false, nil
}
// Успешный вход — сбрасываем счётчик неудачных попыток
_ = db.ResetFails(username)
return true, nil
}

53
internal/config/config.go Normal file
View file

@ -0,0 +1,53 @@
package config
import (
"os"
"github.com/pelletier/go-toml/v2"
)
/**
* @brief Структура конфигурации приложения
*/
type Config struct {
App struct {
Name string `toml:"name"`
MaxLoginAttempts int `toml:"max_login_attempts"`
LockTimeoutSec int `toml:"lock_timeout_sec"`
} `toml:"app"`
Database struct {
Type string `toml:"type"`
Path string `toml:"path"`
} `toml:"database"`
Security struct {
PasswordHash string `toml:"password_hash"`
} `toml:"security"`
UI struct {
ShowWelcome bool `toml:"show_welcome"`
} `toml:"ui"`
}
/**
* @brief Загрузка конфигурации из TOML файла
*
* @param path путь к файлу конфигурации
* @return cfg указатель на структуру Config
* @return error ошибка при загрузке
*/
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
err = toml.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

170
internal/db/sqlite.go Normal file
View file

@ -0,0 +1,170 @@
package db
import (
"database/sql"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
/**
* @brief Структура пользователя
*/
type User struct {
ID int
Username string
PasswordHash string
FailedAttempts int
Locked bool
}
var database *sql.DB
/**
* @brief Инициализация соединения с SQLite БД
* @param path Путь к файлу базы данных
* @return ошибка при неудаче
*/
func Init(path string) error {
var err error
database, err = sql.Open("sqlite3", path)
if err != nil {
return err
}
// Проверка соединения
if err = database.Ping(); err != nil {
return err
}
return createTables()
}
/**
* @brief Закрытие соединения с БД
*/
func Close() {
if database != nil {
_ = database.Close()
}
}
/**
* @brief Создание таблиц при первом запуске
*/
func createTables() error {
query := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
failed_attempts INTEGER DEFAULT 0,
locked INTEGER DEFAULT 0
);
`
_, err := database.Exec(query)
return err
}
/**
* @brief Получить пользователя по имени
* @param username Логин пользователя
* @return структура User или ошибка
*/
func GetUser(username string) (*User, error) {
query := `
SELECT id, username, password_hash, failed_attempts, locked
FROM users
WHERE username = ?;
`
row := database.QueryRow(query, username)
var user User
var locked int
err := row.Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.FailedAttempts,
&locked,
)
if err == sql.ErrNoRows {
return nil, errors.New("пользователь не найден")
}
if err != nil {
return nil, err
}
user.Locked = locked != 0
return &user, nil
}
/**
* @brief Увеличить счётчик неудачных попыток входа
* @param username Логин пользователя
*/
func IncrementFail(username string) error {
query := `
UPDATE users
SET failed_attempts = failed_attempts + 1
WHERE username = ?;
`
_, err := database.Exec(query, username)
return err
}
/**
* @brief Сбросить счётчик неудачных попыток
* @param username Логин пользователя
*/
func ResetFails(username string) error {
query := `
UPDATE users
SET failed_attempts = 0
WHERE username = ?;
`
_, err := database.Exec(query, username)
return err
}
/**
* @brief Заблокировать пользователя
* @param username Логин пользователя
*/
func LockUser(username string) error {
query := `
UPDATE users
SET locked = 1
WHERE username = ?;
`
_, err := database.Exec(query)
return err
}
/**
* @brief Добавить нового пользователя (для инициализации)
* @param username Логин
* @param passwordHash Хеш пароля
*/
func CreateUser(username, passwordHash string) error {
query := `
INSERT INTO users (username, password_hash)
VALUES (?, ?);
`
_, err := database.Exec(query, username, passwordHash)
if err != nil {
return fmt.Errorf("не удалось создать пользователя: %w", err)
}
return nil
}

70
internal/ui/console.go Normal file
View file

@ -0,0 +1,70 @@
package ui
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"golang.org/x/term"
)
var reader = bufio.NewReader(os.Stdin)
/**
* @brief Считывание логина и пароля с консоли
*
* Логин вводится обычным текстом, пароль с маской.
*
* @return login введённый логин
* @return password введённый пароль
* @return err ошибка при вводе
*/
func ReadCredentials() (login string, password string, err error) {
// Ввод логина
fmt.Print("Логин: ")
login, err = reader.ReadString('\n')
if err != nil {
return "", "", err
}
login = strings.TrimSpace(login)
// Ввод пароля с маской
fmt.Print("Пароль: ")
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", "", err
}
password = strings.TrimSpace(string(bytePassword))
return login, password, nil
}
/**
* @brief Предложить пользователю очистку формы
*
* Пользователь может ввести "y" или "n".
*
* @return true если пользователь выбрал сброс
*/
func AskReset() bool {
for {
fmt.Print("Очистить форму и попробовать снова? (y/n): ")
resp, err := reader.ReadString('\n')
if err != nil {
return false
}
resp = strings.ToLower(strings.TrimSpace(resp))
if resp == "y" || resp == "yes" {
return true
} else if resp == "n" || resp == "no" {
return false
} else {
fmt.Println("Введите y или n.")
}
}
}

26
internal/utils/hash.go Normal file
View file

@ -0,0 +1,26 @@
package utils
import (
"crypto/sha256"
"encoding/hex"
)
/**
* @brief Вычислить SHA-256 хеш пароля
* @param password Строка с паролем
* @return Хеш в виде строки HEX
*/
func HashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
/**
* @brief Проверить совпадение пароля и хеша
* @param password Введённый пароль
* @param hash Хеш пароля из БД
* @return true если совпадает
*/
func CheckPassword(password, hash string) bool {
return HashPassword(password) == hash
}