Commit Graph

18 Commits

Author SHA1 Message Date
Víctor Falcón 87c63f2100
fix(auth): make build deterministic when REGISTRATION_ENABLED=false (#720)
## Problem

`bun run build` failed whenever `REGISTRATION_ENABLED=false`.

`config/fortify.php` only added `Features::registration()` when the env
flag
was truthy, so with the flag off Fortify never registered the
`/register`
routes. Wayfinder only generates helpers for registered routes, so the
`register` / `register.store` helpers imported by
`resources/js/pages/auth/login.tsx`
and `resources/js/pages/auth/register.tsx` no longer resolved and the
build
broke. The build outcome depended on an env flag — not deterministic.

## Fix

Decouple *route registration* from *whether sign-ups are accepted*:

- **Always register** `Features::registration()` in
`config/fortify.php`, so the
`/register` routes (and their Wayfinder helpers) always exist and the
build is
  deterministic regardless of the flag.
- Gate acceptance at **runtime** via a new
`config('auth.registration_enabled')`
value (mapped from `env('REGISTRATION_ENABLED', true)`), the single
source of truth.
- When registration is disabled:
- `GET /register` (Fortify register view) and `POST /register`
(`CreateNewUser`)
    return **403**.
- Registration CTAs stay hidden via `canRegister` (login view + landing
page).
- Guests are redirected to `/login`
(`AuthEntryPointService::guestRedirectRoute`).
- With the flag enabled (the default), registration behaves exactly as
before.

## Tests

- Rewrote `tests/Feature/Auth/RegistrationDisabledTest.php` to the
runtime
mechanism: registration enabled by default, route helpers always
registered
regardless of the flag, `GET`/`POST /register` return 403 when disabled
(and no
  user is created), and the landing/login CTAs hide.
- Updated the `DashboardTest` disabled-registration case to toggle
`config('auth.registration_enabled')` instead of filtering the Fortify
feature.
- Verified `bun run build` succeeds with `REGISTRATION_ENABLED=false`
and that the
  `register` / `register.store` Wayfinder helpers are generated.

## Docs

- Updated `README.md` and `.env.example` to describe the 403 behavior
(routes stay
  registered rather than being removed).
2026-07-22 07:04:20 +00:00
Víctor Falcón a5117aaae6
refactor: remove HIDE_AUTH_BUTTONS launch gate and waitlist apparatus (#717)
## What & why

The `HIDE_AUTH_BUTTONS` flag and its signed-link auth-override existed
only to gate registration/login behind a waitlist during the launch
period. That period is long over, so this removes the flag and the whole
waitlist apparatus. Registration and login are now always open, still
governed by `REGISTRATION_ENABLED` (Fortify's registration feature),
which is untouched.

## Removed

- **`HIDE_AUTH_BUTTONS`** config/env and
**`LandingAuthOverrideService`** — the signed-link override, override
cookie, and `?force=` bypass — plus its
`GenerateLandingAuthLinkCommand`.
- **Waitlist lead-capture + invitation apparatus**: `UserLeadController`
+ `waitlist.*`/`user-leads.*` routes, `StoreUserLeadRequest`, the
landing `WaitlistForm`, `waitlist/*` pages, waitlist/invitation mails,
invitation & re-invitation commands, the lead verification notification,
`LeadCohort`/`LeadCohortResolver`/`LeadPromoCodeAllocator`, and the
lead→Resend segment sync (scheduled tasks removed too).
- Frontend `hideAuthButtons`/`forcedRegistration` threading and the
`?force=` query on auth links (header, welcome, register, login).
- All tests for the removed features; `landing.hide_auth_buttons` config
stubs stripped from surviving tests.

## Kept on purpose (data preservation)

The `user_leads` **table, its migrations, and a lean `UserLead` model
are retained** — no destructive migration. Pre-launch contacts are not
lost, and new signups whose email matches a preserved lead still receive
that lead's promo code at Stripe checkout
(`SubscriptionController::resolveLeadPromotionCodeId`, unchanged).

## Testing

- Full non-Browser Pest suite green locally (**1948 passed**), plus
Pint, Prettier, ESLint.
- Browser QA (Playwright) on the running app: landing no longer shows
the waitlist form and renders the register/login CTAs; `/register`
renders the form (previously returned null when hidden); `/login` shows
the "Sign up" link pointing to `/register` with no `?force`. 16/16
functional checks passed; only pre-existing external-resource 404s in
console (gravatar `d=404`, `via.placeholder.com` seed images).

## Demo


https://github.com/user-attachments/assets/18a70f6d-b82f-4d92-b9de-da2610dc017a
2026-07-22 08:51:48 +02:00
Jesús Mejías Leiva d55ee5bdcd
feat(auth): add REGISTRATION_ENABLED env to disable public sign-ups (#713)
## Summary

Adds a dedicated `REGISTRATION_ENABLED` env var (default `true`) so
self-hosters on a publicly reachable instance can **close public
sign-ups while keeping login open** — the exact "sign-ups closed, login
open, permanently" case from #711 that `HIDE_AUTH_BUTTONS` can't cover.

Closes #711.

## What changed

- **`config/fortify.php`** — the Fortify `registration()` feature is now
conditional on `env('REGISTRATION_ENABLED', true)`. When disabled:
- Fortify **never registers the `/register` routes** (GET form + POST
submit → `404`), i.e. a real server-side block, not just a hidden
button.
- The existing `canRegister =
Features::enabled(Features::registration())` flag flips to `false`
automatically everywhere it's used (`routes/web.php`,
`FortifyServiceProvider`, login page).
- **`resources/js/pages/welcome.tsx`** — the hardcoded `/register` CTAs
(hero button, Free/paid pricing cards, final "Ready to take control"
CTA) now respect `canRegister`, falling back to a `Log in` → `/login`
button when sign-ups are closed. The header already gated its `Register`
button on `canRegister`.
- **`.env.example` / `README.md`** — document the new variable.

**Backward compatible / optional:** `env('REGISTRATION_ENABLED', true)`
defaults to `true`, so existing installs that upgrade without setting
the var keep public sign-ups exactly as today. It's shown as a
commented, optional example in `.env.example`.

`/login` and the "Iniciar sesión / Log in" button stay fully available
in all cases. `AuthEntryPointService::guestRedirectRoute()` already
redirects guests to `/login` when registration is disabled, so no dead
`route('register')` calls remain.

## Tests

New `tests/Feature/Auth/RegistrationDisabledTest.php`:

- registration feature present by default / removed when
`REGISTRATION_ENABLED=false` (config-level).
- landing page exposes `canRegister=true` by default and `false` when
the feature is disabled.
- login page hides its sign-up link when registration is disabled.
- **`/register` GET + POST return 404 and the named routes are gone**
when disabled (via `refreshApplication()` with the env set), while
`/login` still returns 200.

All new tests pass; the wider `tests/Feature/Auth` + landing-override
suites stay green (the one unrelated `Asia/Calcutta` legacy-timezone
failure is a pre-existing tzdata quirk of the CI-less local box, not
touched here). `pint`, `prettier --check`, and `eslint` are clean.

## Manual QA

Verified against a running instance (headless Chromium) in both states:

| `REGISTRATION_ENABLED=true` (default) | `REGISTRATION_ENABLED=false` |
| --- | --- |
| Header: `Log in` + `Register`; hero CTA `Get Started` → `/register` |
Header: `Register` gone, only `Log in`; hero CTA `Log in` → `/login` |
| `GET /register` → `200` | `GET /register` → `404`, `GET /login` →
`200`, `Route::has('register')` → `false` |
2026-07-21 16:22:45 +02:00
Víctor Falcón fcf2d3d1ad
feat(users): track last login and last active timestamps (#516)
## What

Adds two timestamp columns to `users`:

- **`last_logged_in_at`** — set by an `UpdateLastLoggedInAt` listener on
Laravel's `Login` event. Records each explicit authentication (login
form or remember-me re-auth). Does **not** update on plain session-based
requests.
- **`last_active_at`** — set by a `TrackLastActiveAt` middleware on
authenticated web requests. Records the last moment the user did
anything. Throttled to at most one write per 5 minutes to avoid a DB
write on every request.

## Why

`last_logged_in_at` answers "when did they last authenticate";
`last_active_at` answers "when were they last using the app" (any
screen/activity), which a session-based login does not capture.

## Tests

- Login records `last_logged_in_at`
- Authenticated request records `last_active_at`
- Throttle window respected (no rewrite within 5 min) and refreshed once
it passes

Migrations not yet run on environments.
2026-06-10 11:01:30 +02:00
Víctor Falcón 14b79557d7
fix: verify email via signed link without requiring login (#490)
## Problem

When the verification email link opens in a browser where the user is
not logged in (common on phones, where the link escapes the app), the
`verification.verify` route's `auth` middleware redirects to login and
the email is never verified.

## Fix

The signed verification URL already self-secures via `id` + `hash` +
signature + expiry, so the `auth` requirement was redundant. This adds a
public, signed-only verify route and points the verification email at
it.

- `Auth/VerifyEmailController` — resolves the user by `id`, validates
`hash` with `hash_equals(sha1(email))`, marks verified and fires
`Verified`. Guests → `login` with a success status; the same logged-in
user → `dashboard?verified=1`.
- New route `GET /verify-email/{id}/{hash}` with `signed` + `throttle`
(name `verification.verify.public`). Distinct URI/name so it doesn't
clash with Fortify's existing auth-protected route, which stays intact.
- `VerifyEmailNotification::verificationUrl()` overridden to sign the
public route.

Now the link verifies regardless of login state, and the user can return
to the app and sign in.

## Tests

- 6 new cases in `EmailVerificationTest` (logged-out verify, logged-in
verify, bad hash, unsigned request, already-verified no refire).
- 1 new case in `VerificationNotificationTest` asserting the email links
the public signed route.
- All pass; pint clean.
2026-06-05 10:01:32 +02:00
Víctor Falcón 96ee311299
fix(register): don't block signup on unrecognized browser timezone (#462)
## Problem

Some users reported they couldn't register: they filled the form
correctly, pressed the button, saw a loading spinner, then got bounced
back to the registration page with **no error message**. Meanwhile 20-30
users register fine every day, and the issue wasn't reproducible by the
team.

## Root cause

The register form sends a **hidden, browser-auto-detected `timezone`**
field (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The backend
validated it with Laravel's `timezone` rule:

```php
'timezone' => ['nullable', 'string', 'timezone'],
```

That rule rejects **legacy IANA aliases** that many browsers/OSes still
emit — e.g. `Asia/Calcutta`, `Asia/Saigon`, `Asia/Rangoon`,
`US/Pacific`. Proof on PHP 8.4:

```
in_array("Asia/Calcutta", DateTimeZone::listIdentifiers())  →  false
```

When validation failed, Fortify redirected back with a `timezone` error
— but the form had **no error display** for timezone (every other field
has one). Result: spinner → silent bounce → no message → and the user
can't fix a hidden field anyway. The team couldn't reproduce because
their browsers send canonical zones that pass. Production tzdata may
reject even more zones.

## Fix

- Stop validating the auto-detected hidden field against the strict
timezone list.
- Add `normalizeTimezone()`: keep recognized zones (including BC aliases
via `DateTimeZone::ALL_WITH_BC`), drop anything unrecognized to `null`.
Registration can never be blocked by it again.
- Surface `errors.timezone` in the form as a safety net so a
hidden-field failure can never be silent in the future.

## Tests

- Legacy alias `Asia/Calcutta` → registers, stored as-is.
- Unrecognized zone → registers, stored `null`.
- All existing registration tests pass (13 passed).
2026-06-01 09:03:15 +02:00
Víctor Falcón 2604f1158c
Support soft-deleted users with reusable emails (#316)
## Summary
- soft-delete users by adding `deleted_at` to `users`
- rename deleted user emails with a timestamp prefix so original email
can be reused
- block email sends and banking follow-up work for deleted users while
preserving data

## Testing
- php artisan test --compact tests/Feature/DeleteUserCommandTest.php
tests/Feature/Settings/ProfileUpdateTest.php
tests/Feature/Auth/RegistrationTest.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Jobs/Drip/SendWelcomeEmailJobTest.php
tests/Feature/Console/SendUpdateEmailCommandTest.php
- vendor/bin/pint --dirty --format agent
2026-04-22 11:41:41 +01:00
Víctor Falcón 905edeb4a2
fix: route new PWA guests to signup (#313)
## Summary
- redirect guest access to protected routes based on returning-user
cookie
- send new PWA guests to registration and returning guests to login
- persist returning-user cookie after register, login, and 2FA login
- keep explicit login links working via `force=1`

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/Auth/AuthenticationTest.php
tests/Feature/DashboardTest.php tests/Feature/Auth/RegistrationTest.php
2026-04-21 10:53:05 +01:00
Víctor Falcón 240fcf1703
feat(landing): add signed auth links (#312)
## Summary
- add signed landing links that unlock auth buttons while
HIDE_AUTH_BUTTONS is enabled
- persist the unlock in a secure cookie so desktop and installed PWA
users can still sign up
- add artisan command to generate signed landing auth links and test
coverage for the flow

## Testing
- php artisan test --compact tests/Feature/LandingAuthOverrideTest.php
tests/Feature/Console/GenerateLandingAuthLinkCommandTest.php
tests/Feature/Auth/RegistrationTest.php
- php artisan test --compact tests/Feature/WelcomeBanksOrderingTest.php
tests/Feature/SubscriptionTest.php
- vendor/bin/pint --dirty --format agent
2026-04-21 08:28:59 +01:00
Víctor Falcón 75736f3e59
fix(auth): allow forced registration (#307)
## Summary
- allow `/register?force=1` to bypass the hidden-auth redirect and
render the registration page
- preserve the `force=1` query on form submit so hidden signup still
works end-to-end
- enforce hidden-signup blocking in the Fortify user creation path and
cover the forced/unforced flows with tests
2026-04-20 10:00:52 +01:00
Víctor Falcón fde5405777
fix(user): persist detected timezones (#296)
## Summary
- store browser-detected IANA timezones on users during registration and
share the value to the frontend
- silently backfill missing timezones for authenticated users without
overwriting existing saved values
- add focused feature coverage for registration, shared props, and the
timezone backfill endpoint

## Testing
- php artisan test --compact tests/Feature/Auth/RegistrationTest.php
tests/Feature/InertiaSharedDataTest.php
tests/Feature/Settings/TimezoneTest.php
- npm test -- resources/js/utils/currency.test.ts
2026-04-16 11:36:57 +01:00
Víctor Falcón 1b0f3ba24d fix: Disable email verification on dev/local 2026-02-07 17:52:53 +01:00
Víctor Falcón 370d388d99
feat: Enable email verification on sign up (#97)
## Summary
- Enables `MustVerifyEmail` on the `User` model so new users must verify
their email before accessing protected routes
- Unverified users are redirected to `/email/verify` when attempting to
access routes behind the `verified` middleware (subscribe, onboarding,
dashboard)
- A verification email is automatically sent on registration via Fortify

## Screenshot

![Email verification
page](https://raw.githubusercontent.com/whisper-money/whisper-money/mail-verification/storage/videos/email-verification-page.png)

## Video


https://github.com/whisper-money/whisper-money/raw/mail-verification/storage/videos/ac0945c527f014b9cd657f18c911f496.webm

## Test plan
- [x] New test: registration sends a `VerifyEmail` notification
- [x] New test: newly registered users have `email_verified_at` as null
- [x] New test: unverified users are redirected to `verification.notice`
from protected routes (subscribe, onboarding, dashboard)
- [x] New test: verified users are not redirected to verification notice
- [x] Existing email verification and notification tests still pass (31
auth tests, 52 settings tests)
- [x] Browser walkthrough: registration → redirected to `/email/verify`
page with "Resend verification email" button
2026-02-03 10:15:07 +01:00
Víctor Falcón 3d0d6c8bef feat(queue): Implement queueable email jobs with rate limiting 2025-12-30 07:22:18 +01:00
Víctor Falcón c433088806
User Onboarding Flow (#23)
## Summary

Implements a complete user onboarding flow that guides new users through
setting up their account after registration.

## Features

- **Step-by-step wizard UI** with progress indicator and smooth
animations
- **E2E Encryption setup** with password strength indicator and
explanation of how encryption works
- **Account creation** supporting all account types (checking, savings,
credit card, investment, pension)
- **Category customization** with explanation of category types
(expenses, income, transfer)
- **Smart rules explanation** covering why there is no AI
auto-categorization (privacy & E2EE)
- **Transaction/balance import** based on account type
- **Sync integration** to ensure data consistency with backend

## Flow Diagram

```mermaid
flowchart TD
    A[User Registration] --> B[Welcome]

    B --> E[Encryption Explained]
    
    E --> C{Has encryption key?}

    C -->|No|F[Encryption Setup]
    C -->|Yes|G{Has Existing Accounts?}
    G -->|Yes| H[Show Existing Accounts]
    G -->|No| I[Create First Account]
    
    H --> J[Continue]
    I --> K{Account Type?}
    
    K -->|Checking/Savings/Credit Card| L[Import Transactions]
    K -->|Investment/Pension| M[Import Balances]
    
    J --> N[Category Types Explanation]
    L --> N
    M --> N
    
    N --> O[Customize Categories]
    O --> P[Smart Rules Explanation]
    P --> Q[More Accounts?]
    
    Q -->|Add More| I
    Q -->|Finish| R[Complete]
    
    R --> S[Redirect to Dashboard]
    S --> T{Subscribed?}
    T -->|No| U[Subscribe Page]
    T -->|Yes| V[Dashboard]
```

## Technical Changes

### Backend
- Added `onboarded_at` field to users table with migration
- Created `EnsureOnboardingComplete` middleware for redirect logic
- Created `OnboardingController` with index and complete actions
- Custom `RegisterResponse` to redirect new users to onboarding
- Updated `AccountController::store` to return JSON for fetch requests

### Frontend
- `OnboardingLayout` - fullscreen layout with step progress
- `useOnboardingState` hook - manages step navigation and state
- 12 step components for each onboarding screen
- Backend sync after account creation and imports

### Tests
- Feature tests for onboarding middleware
- Updated existing tests to use `onboarded()` factory state
2025-12-12 13:06:08 +01:00
Víctor Falcón 292297f483 E2E Encryption 2025-11-07 14:21:25 +00:00
Víctor Falcón 2eaad3204c Install Pest 2025-11-07 12:01:58 +00:00
Víctor Falcón fb140bbece Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00