Add Go WireGuard panel with PostgreSQL 17 and Dokploy compose.

This commit is contained in:
2026-07-29 09:15:00 +03:00
parent 370e2455d4
commit 31ce2af622
25 changed files with 2423 additions and 0 deletions
+180
View File
@@ -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()))
}