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
20 changes: 20 additions & 0 deletions backend/src/auth/refresh-token.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions backend/src/merchant/merchant.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 34 additions & 0 deletions backend/src/waitlist/waitlist.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions pr_description.md
Original file line number Diff line number Diff line change
@@ -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
Loading