feat: add Terms & Conditions checkbox to signup (refs #4)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m38s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

- Add terms_agreements table for legal audit trail
- Create terms-agreement feature capsule with repository
- Modify signup to create terms agreement atomically
- Add checkbox with PDF link to SignupForm
- Capture IP, User-Agent, terms version, content hash
- Update CLAUDE.md documentation index

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-03 12:27:45 -06:00
parent 0391a23bb6
commit dec91ccfc2
14 changed files with 390 additions and 15 deletions

View File

@@ -6,6 +6,8 @@
import { FastifyRequest, FastifyReply } from 'fastify';
import { AuthService } from '../domain/auth.service';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { TermsAgreementRepository } from '../../terms-agreement/data/terms-agreement.repository';
import { termsConfig } from '../../terms-agreement/domain/terms-config';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { signupSchema, resendVerificationPublicSchema } from './auth.validation';
@@ -15,7 +17,22 @@ export class AuthController {
constructor() {
const userProfileRepository = new UserProfileRepository(pool);
this.authService = new AuthService(userProfileRepository);
const termsAgreementRepository = new TermsAgreementRepository(pool);
this.authService = new AuthService(userProfileRepository, termsAgreementRepository);
}
/**
* Extract client IP address from request
* Checks X-Forwarded-For header for proxy scenarios (Traefik)
*/
private getClientIp(request: FastifyRequest): string {
const forwardedFor = request.headers['x-forwarded-for'];
if (forwardedFor) {
// X-Forwarded-For can be comma-separated list; first IP is the client
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
return ips.split(',')[0].trim();
}
return request.ip || 'unknown';
}
/**
@@ -34,9 +51,18 @@ export class AuthController {
});
}
const { email, password } = validation.data;
const { email, password, termsAccepted } = validation.data;
const result = await this.authService.signup({ email, password });
// Extract terms data for audit trail
const termsData = {
ipAddress: this.getClientIp(request),
userAgent: (request.headers['user-agent'] as string) || 'unknown',
termsVersion: termsConfig.version,
termsUrl: termsConfig.url,
termsContentHash: termsConfig.getContentHash(),
};
const result = await this.authService.signup({ email, password, termsAccepted }, termsData);
logger.info('User signup successful', { email, userId: result.userId });

View File

@@ -18,6 +18,9 @@ const passwordSchema = z
export const signupSchema = z.object({
email: z.string().email('Invalid email format'),
password: passwordSchema,
termsAccepted: z.literal(true, {
errorMap: () => ({ message: 'You must agree to the Terms & Conditions to create an account' }),
}),
});
export type SignupInput = z.infer<typeof signupSchema>;