package routers import ( "context" "encoding/json" "errors" "net/http" "strings" "time" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/internal/server/confirmationimp" "go.uber.org/zap" ) const pendingLoginTTLMinutes = 10 func (pr *PublicRouter) logUserIn(ctx context.Context, _ *http.Request, req *srequest.Login) http.HandlerFunc { // Get the account database entry trimmedLogin := strings.TrimSpace(req.Login) account, err := pr.db.GetByEmail(ctx, strings.ToLower(trimmedLogin)) if errors.Is(err, merrors.ErrNoData) || (account == nil) { pr.logger.Debug("User not found while logging in", zap.Error(err), zap.String("login", req.Login)) return response.Unauthorized(pr.logger, pr.service, "user not found") } if err != nil { pr.logger.Warn("Failed to query user with email", zap.Error(err), zap.String("login", req.Login)) return response.Internal(pr.logger, pr.service, err) } if account.VerifyToken != "" { return response.Forbidden(pr.logger, pr.service, "account_not_verified", "Account verification required") } if !account.MatchPassword(req.Password) { return response.Unauthorized(pr.logger, pr.service, "password does not match") } pendingToken, err := pr.imp.CreatePendingToken(account, pendingLoginTTLMinutes) if err != nil { pr.logger.Warn("Failed to generate pending token", zap.Error(err)) return response.Internal(pr.logger, pr.service, err) } cfg := confirmationimp.DefaultConfig() _, rec, err := pr.cstore.Create(ctx, account.ID, account.Login, model.ConfirmationTargetLogin, cfg, pr.generateCode) if err != nil { pr.logger.Warn("Failed to create login confirmation code", zap.Error(err)) return response.Internal(pr.logger, pr.service, err) } pr.logger.Info("Login confirmation code issued", zap.String("destination", pr.maskEmail(account.Login))) return sresponse.LoginPending(pr.logger, account, &pendingToken, pr.maskEmail(account.Login), int(time.Until(rec.ExpiresAt).Seconds())) } func (a *PublicRouter) login(r *http.Request) http.HandlerFunc { // TODO: add rate check var req srequest.Login if err := json.NewDecoder(r.Body).Decode(&req); err != nil { a.logger.Info("Failed to decode login request", zap.Error(err)) return response.BadPayload(a.logger, mservice.Accounts, err) } req.Login = strings.TrimSpace(req.Login) req.Password = strings.TrimSpace(req.Password) if req.Login == "" { return response.BadRequest(a.logger, mservice.Accounts, "email_missing", "login request has no user name") } if req.Password == "" { return response.BadRequest(a.logger, mservice.Accounts, "password_missing", "login request has no password") } return a.logUserIn(r.Context(), r, &req) }