diff --git a/cmd/api/main.go b/cmd/api/main.go index e449e73..163a529 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -114,7 +114,7 @@ func main() { unmarkHandler := commands.NewUnmarkHabitHandler(habitRepo, entryRepo) authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator) - habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo, translator) + habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator) statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler, translator) healthHandlers := httpInfra.NewHealthHandlers(db.Conn()) userHandlers := httpInfra.NewUserHandlers(deleteUserHandler, translator) diff --git a/internal/application/commands/delete_user_test.go b/internal/application/commands/delete_user_test.go index 047046e..3f1ad0b 100644 --- a/internal/application/commands/delete_user_test.go +++ b/internal/application/commands/delete_user_test.go @@ -48,7 +48,7 @@ func TestDeleteUserHandler_Success(t *testing.T) { repo := &mockDeleteUserRepo{ findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) { - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = id return user, nil }, @@ -112,7 +112,7 @@ func TestDeleteUserHandler_DeleteError(t *testing.T) { repo := &mockDeleteUserRepo{ findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) { - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = id return user, nil }, diff --git a/internal/application/commands/register_user.go b/internal/application/commands/register_user.go index 4d63dbf..6c32839 100644 --- a/internal/application/commands/register_user.go +++ b/internal/application/commands/register_user.go @@ -17,7 +17,6 @@ import ( type RegisterUserCommand struct { Email string Password string - Timezone string } type RegisterUserResult struct { @@ -57,7 +56,7 @@ func (h *RegisterUserHandler) Handle(ctx context.Context, cmd RegisterUserComman return nil, errors.ErrRegistrationClosed } - if err := validation.ValidateRegistration(cmd.Email, cmd.Password, cmd.Timezone); err != nil { + if err := validation.ValidateRegistration(cmd.Email, cmd.Password); err != nil { return nil, fmt.Errorf("%w: %v", errors.ErrInvalidInput, err) } @@ -71,7 +70,7 @@ func (h *RegisterUserHandler) Handle(ctx context.Context, cmd RegisterUserComman return nil, err } - user := entities.NewUser(cmd.Email, hashedPassword, cmd.Timezone) + user := entities.NewUser(cmd.Email, hashedPassword) emailVerificationRequired := false if h.emailService != nil { diff --git a/internal/application/commands/register_user_test.go b/internal/application/commands/register_user_test.go index 88b532b..1b26814 100644 --- a/internal/application/commands/register_user_test.go +++ b/internal/application/commands/register_user_test.go @@ -74,7 +74,6 @@ func TestRegisterUserHandler_Success(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: "Secure123!", - Timezone: "UTC", } result, err := handler.Handle(context.Background(), cmd) @@ -97,10 +96,6 @@ func TestRegisterUserHandler_Success(t *testing.T) { if createdUser.Email != cmd.Email { t.Errorf("expected email %q, got %q", cmd.Email, createdUser.Email) } - - if createdUser.Timezone != cmd.Timezone { - t.Errorf("expected timezone %q, got %q", cmd.Timezone, createdUser.Timezone) - } } func TestRegisterUserHandler_InvalidEmail(t *testing.T) { @@ -125,7 +120,6 @@ func TestRegisterUserHandler_InvalidEmail(t *testing.T) { cmd := RegisterUserCommand{ Email: tt.email, Password: "Secure123!", - Timezone: "UTC", } _, err := handler.Handle(context.Background(), cmd) @@ -160,38 +154,6 @@ func TestRegisterUserHandler_InvalidPassword(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: tt.password, - Timezone: "UTC", - } - - _, err := handler.Handle(context.Background(), cmd) - if !errors.Is(err, appErrors.ErrInvalidInput) { - t.Errorf("expected ErrInvalidInput, got %v", err) - } - }) - } -} - -func TestRegisterUserHandler_InvalidTimezone(t *testing.T) { - repo := &mockUserRepo{} - hasher := &mockPasswordHasher{} - handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false) - - tests := []struct { - name string - timezone string - }{ - {"empty timezone", ""}, - {"invalid timezone", "InvalidTimezone"}, - {"numeric format", "GMT+1"}, - {"partial timezone", "America"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd := RegisterUserCommand{ - Email: "test@example.com", - Password: "Secure123!", - Timezone: tt.timezone, } _, err := handler.Handle(context.Background(), cmd) @@ -203,7 +165,7 @@ func TestRegisterUserHandler_InvalidTimezone(t *testing.T) { } func TestRegisterUserHandler_EmailAlreadyExists(t *testing.T) { - existingUser := entities.NewUser("test@example.com", "hashed", "UTC") + existingUser := entities.NewUser("test@example.com", "hashed") repo := &mockUserRepo{ findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) { return existingUser, nil @@ -215,7 +177,6 @@ func TestRegisterUserHandler_EmailAlreadyExists(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: "Secure123!", - Timezone: "UTC", } _, err := handler.Handle(context.Background(), cmd) @@ -237,7 +198,6 @@ func TestRegisterUserHandler_PasswordHashingError(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: "Secure123!", - Timezone: "UTC", } _, err := handler.Handle(context.Background(), cmd) @@ -259,7 +219,6 @@ func TestRegisterUserHandler_RepositoryError(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: "Secure123!", - Timezone: "UTC", } _, err := handler.Handle(context.Background(), cmd) @@ -283,7 +242,6 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) { RegisterUserCommand{ Email: "user+tag@example.com", Password: "Secure123!", - Timezone: "UTC", }, nil, }, @@ -292,16 +250,6 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) { RegisterUserCommand{ Email: "user@mail.example.com", Password: "Secure123!", - Timezone: "UTC", - }, - nil, - }, - { - "complex timezone", - RegisterUserCommand{ - Email: "user@example.com", - Password: "Secure123!", - Timezone: "America/Argentina/Buenos_Aires", }, nil, }, @@ -310,7 +258,6 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) { RegisterUserCommand{ Email: "user@example.com", Password: "Sëcure123!", - Timezone: "UTC", }, nil, }, @@ -319,7 +266,6 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) { RegisterUserCommand{ Email: "user@example.com", Password: "ValidP@ss1" + string(make([]byte, 100)), - Timezone: "UTC", }, nil, }, @@ -342,7 +288,6 @@ func TestRegisterUserHandler_ClosedRegistration(t *testing.T) { cmd := RegisterUserCommand{ Email: "test@example.com", Password: "Secure123!", - Timezone: "UTC", } _, err := handler.Handle(context.Background(), cmd) diff --git a/internal/application/commands/reset_password_test.go b/internal/application/commands/reset_password_test.go index 46a8a29..52610f5 100644 --- a/internal/application/commands/reset_password_test.go +++ b/internal/application/commands/reset_password_test.go @@ -98,7 +98,7 @@ func (m *mockResetPasswordHasher) Compare(hashedPassword, password string) error } func TestResetPasswordHandler_Success(t *testing.T) { - user := entities.NewUser("test@example.com", "old_hash", "UTC") + user := entities.NewUser("test@example.com", "old_hash") user.ID = "user-123" resetToken := entities.NewPasswordResetToken( @@ -340,7 +340,7 @@ func TestResetPasswordHandler_UserNotFound(t *testing.T) { } func TestResetPasswordHandler_HashingError(t *testing.T) { - user := entities.NewUser("test@example.com", "old_hash", "UTC") + user := entities.NewUser("test@example.com", "old_hash") user.ID = "user-123" resetToken := entities.NewPasswordResetToken( diff --git a/internal/application/commands/verify_email_test.go b/internal/application/commands/verify_email_test.go index 7fffe4f..9c8c877 100644 --- a/internal/application/commands/verify_email_test.go +++ b/internal/application/commands/verify_email_test.go @@ -67,7 +67,7 @@ func TestVerifyEmailHandler_Success(t *testing.T) { token := "valid-token" expiry := time.Now().Add(24 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token @@ -148,7 +148,7 @@ func TestVerifyEmailHandler_AlreadyVerified(t *testing.T) { token := "valid-token" expiry := time.Now().Add(24 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = true user.EmailVerificationToken = &token @@ -176,7 +176,7 @@ func TestVerifyEmailHandler_ExpiredToken(t *testing.T) { token := "expired-token" expiry := time.Now().Add(-1 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token @@ -203,7 +203,7 @@ func TestVerifyEmailHandler_ExpiredToken(t *testing.T) { func TestVerifyEmailHandler_NilExpiry(t *testing.T) { token := "valid-token" - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token @@ -231,7 +231,7 @@ func TestVerifyEmailHandler_WithWelcomeEmail(t *testing.T) { token := "valid-token" expiry := time.Now().Add(24 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token @@ -277,7 +277,7 @@ func TestVerifyEmailHandler_WithoutWelcomeEmail(t *testing.T) { token := "valid-token" expiry := time.Now().Add(24 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token @@ -310,7 +310,7 @@ func TestVerifyEmailHandler_UpdateError(t *testing.T) { token := "valid-token" expiry := time.Now().Add(24 * time.Hour) - user := entities.NewUser("test@example.com", "hashedPassword", "UTC") + user := entities.NewUser("test@example.com", "hashedPassword") user.ID = "user-123" user.EmailVerified = false user.EmailVerificationToken = &token diff --git a/internal/application/queries/login_user.go b/internal/application/queries/login_user.go index fc8af98..d670bb6 100644 --- a/internal/application/queries/login_user.go +++ b/internal/application/queries/login_user.go @@ -14,9 +14,8 @@ type LoginUserQuery struct { } type LoginUserResult struct { - UserID string - Email string - Timezone string + UserID string + Email string } type LoginUserHandler struct { @@ -50,8 +49,7 @@ func (h *LoginUserHandler) Handle(ctx context.Context, query LoginUserQuery) (*L } return &LoginUserResult{ - UserID: user.ID, - Email: user.Email, - Timezone: user.Timezone, + UserID: user.ID, + Email: user.Email, }, nil } diff --git a/internal/application/queries/login_user_test.go b/internal/application/queries/login_user_test.go index 672b62d..c2edda9 100644 --- a/internal/application/queries/login_user_test.go +++ b/internal/application/queries/login_user_test.go @@ -55,7 +55,7 @@ func (m *mockLoginPasswordHasher) Compare(hashedPassword, password string) error } func TestLoginUserHandler_Success(t *testing.T) { - user := entities.NewUser("test@example.com", "hashed_password", "UTC") + user := entities.NewUser("test@example.com", "hashed_password") user.ID = "user-123" user.EmailVerified = true @@ -150,7 +150,7 @@ func TestLoginUserHandler_UserNotFound(t *testing.T) { } func TestLoginUserHandler_InvalidPassword(t *testing.T) { - user := entities.NewUser("test@example.com", "hashed_password", "UTC") + user := entities.NewUser("test@example.com", "hashed_password") user.ID = "user-123" user.EmailVerified = true @@ -180,7 +180,7 @@ func TestLoginUserHandler_InvalidPassword(t *testing.T) { } func TestLoginUserHandler_EmailNotVerified(t *testing.T) { - user := entities.NewUser("test@example.com", "hashed_password", "UTC") + user := entities.NewUser("test@example.com", "hashed_password") user.ID = "user-123" user.EmailVerified = false diff --git a/internal/application/queries/refresh_token.go b/internal/application/queries/refresh_token.go index bf5bb45..06aa2bc 100644 --- a/internal/application/queries/refresh_token.go +++ b/internal/application/queries/refresh_token.go @@ -17,9 +17,8 @@ type RefreshTokenQuery struct { } type RefreshTokenResult struct { - UserID string - Email string - Timezone string + UserID string + Email string } type RefreshTokenHandler struct { @@ -57,9 +56,8 @@ func (h *RefreshTokenHandler) Handle(ctx context.Context, query RefreshTokenQuer } return &RefreshTokenResult{ - UserID: user.ID, - Email: user.Email, - Timezone: user.Timezone, + UserID: user.ID, + Email: user.Email, }, nil } diff --git a/internal/application/queries/refresh_token_test.go b/internal/application/queries/refresh_token_test.go index a5e0401..a82531c 100644 --- a/internal/application/queries/refresh_token_test.go +++ b/internal/application/queries/refresh_token_test.go @@ -80,7 +80,7 @@ func TestRefreshTokenHandler_Success(t *testing.T) { userRepo := &mockUserRepositoryForRefresh{ findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) { - user := entities.NewUser("test@example.com", "hash", "UTC") + user := entities.NewUser("test@example.com", "hash") user.ID = id return user, nil }, diff --git a/internal/domain/entities/user.go b/internal/domain/entities/user.go index 45d1e12..2a9469d 100644 --- a/internal/domain/entities/user.go +++ b/internal/domain/entities/user.go @@ -6,7 +6,6 @@ type User struct { ID string Email string PasswordHash string - Timezone string EmailVerified bool EmailVerificationToken *string EmailVerificationExpiry *time.Time @@ -14,15 +13,11 @@ type User struct { UpdatedAt time.Time } -func NewUser(email, passwordHash, timezone string) *User { +func NewUser(email, passwordHash string) *User { now := time.Now() - if timezone == "" { - timezone = "UTC" - } return &User{ Email: email, PasswordHash: passwordHash, - Timezone: timezone, CreatedAt: now, UpdatedAt: now, } diff --git a/internal/domain/entities/user_test.go b/internal/domain/entities/user_test.go index e2e0b65..d9c75bd 100644 --- a/internal/domain/entities/user_test.go +++ b/internal/domain/entities/user_test.go @@ -8,9 +8,8 @@ import ( func TestNewUser(t *testing.T) { email := "test@example.com" passwordHash := "hashed_password_123" - timezone := "Europe/Madrid" - user := NewUser(email, passwordHash, timezone) + user := NewUser(email, passwordHash) if user.Email != email { t.Errorf("Expected email %s, got %s", email, user.Email) @@ -20,10 +19,6 @@ func TestNewUser(t *testing.T) { t.Errorf("Expected password hash %s, got %s", passwordHash, user.PasswordHash) } - if user.Timezone != timezone { - t.Errorf("Expected timezone %s, got %s", timezone, user.Timezone) - } - if user.CreatedAt.IsZero() { t.Error("CreatedAt should not be zero") } @@ -37,11 +32,3 @@ func TestNewUser(t *testing.T) { t.Errorf("CreatedAt and UpdatedAt should be nearly identical, diff: %v", diff) } } - -func TestUser_DefaultTimezone(t *testing.T) { - user := NewUser("test@example.com", "hash", "") - - if user.Timezone != "UTC" { - t.Errorf("Expected default timezone UTC, got %s", user.Timezone) - } -} diff --git a/internal/i18n/locales/en.json b/internal/i18n/locales/en.json index 0e34a7c..ed54c30 100644 --- a/internal/i18n/locales/en.json +++ b/internal/i18n/locales/en.json @@ -46,7 +46,9 @@ "invalid_token_or_password": "Invalid or expired token, or password requirements not met", "failed_reset_password": "Failed to reset password", "failed_delete_user": "Failed to delete user", - "failed_get_stats": "Failed to get statistics" + "failed_get_stats": "Failed to get statistics", + "timezone_required": "Timezone is required", + "invalid_timezone": "Invalid timezone (must be a valid IANA timezone)" }, "success": { "registration_with_verification": "Registration successful. Please check your email to verify your account.", diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json index 6a59f0e..b826d6d 100644 --- a/internal/i18n/locales/es.json +++ b/internal/i18n/locales/es.json @@ -46,7 +46,9 @@ "invalid_token_or_password": "Token inválido o expirado, o no se cumplen los requisitos de contraseña", "failed_reset_password": "Error al restablecer contraseña", "failed_delete_user": "Error al eliminar usuario", - "failed_get_stats": "Error al obtener estadísticas" + "failed_get_stats": "Error al obtener estadísticas", + "timezone_required": "La zona horaria es requerida", + "invalid_timezone": "Zona horaria inválida (debe ser una zona horaria IANA válida)" }, "success": { "registration_with_verification": "Registro exitoso. Por favor revisa tu correo electrónico para verificar tu cuenta.", diff --git a/internal/infrastructure/http/auth_handlers.go b/internal/infrastructure/http/auth_handlers.go index 05ecc43..45dbbe8 100644 --- a/internal/infrastructure/http/auth_handlers.go +++ b/internal/infrastructure/http/auth_handlers.go @@ -65,7 +65,6 @@ func NewAuthHandlers( type RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` - Timezone string `json:"timezone"` } type LoginRequest struct { @@ -100,7 +99,7 @@ type LogoutRequest struct { // @Produce json // @Param request body RegisterRequest true "Registration data (password requires: min 8 chars, uppercase, lowercase, digit, special char)" // @Success 201 {object} RegisterResponse "Returns user ID and message about next steps" -// @Failure 400 {object} ValidationErrorResponse "Invalid input: email format, password requirements, or timezone" +// @Failure 400 {object} ValidationErrorResponse "Invalid input: email format or password requirements" // @Failure 403 {object} ErrorResponse "Registration is closed" // @Failure 409 {object} ErrorResponse "Email already registered" // @Failure 500 {object} ErrorResponse "Internal server error" @@ -115,7 +114,6 @@ func (h *AuthHandlers) Register(w http.ResponseWriter, r *http.Request) { cmd := commands.RegisterUserCommand{ Email: req.Email, Password: req.Password, - Timezone: req.Timezone, } result, err := h.registerHandler.Handle(r.Context(), cmd) diff --git a/internal/infrastructure/http/habit_handlers.go b/internal/infrastructure/http/habit_handlers.go index 547656b..c4d61a3 100644 --- a/internal/infrastructure/http/habit_handlers.go +++ b/internal/infrastructure/http/habit_handlers.go @@ -9,7 +9,6 @@ import ( "apocapoc-api/internal/application/commands" "apocapoc-api/internal/application/queries" - "apocapoc-api/internal/domain/repositories" "apocapoc-api/internal/i18n" "apocapoc-api/internal/shared/errors" @@ -26,7 +25,6 @@ type HabitHandlers struct { archiveHandler *commands.ArchiveHabitHandler markHandler *commands.MarkHabitHandler unmarkHandler *commands.UnmarkHabitHandler - userRepo repositories.UserRepository translator *i18n.Translator } @@ -40,7 +38,6 @@ func NewHabitHandlers( archiveHandler *commands.ArchiveHabitHandler, markHandler *commands.MarkHabitHandler, unmarkHandler *commands.UnmarkHabitHandler, - userRepo repositories.UserRepository, translator *i18n.Translator, ) *HabitHandlers { return &HabitHandlers{ @@ -53,7 +50,6 @@ func NewHabitHandlers( archiveHandler: archiveHandler, markHandler: markHandler, unmarkHandler: unmarkHandler, - userRepo: userRepo, translator: translator, } } @@ -440,11 +436,13 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request) // GetTodaysHabits godoc // @Summary Get today's habits -// @Description Get all habits scheduled for today for the authenticated user. Includes the entry for today if it exists. +// @Description Get all habits scheduled for today for the authenticated user. Includes the entry for today if it exists. Requires timezone as query parameter (e.g., ?timezone=America/New_York). // @Tags habits // @Produce json // @Security BearerAuth +// @Param timezone query string true "IANA timezone (e.g., 'America/New_York', 'Europe/Madrid', 'UTC')" // @Success 200 {array} TodaysHabitResponse +// @Failure 400 {object} ErrorResponse "Invalid or missing timezone" // @Failure 401 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /habits/today [get] @@ -455,15 +453,16 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request) return } - user, err := h.userRepo.FindByID(r.Context(), userID) - if err != nil { - respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_user") + timezone := r.URL.Query().Get("timezone") + if timezone == "" { + respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "timezone_required") return } - loc, err := time.LoadLocation(user.Timezone) + loc, err := time.LoadLocation(timezone) if err != nil { - loc = time.UTC + respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_timezone") + return } today := time.Now().In(loc) @@ -471,7 +470,7 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request) query := queries.GetTodaysHabitsQuery{ UserID: userID, - Timezone: user.Timezone, + Timezone: timezone, Date: todayDate, } diff --git a/internal/infrastructure/http/integration_test.go b/internal/infrastructure/http/integration_test.go index 693e2dc..a7efdb6 100644 --- a/internal/infrastructure/http/integration_test.go +++ b/internal/infrastructure/http/integration_test.go @@ -70,7 +70,7 @@ func setupTestServer(t *testing.T) *TestServer { translator, _ := i18n.NewTranslator() authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator) - habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo, translator) + habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator) statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator) healthHandlers := NewHealthHandlers(db) userHandlers := NewUserHandlers(deleteUserHandler, translator) diff --git a/internal/infrastructure/persistence/sqlite/migrations.go b/internal/infrastructure/persistence/sqlite/migrations.go index 02cfd26..acf0d66 100644 --- a/internal/infrastructure/persistence/sqlite/migrations.go +++ b/internal/infrastructure/persistence/sqlite/migrations.go @@ -25,6 +25,10 @@ func RunMigrations(db *sql.DB) error { return err } + if err := removeTimezoneColumn(db); err != nil { + return err + } + return nil } @@ -54,6 +58,23 @@ func addEmailVerificationColumns(db *sql.DB) error { return nil } +func removeTimezoneColumn(db *sql.DB) error { + exists, err := columnExists(db, "users", "timezone") + if err != nil { + return err + } + + if !exists { + return nil + } + + if _, err := db.Exec("ALTER TABLE users DROP COLUMN timezone"); err != nil { + return fmt.Errorf("failed to drop timezone column: %w", err) + } + + return nil +} + func columnExists(db *sql.DB, table, column string) (bool, error) { query := fmt.Sprintf("SELECT COUNT(*) FROM pragma_table_info('%s') WHERE name = ?", table) var count int @@ -69,7 +90,6 @@ CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, - timezone TEXT DEFAULT 'UTC', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); diff --git a/internal/infrastructure/persistence/sqlite/user_repository.go b/internal/infrastructure/persistence/sqlite/user_repository.go index fdc2f41..1e8116e 100644 --- a/internal/infrastructure/persistence/sqlite/user_repository.go +++ b/internal/infrastructure/persistence/sqlite/user_repository.go @@ -24,15 +24,14 @@ func (r *UserRepository) Create(ctx context.Context, user *entities.User) error user.ID = uuid.New().String() query := ` - INSERT INTO users (id, email, password_hash, timezone, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` _, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.PasswordHash, - user.Timezone, user.EmailVerified, user.EmailVerificationToken, user.EmailVerificationExpiry, @@ -52,7 +51,7 @@ func (r *UserRepository) Create(ctx context.Context, user *entities.User) error func (r *UserRepository) FindByID(ctx context.Context, id string) (*entities.User, error) { query := ` - SELECT id, email, password_hash, timezone, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at + SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at FROM users WHERE id = ? ` @@ -62,7 +61,6 @@ func (r *UserRepository) FindByID(ctx context.Context, id string) (*entities.Use &user.ID, &user.Email, &user.PasswordHash, - &user.Timezone, &user.EmailVerified, &user.EmailVerificationToken, &user.EmailVerificationExpiry, @@ -82,7 +80,7 @@ func (r *UserRepository) FindByID(ctx context.Context, id string) (*entities.Use func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) { query := ` - SELECT id, email, password_hash, timezone, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at + SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at FROM users WHERE email = ? ` @@ -92,7 +90,6 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entiti &user.ID, &user.Email, &user.PasswordHash, - &user.Timezone, &user.EmailVerified, &user.EmailVerificationToken, &user.EmailVerificationExpiry, @@ -112,7 +109,7 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entiti func (r *UserRepository) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) { query := ` - SELECT id, email, password_hash, timezone, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at + SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at FROM users WHERE email_verification_token = ? ` @@ -122,7 +119,6 @@ func (r *UserRepository) FindByVerificationToken(ctx context.Context, token stri &user.ID, &user.Email, &user.PasswordHash, - &user.Timezone, &user.EmailVerified, &user.EmailVerificationToken, &user.EmailVerificationExpiry, @@ -143,14 +139,13 @@ func (r *UserRepository) FindByVerificationToken(ctx context.Context, token stri func (r *UserRepository) Update(ctx context.Context, user *entities.User) error { query := ` UPDATE users - SET email = ?, password_hash = ?, timezone = ?, email_verified = ?, email_verification_token = ?, email_verification_expiry = ?, updated_at = ? + SET email = ?, password_hash = ?, email_verified = ?, email_verification_token = ?, email_verification_expiry = ?, updated_at = ? WHERE id = ? ` result, err := r.db.ExecContext(ctx, query, user.Email, user.PasswordHash, - user.Timezone, user.EmailVerified, user.EmailVerificationToken, user.EmailVerificationExpiry, diff --git a/internal/infrastructure/persistence/sqlite/user_repository_test.go b/internal/infrastructure/persistence/sqlite/user_repository_test.go index 748bba9..93b54b4 100644 --- a/internal/infrastructure/persistence/sqlite/user_repository_test.go +++ b/internal/infrastructure/persistence/sqlite/user_repository_test.go @@ -35,7 +35,6 @@ func TestUserRepositoryCreate(t *testing.T) { user := &entities.User{ Email: "test@example.com", PasswordHash: "hashed_password", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -60,7 +59,6 @@ func TestUserRepositoryCreateDuplicateEmail(t *testing.T) { user1 := &entities.User{ Email: "duplicate@example.com", PasswordHash: "hash1", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -73,7 +71,6 @@ func TestUserRepositoryCreateDuplicateEmail(t *testing.T) { user2 := &entities.User{ Email: "duplicate@example.com", PasswordHash: "hash2", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -94,7 +91,6 @@ func TestUserRepositoryFindByID(t *testing.T) { user := &entities.User{ Email: "find@example.com", PasswordHash: "hashed", - Timezone: "America/New_York", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -115,8 +111,6 @@ func TestUserRepositoryFindByID(t *testing.T) { if found.Email != user.Email { t.Errorf("Expected email %s, got %s", user.Email, found.Email) } - if found.Timezone != user.Timezone { - t.Errorf("Expected timezone %s, got %s", user.Timezone, found.Timezone) } } @@ -143,7 +137,6 @@ func TestUserRepositoryFindByEmail(t *testing.T) { user := &entities.User{ Email: "email@test.com", PasswordHash: "hashed", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -189,7 +182,6 @@ func TestUserRepositoryUpdate(t *testing.T) { user := &entities.User{ Email: "original@example.com", PasswordHash: "hash1", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -200,7 +192,6 @@ func TestUserRepositoryUpdate(t *testing.T) { } user.Email = "updated@example.com" - user.Timezone = "Europe/Madrid" user.UpdatedAt = time.Now() err = repo.Update(ctx, user) @@ -216,8 +207,6 @@ func TestUserRepositoryUpdate(t *testing.T) { if found.Email != "updated@example.com" { t.Errorf("Expected email updated@example.com, got %s", found.Email) } - if found.Timezone != "Europe/Madrid" { - t.Errorf("Expected timezone Europe/Madrid, got %s", found.Timezone) } } @@ -232,7 +221,6 @@ func TestUserRepositoryUpdateNotFound(t *testing.T) { ID: "non-existent", Email: "test@example.com", PasswordHash: "hash", - Timezone: "UTC", CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/internal/shared/validation/validator.go b/internal/shared/validation/validator.go index 0b763f8..c889e01 100644 --- a/internal/shared/validation/validator.go +++ b/internal/shared/validation/validator.go @@ -114,7 +114,7 @@ func ValidateTimezone(timezone string) error { return nil } -func ValidateRegistration(email, password, timezone string) error { +func ValidateRegistration(email, password string) error { if err := ValidateEmail(email); err != nil { return err } @@ -123,9 +123,5 @@ func ValidateRegistration(email, password, timezone string) error { return err } - if err := ValidateTimezone(timezone); err != nil { - return err - } - return nil }