mirror of
https://git.evilfox.cc/test2/wg.git
synced 2026-07-31 20:03:22 +00:00
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/evilfox/wg-panel/internal/config"
|
|
"github.com/evilfox/wg-panel/internal/models"
|
|
"github.com/gorilla/sessions"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
func init() {
|
|
gob.Register(int64(0))
|
|
}
|
|
|
|
const SessionName = "wg_session"
|
|
|
|
type Store struct {
|
|
Sessions *sessions.CookieStore
|
|
DB *pgxpool.Pool
|
|
Cfg config.Config
|
|
}
|
|
|
|
func NewStore(cfg config.Config, db *pgxpool.Pool) *Store {
|
|
store := sessions.NewCookieStore([]byte(cfg.SessionSecret))
|
|
store.Options = &sessions.Options{
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: cfg.SecureCookies,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 0,
|
|
}
|
|
return &Store{Sessions: store, DB: db, Cfg: cfg}
|
|
}
|
|
|
|
func (s *Store) EnsureAdmin(ctx context.Context) error {
|
|
if s.Cfg.AdminPassword == "" {
|
|
return errors.New("ADMIN_PASSWORD is required in environment")
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(s.Cfg.AdminPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var id int64
|
|
err = s.DB.QueryRow(ctx, `SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1`).Scan(&id)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
_, err = s.DB.Exec(ctx, `
|
|
INSERT INTO users (username, email, password_hash, role, is_approved)
|
|
VALUES ($1, $2, $3, 'admin', TRUE)
|
|
ON CONFLICT (username) DO UPDATE
|
|
SET email = EXCLUDED.email,
|
|
password_hash = EXCLUDED.password_hash,
|
|
role = 'admin',
|
|
is_approved = TRUE,
|
|
updated_at = NOW()`,
|
|
s.Cfg.AdminUsername, s.Cfg.AdminEmail, string(hash),
|
|
)
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = s.DB.Exec(ctx, `
|
|
UPDATE users
|
|
SET username = $1,
|
|
email = $2,
|
|
password_hash = $3,
|
|
role = 'admin',
|
|
is_approved = TRUE,
|
|
updated_at = NOW()
|
|
WHERE id = $4`,
|
|
s.Cfg.AdminUsername, s.Cfg.AdminEmail, string(hash), id,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) Authenticate(ctx context.Context, login, password string) (*models.User, error) {
|
|
var u models.User
|
|
err := s.DB.QueryRow(ctx, `
|
|
SELECT id, username, email, password_hash, role, is_approved, created_at
|
|
FROM users
|
|
WHERE username = $1 OR email = $1
|
|
LIMIT 1`, login,
|
|
).Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.IsApproved, &u.CreatedAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !u.IsApproved {
|
|
return nil, errors.New("account pending approval")
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
func (s *Store) Login(w http.ResponseWriter, r *http.Request, u *models.User, remember bool) error {
|
|
sess, _ := s.Sessions.Get(r, SessionName)
|
|
sess.Values["user_id"] = u.ID
|
|
sess.Values["username"] = u.Username
|
|
sess.Values["role"] = u.Role
|
|
sess.Values["remember"] = remember
|
|
if remember {
|
|
sess.Options.MaxAge = int(s.Cfg.RememberDuration().Seconds())
|
|
} else {
|
|
sess.Options.MaxAge = 0
|
|
}
|
|
return sess.Save(r, w)
|
|
}
|
|
|
|
func (s *Store) Logout(w http.ResponseWriter, r *http.Request) error {
|
|
sess, _ := s.Sessions.Get(r, SessionName)
|
|
sess.Options.MaxAge = -1
|
|
sess.Values = map[interface{}]interface{}{}
|
|
return sess.Save(r, w)
|
|
}
|
|
|
|
func (s *Store) CurrentUser(r *http.Request) *models.SessionUser {
|
|
sess, err := s.Sessions.Get(r, SessionName)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
id, ok := sess.Values["user_id"].(int64)
|
|
if !ok {
|
|
// gorilla may decode numbers as int
|
|
switch v := sess.Values["user_id"].(type) {
|
|
case int:
|
|
id = int64(v)
|
|
ok = true
|
|
case float64:
|
|
id = int64(v)
|
|
ok = true
|
|
}
|
|
}
|
|
if !ok || id == 0 {
|
|
return nil
|
|
}
|
|
username, _ := sess.Values["username"].(string)
|
|
role, _ := sess.Values["role"].(string)
|
|
return &models.SessionUser{ID: id, Username: username, Role: role}
|
|
}
|
|
|
|
func (s *Store) RequireAdmin(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
u := s.CurrentUser(r)
|
|
if u == nil {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if u.Role != "admin" {
|
|
http.Error(w, "Доступ запрещён", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func HashPassword(password string) (string, error) {
|
|
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func FormatDuration(d time.Duration) string {
|
|
return fmt.Sprintf("%d", int(d.Seconds()))
|
|
}
|