From 9a5b93dd0866eb07f102dee92d6d318ca419fcc7 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 05:09:34 -0400 Subject: [PATCH] feat(api): early-access signup endpoint (POST /api/early-access) Real @Public() NestJS endpoint persisting to the existing early_access_signups table (email + server_count), matching the schema exactly (no migration). Duplicate-email safe (pre-check + unique-constraint catch -> friendly success). Wired into app.module. Makes the marketing early-access form functional end-to-end on next API deploy. tsc/nest build green. Co-Authored-By: Claude Opus 4.8 --- backend-nest/src/app.module.ts | 2 + .../dto/create-early-access.dto.ts | 14 +++++++ .../early-access/early-access.controller.ts | 19 +++++++++ .../early-access/early-access.module.ts | 12 ++++++ .../early-access/early-access.service.ts | 42 +++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 backend-nest/src/modules/early-access/dto/create-early-access.dto.ts create mode 100644 backend-nest/src/modules/early-access/early-access.controller.ts create mode 100644 backend-nest/src/modules/early-access/early-access.module.ts create mode 100644 backend-nest/src/modules/early-access/early-access.service.ts diff --git a/backend-nest/src/app.module.ts b/backend-nest/src/app.module.ts index 0777091..018dc41 100644 --- a/backend-nest/src/app.module.ts +++ b/backend-nest/src/app.module.ts @@ -44,6 +44,7 @@ import { FurnaceSplitterModule } from './modules/furnacesplitter/furnacesplitter import { BetterChatModule } from './modules/betterchat/betterchat.module'; import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module'; import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module'; +import { EarlyAccessModule } from './modules/early-access/early-access.module'; // Shared Services import { NatsService } from './services/nats.service'; @@ -123,6 +124,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway'; BetterChatModule, TimedExecuteModule, RaidableBasesModule, + EarlyAccessModule, ], providers: [ // Global guards (order matters: auth first, then license, then permissions) diff --git a/backend-nest/src/modules/early-access/dto/create-early-access.dto.ts b/backend-nest/src/modules/early-access/dto/create-early-access.dto.ts new file mode 100644 index 0000000..7240698 --- /dev/null +++ b/backend-nest/src/modules/early-access/dto/create-early-access.dto.ts @@ -0,0 +1,14 @@ +import { IsEmail, IsOptional, IsString, MaxLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateEarlyAccessDto { + @ApiProperty({ example: 'admin@example.com' }) + @IsEmail() + email: string; + + @ApiPropertyOptional({ example: 'rust', description: 'Primary game interest or server count' }) + @IsOptional() + @IsString() + @MaxLength(10) + server_count?: string; +} diff --git a/backend-nest/src/modules/early-access/early-access.controller.ts b/backend-nest/src/modules/early-access/early-access.controller.ts new file mode 100644 index 0000000..ab2bba9 --- /dev/null +++ b/backend-nest/src/modules/early-access/early-access.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Public } from '../../common/decorators/public.decorator'; +import { EarlyAccessService } from './early-access.service'; +import { CreateEarlyAccessDto } from './dto/create-early-access.dto'; + +@ApiTags('early-access') +@Controller() +export class EarlyAccessController { + constructor(private readonly earlyAccessService: EarlyAccessService) {} + + @Public() + @Post('early-access') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Register for early access' }) + async register(@Body() dto: CreateEarlyAccessDto) { + return this.earlyAccessService.register(dto); + } +} diff --git a/backend-nest/src/modules/early-access/early-access.module.ts b/backend-nest/src/modules/early-access/early-access.module.ts new file mode 100644 index 0000000..b29d0c2 --- /dev/null +++ b/backend-nest/src/modules/early-access/early-access.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EarlyAccessSignup } from '../../entities/early-access-signup.entity'; +import { EarlyAccessController } from './early-access.controller'; +import { EarlyAccessService } from './early-access.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([EarlyAccessSignup])], + controllers: [EarlyAccessController], + providers: [EarlyAccessService], +}) +export class EarlyAccessModule {} diff --git a/backend-nest/src/modules/early-access/early-access.service.ts b/backend-nest/src/modules/early-access/early-access.service.ts new file mode 100644 index 0000000..281ad21 --- /dev/null +++ b/backend-nest/src/modules/early-access/early-access.service.ts @@ -0,0 +1,42 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EarlyAccessSignup } from '../../entities/early-access-signup.entity'; +import { CreateEarlyAccessDto } from './dto/create-early-access.dto'; + +@Injectable() +export class EarlyAccessService { + private readonly logger = new Logger(EarlyAccessService.name); + + constructor( + @InjectRepository(EarlyAccessSignup) + private readonly repo: Repository, + ) {} + + async register(dto: CreateEarlyAccessDto): Promise<{ success: true; alreadyRegistered: boolean }> { + const existing = await this.repo.findOne({ where: { email: dto.email } }); + if (existing) { + // Duplicate email — return friendly success rather than a 409 that would break the UX + return { success: true, alreadyRegistered: true }; + } + + const signup = this.repo.create({ + email: dto.email, + server_count: dto.server_count ?? 'not specified', + }); + + try { + await this.repo.save(signup); + } catch (err: unknown) { + // Guard against a race-condition duplicate (unique constraint violation) + const pg = err as { code?: string }; + if (pg.code === '23505') { + return { success: true, alreadyRegistered: true }; + } + this.logger.error('Failed to save early-access signup', err); + throw err; + } + + return { success: true, alreadyRegistered: false }; + } +}