package auth import ( "context" "crypto/rand" "encoding/hex" "errors" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "golang.org/x/crypto/bcrypt" "logjensticks/internal/db" ) var ErrInvalidCredentials = errors.New("invalid username or password") var ErrSessionNotFound = errors.New("session not found or expired") type User struct { Username string `bson:"username"` PasswordHash string `bson:"password_hash"` Role string `bson:"role"` } type Session struct { Token string `bson:"token"` Username string `bson:"username"` Role string `bson:"role"` ExpiresAt time.Time `bson:"expires_at"` } // Login verifies credentials, creates a session, and returns the session token. func Login(ctx context.Context, dbc *db.Client, username, password string) (string, *Session, error) { var user User err := dbc.Users().FindOne(ctx, bson.D{{Key: "username", Value: username}}).Decode(&user) if errors.Is(err, mongo.ErrNoDocuments) { return "", nil, ErrInvalidCredentials } if err != nil { return "", nil, err } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return "", nil, ErrInvalidCredentials } token, err := generateToken() if err != nil { return "", nil, err } session := Session{ Token: token, Username: user.Username, Role: user.Role, ExpiresAt: time.Now().UTC().Add(db.SessionTTL), } if _, err := dbc.Sessions().InsertOne(ctx, session); err != nil { return "", nil, err } return token, &session, nil } // ValidateSession looks up a session by token and confirms it has not expired. func ValidateSession(ctx context.Context, dbc *db.Client, token string) (*Session, error) { var session Session err := dbc.Sessions().FindOne(ctx, bson.D{{Key: "token", Value: token}}).Decode(&session) if errors.Is(err, mongo.ErrNoDocuments) { return nil, ErrSessionNotFound } if err != nil { return nil, err } if time.Now().After(session.ExpiresAt) { return nil, ErrSessionNotFound } return &session, nil } // DeleteSession removes a session document (used on logout). func DeleteSession(ctx context.Context, dbc *db.Client, token string) error { _, err := dbc.Sessions().DeleteOne(ctx, bson.D{{Key: "token", Value: token}}) return err } func generateToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }