feat: first working version
This commit is contained in:
parent
a67e208d6e
commit
1029563cb1
11 changed files with 585 additions and 0 deletions
54
internal/auth/auth.go
Normal file
54
internal/auth/auth.go
Normal 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
53
internal/config/config.go
Normal 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
170
internal/db/sqlite.go
Normal 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
70
internal/ui/console.go
Normal 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
26
internal/utils/hash.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue