Files
Owen f3a959334f Rename project from inventree to vibetree
- 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).
2026-08-17 17:14:52 -05:00

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
}