Add password reset flow
Users with a confirmed email can now reset a forgotten password via emailed single-use links. Backend - New PasswordResetToken entity (hash-only, 15-minute TTL). - POST /api/auth/forgot-password always returns 200, never disclosing whether an email is registered. Internally only emits a reset email when a user exists with EmailConfirmedAt set, and burns any existing outstanding tokens for that user before issuing a new one. - POST /api/auth/reset-password validates the token, rotates the password hash, and consumes the token. Single-use, expiry-checked. - Both endpoints are rate-limited (forgot 5/hr, reset 10/15min) per the same partitioning the login/register endpoints already use. - Reset email template added; uses AppBaseUrl plumbed in the previous PR. Frontend - /forgot-password page (email field, generic confirmation message regardless of whether the email is registered). - /reset-password page reads ?token=, validates the new password client-side, posts to the API, then redirects to /login. - "Forgot your password?" link added under the login form. Tests - 9 new integration tests cover the happy path, single-use enforcement, expired/unknown tokens, short-password rejection, silent 200 for unknown email, no email for unconfirmed users, and outstanding-token invalidation when a fresh request is made.
This commit is contained in:
@@ -122,6 +122,41 @@ namespace YesChef.Api.Migrations
|
||||
b.ToTable("Invites");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.PasswordResetToken", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ConsumedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("IssuedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId", "ConsumedAt");
|
||||
|
||||
b.ToTable("PasswordResetTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -451,6 +486,17 @@ namespace YesChef.Api.Migrations
|
||||
b.Navigation("IssuedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.PasswordResetToken", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Recipe", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.User", "CreatedByUser")
|
||||
|
||||
Reference in New Issue
Block a user