Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
5 changes: 3 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
74 changes: 54 additions & 20 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}

Expand Down
Loading
Loading