- go.mod module path (and every internal/vibetree import) inventree -> vibetree - cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service - Default site name (seed data + the siteName template func's fallback) is now "vibetree", shown in the navbar, <title>, and login page - Session cookie renamed inventree_session -> vibetree_session - Default DB path data/inventree.db -> data/vibetree.db, CLI/log text, Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all updated to match Existing installs: this changes the session cookie name (users will be signed out once) and the default DB filename (point -db at your existing file, or rename it, to keep your data).
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
// Package auth implements session-based authentication with secure cookies.
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"vibetree/internal/models"
|
|
"vibetree/internal/users"
|
|
)
|
|
|
|
const (
|
|
CookieName = "vibetree_session"
|
|
SessionTTL = 7 * 24 * time.Hour
|
|
contextUserK = ctxKey("current_user")
|
|
)
|
|
|
|
type ctxKey string
|
|
|
|
var ErrNoSession = errors.New("no valid session")
|
|
|
|
type Store struct {
|
|
db *sql.DB
|
|
users *users.Store
|
|
}
|
|
|
|
func NewStore(db *sql.DB, u *users.Store) *Store { return &Store{db: db, users: u} }
|
|
|
|
func newToken() (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// CreateSession issues a new session for the given user and returns the
|
|
// opaque session token to be stored in a cookie.
|
|
func (s *Store) CreateSession(ctx context.Context, userID int64) (string, time.Time, error) {
|
|
token, err := newToken()
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
expires := time.Now().Add(SessionTTL)
|
|
_, err = s.db.ExecContext(ctx, `INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)`,
|
|
token, userID, expires.UTC().Format("2006-01-02T15:04:05.000Z"))
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
return token, expires, nil
|
|
}
|
|
|
|
func (s *Store) DestroySession(ctx context.Context, token string) error {
|
|
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, token)
|
|
return err
|
|
}
|
|
|
|
// UserForToken resolves a session token to the current, enabled user. Expired
|
|
// sessions are treated as invalid (and lazily cleaned up).
|
|
func (s *Store) UserForToken(ctx context.Context, token string) (*models.User, error) {
|
|
if token == "" {
|
|
return nil, ErrNoSession
|
|
}
|
|
var userID int64
|
|
var expiresAt string
|
|
err := s.db.QueryRowContext(ctx, `SELECT user_id, expires_at FROM sessions WHERE id = ?`, token).Scan(&userID, &expiresAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNoSession
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exp, _ := time.Parse("2006-01-02T15:04:05.000Z", expiresAt)
|
|
if time.Now().After(exp) {
|
|
_, _ = s.db.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, token)
|
|
return nil, ErrNoSession
|
|
}
|
|
u, err := s.users.Get(ctx, userID)
|
|
if err != nil {
|
|
return nil, ErrNoSession
|
|
}
|
|
if !u.Enabled {
|
|
return nil, ErrNoSession
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// CleanExpired removes expired sessions; safe to call periodically.
|
|
func (s *Store) CleanExpired(ctx context.Context) error {
|
|
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at < ?`, time.Now().UTC().Format("2006-01-02T15:04:05.000Z"))
|
|
return err
|
|
}
|
|
|
|
func SetCookie(w http.ResponseWriter, token string, expires time.Time, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: CookieName,
|
|
Value: token,
|
|
Path: "/",
|
|
Expires: expires,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func ClearCookie(w http.ResponseWriter, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: CookieName,
|
|
Value: "",
|
|
Path: "/",
|
|
Expires: time.Unix(0, 0),
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
// WithUser stores the current user in the request context.
|
|
func WithUser(ctx context.Context, u *models.User) context.Context {
|
|
return context.WithValue(ctx, contextUserK, u)
|
|
}
|
|
|
|
// UserFromContext returns the current authenticated user, if any.
|
|
func UserFromContext(ctx context.Context) (*models.User, bool) {
|
|
u, ok := ctx.Value(contextUserK).(*models.User)
|
|
return u, ok && u != nil
|
|
}
|