Files
nhost/services/auth/go/controller/sign_in_passwordless_email.go

185 lines
4.6 KiB
Go

package controller
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/notifications"
"github.com/nhost/nhost/services/auth/go/sql"
)
func (ctrl *Controller) SignInPasswordlessEmail( //nolint:ireturn
ctx context.Context,
request api.SignInPasswordlessEmailRequestObject,
) (api.SignInPasswordlessEmailResponseObject, error) {
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
if !ctrl.config.EmailPasswordlessEnabled {
logger.WarnContext(ctx, "email passwordless signin is disabled")
return ctrl.sendError(ErrDisabledEndpoint), nil
}
options, apiErr := ctrl.signinEmailValidateRequest(
ctx, string(request.Body.Email), request.Body.Options, logger)
if apiErr != nil {
return ctrl.respondWithError(apiErr), nil
}
ticket := generateTicket(TicketTypePasswordLessEmail)
ticketExpiresAt := time.Now().Add(time.Hour)
if apiErr := ctrl.signinWithTicket(
ctx,
string(request.Body.Email),
options,
ticket,
ticketExpiresAt,
notifications.TemplateNameSigninPasswordless,
LinkTypePasswordlessEmail,
logger,
); apiErr != nil {
return ctrl.respondWithError(apiErr), nil
}
return api.SignInPasswordlessEmail200JSONResponse(api.OK), nil
}
func (ctrl *Controller) signinEmailValidateRequest(
ctx context.Context,
email string,
options *api.SignUpOptions,
logger *slog.Logger,
) (*api.SignUpOptions, *APIError) {
if !ctrl.wf.ValidateEmail(email) {
logger.WarnContext(ctx, "email didn't pass access control checks")
return nil, ErrInvalidEmailPassword
}
options, apiErr := ctrl.wf.ValidateSignUpOptions(ctx, options, email, logger)
if apiErr != nil {
return nil, apiErr
}
return options, nil
}
func (ctrl *Controller) signinWithTicket(
ctx context.Context,
email string,
options *api.SignUpOptions,
ticket string,
ticketExpiresAt time.Time,
template notifications.TemplateName,
linkType LinkType,
logger *slog.Logger,
) *APIError {
user, apiErr := ctrl.wf.GetUserByEmail(ctx, email, logger)
switch {
case errors.Is(apiErr, ErrUserEmailNotFound):
logger.InfoContext(ctx, "user does not exist, creating user")
user, apiErr = ctrl.signinWithTicketSignUp(
ctx, email, options, ticket, ticketExpiresAt, logger,
)
if apiErr != nil {
return apiErr
}
case errors.Is(apiErr, ErrUnverifiedUser):
if apiErr = ctrl.wf.SetTicket(ctx, user.ID, ticket, ticketExpiresAt, logger); apiErr != nil {
return apiErr
}
case apiErr != nil:
logger.ErrorContext(ctx, "error getting user by email", logError(apiErr))
return apiErr
default:
if apiErr = ctrl.wf.SetTicket(ctx, user.ID, ticket, ticketExpiresAt, logger); apiErr != nil {
return apiErr
}
}
if apiErr := ctrl.wf.SendEmail(
ctx,
email,
user.Locale,
linkType,
ticket,
deptr(options.RedirectTo),
template,
user.DisplayName,
email,
"",
logger,
); apiErr != nil {
return apiErr
}
return nil
}
func (ctrl *Controller) signinWithTicketSignUp(
ctx context.Context,
email string,
options *api.SignUpOptions,
ticket string,
ticketExpiresAt time.Time,
logger *slog.Logger,
) (sql.AuthUser, *APIError) {
var user sql.AuthUser
apiErr := ctrl.wf.SignupUserWithouthSession(
ctx,
email,
options,
false,
func(
_ pgtype.Text,
_ pgtype.Timestamptz,
metadata []byte,
gravatarURL string,
) error {
resp, err := ctrl.wf.db.InsertUser(ctx, sql.InsertUserParams{
ID: uuid.New(),
Disabled: ctrl.config.DisableNewUsers,
DisplayName: deptr(options.DisplayName),
AvatarUrl: gravatarURL,
Email: sql.Text(email),
PasswordHash: pgtype.Text{}, //nolint:exhaustruct
Ticket: sql.Text(ticket),
TicketExpiresAt: sql.TimestampTz(ticketExpiresAt),
EmailVerified: false,
Locale: deptr(options.Locale),
DefaultRole: deptr(options.DefaultRole),
Metadata: metadata,
Roles: deptr(options.AllowedRoles),
PhoneNumber: pgtype.Text{}, //nolint:exhaustruct
Otp: "",
OtpHashExpiresAt: pgtype.Timestamptz{}, //nolint:exhaustruct
OtpMethodLastUsed: pgtype.Text{}, //nolint:exhaustruct
})
if err != nil {
return fmt.Errorf("error inserting user: %w", err)
}
user = sql.AuthUser{ //nolint:exhaustruct
ID: resp.UserID,
Locale: deptr(options.Locale),
DisplayName: deptr(options.DisplayName),
}
return nil
},
logger,
)
return user, apiErr
}