Initial implementation of inventree
A small IT asset inventory system built around a nested folder tree, matching the project brief: Go + html/template + Bootstrap 5 + vanilla JS, SQLite storage (WAL, FKs, migrations), embedded templates/static assets, single compiled binary. - AJAX-driven inventory tree: lazy-loaded folder expansion with per-user persisted expand state, drag-and-drop move for folders/assets, safe delete-with-relocate-or-cascade confirmation, search with breadcrumbs. - Assets: CRUD, duplicate, move, auto-generated IT-000001-style IDs, CSV import (with optional folder auto-create) and export. - Admin-defined item types with custom fields (text/long_text/number/date/ boolean/dropdown/url/ip_address). - Configurable statuses, session-based auth (bcrypt, secure cookies), admin/user roles, user management. - CLI: serve, version, database migrate, database backup; /healthz; graceful shutdown; Dockerfile/docker-compose/systemd unit; Go test suite.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Package auth implements session-based authentication with secure cookies.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"inventree/internal/models"
|
||||
"inventree/internal/users"
|
||||
)
|
||||
|
||||
const (
|
||||
CookieName = "inventree_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
|
||||
}
|
||||
Reference in New Issue
Block a user