mirror of
https://git.evilfox.cc/test2/wg.git
synced 2026-07-31 20:03:22 +00:00
Add Go WireGuard panel with PostgreSQL 17 and Dokploy compose.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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()))
|
||||
}
|
||||
Reference in New Issue
Block a user