Files
encore-test/profiles/login.go
fabio a0b65a4506 refactor: remove unused Pinia stores and related code
- Deleted the layout-store, profiles-store, and index.ts files from the Pinia store setup.
- Removed the zod-sync script as it is no longer needed.
- Cleaned up the tsconfig.json file by removing unnecessary references.
- Updated the login logic in the profiles package to use polling for login results instead of in-process waiters.
- Added validation methods for personal data and profile parameters.
- Introduced new tests for validation logic in profiles and registration packages.
- Enhanced email normalization and validation in the registration package.
2026-09-19 12:03:16 +02:00

106 lines
3.2 KiB
Go

package profiles
import (
"context"
"errors"
"time"
"encore.app/auth"
"encore.dev/beta/errs"
"encore.dev/pubsub"
"encore.dev/storage/sqldb"
"encore.dev/types/uuid"
)
const loginTimeout = 10 * time.Second
type LoginParams struct {
UserEmail string `json:"user_email"`
Password string `json:"password" encore:"sensitive"`
}
type LoginResponse struct {
Token uuid.UUID `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
var loginRequests = pubsub.TopicRef[pubsub.Publisher[*auth.LoginRequested]](auth.LoginRequests)
// Login is the public entry point for authenticating a user. It first
// resolves the given email to a user ID via the local profiles database.
// The actual credential check lives in the auth service; rather than calling
// it directly, this hands the request off over Pub/Sub (publishing to
// auth.LoginRequests) and waits for the matching auth.LoginCompleted reply
// on auth.LoginResults before responding to the caller.
//
//encore:api public method=POST path=/auth/login
func Login(ctx context.Context, p *LoginParams) (*LoginResponse, error) {
var userID uuid.UUID
err := db.QueryRow(ctx, `
SELECT user_id FROM user_profiles WHERE email = $1
`, p.UserEmail).Scan(&userID)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, &errs.Error{Code: errs.Unauthenticated, Message: "invalid credentials"}
}
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to look up user")
}
requestID, err := uuid.NewV4()
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to generate request id")
}
if _, err := loginRequests.Publish(ctx, &auth.LoginRequested{
RequestID: requestID,
UserID: userID,
Password: p.Password,
}); err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to publish login request")
}
result, err := pollLoginResult(ctx, func(ctx context.Context) (*auth.LoginCompleted, error) {
var ok bool
var token uuid.UUID
var expiresAt time.Time
err := authDB.QueryRow(ctx, `
SELECT ok, token, expires_at FROM login_results WHERE request_id = $1
`, requestID).Scan(&ok, &token, &expiresAt)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, sqldb.ErrNoRows
}
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to read login result")
}
return &auth.LoginCompleted{RequestID: requestID, OK: ok, Token: token, ExpiresAt: expiresAt}, nil
}, loginTimeout, 50*time.Millisecond)
if err != nil {
return nil, err
}
if !result.OK {
return nil, &errs.Error{Code: errs.Unauthenticated, Message: "invalid credentials"}
}
return &LoginResponse{Token: result.Token, ExpiresAt: result.ExpiresAt}, nil
}
func pollLoginResult(ctx context.Context, fetch func(context.Context) (*auth.LoginCompleted, error), timeout, interval time.Duration) (*auth.LoginCompleted, error) {
deadline := time.Now().Add(timeout)
for {
result, err := fetch(ctx)
if err == nil {
return result, nil
}
if !errors.Is(err, sqldb.ErrNoRows) {
return nil, err
}
if time.Now().After(deadline) {
return nil, &errs.Error{Code: errs.DeadlineExceeded, Message: "login timed out"}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(interval):
}
}
}