diff --git a/backend/src/auth/refresh-token.entity.ts b/backend/src/auth/refresh-token.entity.ts new file mode 100644 index 00000000..206e67b2 --- /dev/null +++ b/backend/src/auth/refresh-token.entity.ts @@ -0,0 +1,20 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Merchant } from '../merchant/merchant.entity'; + +@Entity('refresh_tokens') +export class RefreshToken { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + tokenHash: string; + + @Column() + merchantId: string; + + @ManyToOne(() => Merchant, { onDelete: 'CASCADE' }) + merchant: Merchant; + + @Column() + expiresAt: Date; +} diff --git a/backend/src/merchant/merchant.entity.ts b/backend/src/merchant/merchant.entity.ts new file mode 100644 index 00000000..ff922689 --- /dev/null +++ b/backend/src/merchant/merchant.entity.ts @@ -0,0 +1,28 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('merchants') +export class Merchant { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + email: string; + + @Column() + passwordHash: string; + + @Column({ nullable: true }) + businessName: string; + + @Column({ default: false }) + emailVerified: boolean; + + @Column({ nullable: true, type: 'varchar' }) + emailVerifyToken: string | null; + + @Column({ nullable: true, type: 'timestamptz' }) + emailVerifyExpiry: Date | null; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/backend/src/waitlist/waitlist.entity.ts b/backend/src/waitlist/waitlist.entity.ts new file mode 100644 index 00000000..2b41ecfc --- /dev/null +++ b/backend/src/waitlist/waitlist.entity.ts @@ -0,0 +1,34 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('waitlist') +export class WaitlistEntry { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + email: string; + + @Column({ nullable: true }) + username: string; + + @Column({ nullable: true }) + businessName: string; + + @Column({ nullable: true }) + country: string; + + /** Unique referral code this member can share */ + @Column({ unique: true }) + referralCode: string; + + /** Number of successful referrals made by this member */ + @Column({ default: 0 }) + referralCount: number; + + /** Queue position — lower = earlier */ + @Column() + position: number; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/pr_description.md b/pr_description.md new file mode 100644 index 00000000..61446325 --- /dev/null +++ b/pr_description.md @@ -0,0 +1,41 @@ +## Summary + +Implements four features across the auth and waitlist modules. + +### Issues resolved + +- Closes dupdab/dupdapp_stellar#589 — `POST /api/v1/auth/login`: bcrypt password comparison, JWT signing with configurable secret/expiry, returns `{ accessToken, refreshToken, merchant }` +- Closes dupdab/dupdapp_stellar#590 — `POST /api/v1/auth/refresh`: refresh token rotation, SHA-256 hash stored in DB, expired/used tokens rejected with 401 +- Closes dupdab/dupdapp_stellar#592 — `GET /api/v1/auth/verify-email?token=xxx`: sets `emailVerified` flag, 24-hour expiry, `POST /api/v1/auth/resend-verification` endpoint +- Closes dupdab/dupdapp_stellar#690 — `POST /api/v1/waitlist/join`: generates unique referral code per member, accepts `referralCode` on join, moves referrer up 5 queue positions atomically, rejects self-referrals with 400 + +### Files added + +``` +backend/ + src/ + app.module.ts + auth/ + auth.controller.ts — login, refresh, verify-email, resend-verification + auth.module.ts + auth.service.ts — all auth business logic + refresh-token.entity.ts + merchant/ + merchant.entity.ts — emailVerified, emailVerifyToken, emailVerifyExpiry + waitlist/ + waitlist.controller.ts + waitlist.entity.ts — referralCode, referralCount, position + waitlist.module.ts + waitlist.service.ts — join + referral position logic (atomic transaction) + package.json + tsconfig.json +``` + +### Key implementation notes + +- Refresh tokens stored as SHA-256 hashes — raw token never persisted +- Rotation enforced: each use of a refresh token deletes the old one and issues a new one +- Referral position shift runs inside a TypeORM transaction to prevent race conditions +- Email verification expiry is 24 hours; resend endpoint resets the token and expiry +- JWT payload includes `sub` (merchantId) and `email` per spec +- No new dependencies beyond what a standard NestJS/TypeORM project already uses