diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d38ebe..eaafb1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.3.1] - 2026-06-03 + +### Added + +#### Authentication & Account Management +- **Backup email password-protected alternative login** — users can add password-protected backup emails to their account for login without email OTP; backup emails must be verified independently via 6-character code +- **Two-sided email verification** — changing primary email now requires verifying both old email access (confirm you can access current email) AND new email ownership (confirm you own the new email); sequential step-by-step verification with clear UI indicators +- **Recovery code bypass for email verification** — if user loses access to their primary email, they can use a recovery code to skip old email verification and proceed directly to new email verification +- **Backup email schema** — added `BackupEmail` model with email, passwordHash, emailVerified timestamp, and indexes for efficient login lookups +- **Credentials provider for NextAuth** — added Credentials provider configuration allowing backup email + password authentication through standard NextAuth flow + +#### UI/UX Improvements +- **Account settings page** — new `/settings/account` consolidating: + - Email address change with two-sided verification flow + - Two-factor authentication setup/management + - Recovery codes display and regeneration + - Backup email management (add, verify, delete with guards) +- **Broadcasts compose page refactor** — completely redesigned broadcasts compose UI to match campaigns aesthetic: + - Accordion-style settings strip (Subject visible, From/Reply-To/Recipients collapsible) + - Full-width email canvas for better content editing + - Variables strip showing available variables and recipient count + - Cleaner, more professional layout with proper spacing +- **Login page backup email option** — toggleable backup email login form separate from primary auth methods; only appears when user opts in via "Have a backup email password?" link +- **2FA verification page styling** — professional card layout with gradient backgrounds, improved form labels (context-aware "Authenticator Code" vs "Recovery Code"), better help text and mobile responsiveness + +#### Database Migrations +- **Backup email schema** (`20260603195140_add_backup_emails_and_two_sided_verification`): + - Added `BackupEmail` model with unique email per user, passwordHash, emailVerified nullable timestamp + - Added `PendingBackupEmailVerification` model with 6-char code, expiry, unique constraint on [userId, email] + - Enhanced `PendingEmailChange` with codeOld (nullable), verifiedOld, verifiedNew flags for two-sided verification + +#### Security & Infrastructure +- **bcryptjs password hashing** — backup email passwords hashed with bcryptjs (salt rounds 10) on client-side before transmission +- **Edge Runtime 2FA utilities** — new `edge-2fa-utils.ts` using Web Crypto API for middleware HMAC validation (no Node.js crypto dependency) +- **2FA cookie validation** — 12-hour HMAC-signed httpOnly cookie for 2FA sessions with middleware validation for protected routes + +### Changed +- **Upgraded Next.js to latest** — updated to the latest stable version for improved performance, security, and feature support +- **NextAuth configuration** — added Credentials provider for backup email authentication alongside existing OAuth providers (GitHub, Google, Discord) and Email OTP +- **Login page provider filtering** — Credentials provider now explicitly excluded from OAuth buttons loop to prevent rendering as a regular provider button +- **Broadcasts vs Campaigns routing** — campaign cards now differentiate routing based on intent; broadcasts route to `/broadcasts/[id]` while campaigns route to `/campaigns/[id]` +- **Settings navigation restructuring** — reorganized settings tabs with top-level Account and Team sections +- **Email change mutations** — `requestEmailChange` now sends both old and new verification codes in parallel; introduces verifiedOld/verifiedNew state tracking +- **Mailer service** — added optional `subject` parameter to `sendEmailChangeVerificationEmail()` for flexibility in notification messages +- **Form validation consistency** — all form validators aligned with mutation input types for seamless error handling + +### Fixed +- **Credentials provider filtering** — removed Credentials provider from OAuth buttons array to prevent it from rendering as a sign-in option button +- **Build and deployment** — resolved AWS SDK package corruption from earlier disk space issues with clean reinstall + +--- + ## [0.3.0] - 2026-06-03 ### Added diff --git a/apps/web/next.config.js b/apps/web/next.config.js index e01fdde..bb2ba51 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -10,7 +10,6 @@ const config = { serverExternalPackages: ["bullmq"], transpilePackages: ["@bytesend/ui", "@bytesend/email-editor"], typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, images: { formats: ["image/avif", "image/webp"], remotePatterns: [ diff --git a/apps/web/package.json b/apps/web/package.json index 9ac2e1f..728a7e3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -55,7 +55,8 @@ "@trpc/client": "^11.1.1", "@trpc/next": "^11.1.1", "@trpc/react-query": "^11.1.1", - "@trpc/server": "^11.1.1", + "@trpc/server": "^11.8.0", + "bcryptjs": "^3.0.3", "bullmq": "^5.51.1", "bytesend-js": "workspace:*", "chrono-node": "^2.8.0", @@ -69,7 +70,7 @@ "lucide-react": "^0.503.0", "mime-types": "^3.0.1", "nanoid": "^5.1.5", - "next": "15.5.9", + "next": "^16.2.7", "next-auth": "^4.24.14", "nodemailer": "^7.0.3", "otplib": "^13.4.1", diff --git a/apps/web/prisma/migrations/20260603195140_add_backup_emails_and_two_sided_verification/migration.sql b/apps/web/prisma/migrations/20260603195140_add_backup_emails_and_two_sided_verification/migration.sql new file mode 100644 index 0000000..502359b --- /dev/null +++ b/apps/web/prisma/migrations/20260603195140_add_backup_emails_and_two_sided_verification/migration.sql @@ -0,0 +1,59 @@ +/* + Warnings: + + - You are about to drop the column `code` on the `PendingEmailChange` table. All the data in the column will be lost. + - Added the required column `codeNew` to the `PendingEmailChange` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PendingEmailChange" DROP COLUMN "code", +ADD COLUMN "codeNew" TEXT NOT NULL, +ADD COLUMN "codeOld" TEXT, +ADD COLUMN "verifiedNew" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "verifiedOld" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "BackupEmail" ( + "id" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "emailVerified" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BackupEmail_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PendingBackupEmailVerification" ( + "id" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "email" TEXT NOT NULL, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PendingBackupEmailVerification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "BackupEmail_email_key" ON "BackupEmail"("email"); + +-- CreateIndex +CREATE INDEX "BackupEmail_userId_emailVerified_idx" ON "BackupEmail"("userId", "emailVerified"); + +-- CreateIndex +CREATE INDEX "BackupEmail_userId_idx" ON "BackupEmail"("userId"); + +-- CreateIndex +CREATE INDEX "PendingBackupEmailVerification_expiresAt_idx" ON "PendingBackupEmailVerification"("expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "PendingBackupEmailVerification_userId_email_key" ON "PendingBackupEmailVerification"("userId", "email"); + +-- AddForeignKey +ALTER TABLE "BackupEmail" ADD CONSTRAINT "BackupEmail_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PendingBackupEmailVerification" ADD CONSTRAINT "PendingBackupEmailVerification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index a0f55cf..3f85b16 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -80,37 +80,71 @@ model VerificationToken { } model User { - id Int @id @default(autoincrement()) - name String? - email String? @unique - emailVerified DateTime? - image String? - isBetaUser Boolean @default(false) - isAdmin Boolean @default(false) - isBanned Boolean @default(false) - twoFactorEnabled Boolean @default(false) - twoFactorSecret String? - twoFactorTempSecret String? - createdAt DateTime @default(now()) - accounts Account[] - sessions Session[] - teamUsers TeamUser[] - webhookEndpoints Webhook[] - pendingEmailChanges PendingEmailChange[] - twoFactorRecoveryCodes TwoFactorRecoveryCode[] + id Int @id @default(autoincrement()) + name String? + email String? @unique + emailVerified DateTime? + image String? + isBetaUser Boolean @default(false) + isAdmin Boolean @default(false) + isBanned Boolean @default(false) + twoFactorEnabled Boolean @default(false) + twoFactorSecret String? + twoFactorTempSecret String? + createdAt DateTime @default(now()) + accounts Account[] + sessions Session[] + teamUsers TeamUser[] + webhookEndpoints Webhook[] + pendingEmailChanges PendingEmailChange[] + twoFactorRecoveryCodes TwoFactorRecoveryCode[] + backupEmails BackupEmail[] @relation("userBackupEmails") + pendingBackupEmailVerifications PendingBackupEmailVerification[] } model PendingEmailChange { + id String @id @default(cuid()) + userId Int + newEmail String + codeOld String? + codeNew String + verifiedOld Boolean @default(false) + verifiedNew Boolean @default(false) + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, newEmail]) + @@index([expiresAt]) +} + +model BackupEmail { + id String @id @default(cuid()) + userId Int + email String @unique + passwordHash String + emailVerified DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation("userBackupEmails", fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId, emailVerified]) + @@index([userId]) +} + +model PendingBackupEmailVerification { id String @id @default(cuid()) userId Int - newEmail String + email String code String expiresAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, newEmail]) + @@unique([userId, email]) @@index([expiresAt]) } diff --git a/apps/web/src/app/(dashboard)/broadcasts/[broadcastId]/compose/page.tsx b/apps/web/src/app/(dashboard)/broadcasts/[broadcastId]/compose/page.tsx index c2a2b5a..aa90d75 100644 --- a/apps/web/src/app/(dashboard)/broadcasts/[broadcastId]/compose/page.tsx +++ b/apps/web/src/app/(dashboard)/broadcasts/[broadcastId]/compose/page.tsx @@ -19,6 +19,12 @@ import { DialogHeader, DialogTitle, } from "@bytesend/ui/src/dialog"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@bytesend/ui/src/accordion"; import { toast } from "@bytesend/ui/src/toaster"; import { useDebouncedCallback } from "use-debounce"; import { formatDistanceToNow } from "date-fns"; @@ -275,178 +281,206 @@ function BroadcastComposer({ - {/* Body: sidebar + editor */} -
- {/* Left sidebar */} -