feat: add OEM code copy tracking, PostHog analytics, Postal email integration

- Add oem_code_copies table and analytics module for tracking part code copies
- Integrate PostHog for frontend product analytics (VIN decode, OEM copy events)
- Switch email service from stub to Postal API with proper error handling
- Add copy button to parts panel with clipboard + server-side logging
- Add admin copy-logs page with filtering and top-copied-codes view
- Add VIN report endpoint for users to flag unrecognized chassis numbers
- Add Postal email env vars to config schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-17 18:09:07 +00:00
parent dcbeb83ccc
commit f2126754a6
42 changed files with 1307 additions and 223 deletions

View File

@@ -2,6 +2,7 @@ import { Module, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { EmailService } from "../email/email.service";
import { createAuth } from "./auth";
@Module({
@@ -10,7 +11,10 @@ import { createAuth } from "./auth";
exports: [AuthService],
})
export class AuthModule implements OnModuleInit {
constructor(private configService: ConfigService) {}
constructor(
private configService: ConfigService,
private emailService: EmailService,
) {}
onModuleInit() {
const databaseUrl = this.configService.get<string>("database.url")!;
@@ -18,6 +22,9 @@ export class AuthModule implements OnModuleInit {
const baseUrl = this.configService.get<string>("auth.url")!;
const googleClientId = this.configService.get<string>("auth.googleClientId");
const googleClientSecret = this.configService.get<string>("auth.googleClientSecret");
createAuth(databaseUrl, secret, baseUrl, { googleClientId, googleClientSecret });
createAuth(databaseUrl, secret, baseUrl, {
social: { googleClientId, googleClientSecret },
emailService: this.emailService,
});
}
}

View File

@@ -5,6 +5,7 @@ import postgres from "postgres";
import { randomUUID } from "crypto";
import * as schema from "../database/schema/core";
import type { EmailService } from "../email/email.service";
let authInstance: ReturnType<typeof betterAuth> | null = null;
@@ -13,7 +14,12 @@ interface SocialCredentials {
googleClientSecret?: string;
}
export function createAuth(databaseUrl: string, secret: string, baseUrl: string, social?: SocialCredentials) {
interface AuthOptions {
social?: SocialCredentials;
emailService?: EmailService;
}
export function createAuth(databaseUrl: string, secret: string, baseUrl: string, options?: AuthOptions) {
if (authInstance) return authInstance;
const client = postgres(databaseUrl, { max: 5 });
@@ -35,13 +41,30 @@ export function createAuth(databaseUrl: string, secret: string, baseUrl: string,
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
sendResetPassword: async (data) => {
if (options?.emailService) {
await options.emailService.sendPasswordReset(data.user.email, data.url);
} else {
console.log(`[DEV] Password reset URL for ${data.user.email}: ${data.url}`);
}
},
},
...(social?.googleClientId && social?.googleClientSecret
emailVerification: {
sendVerificationEmail: async (data) => {
if (options?.emailService) {
await options.emailService.sendEmailVerification(data.user.email, data.url);
} else {
console.log(`[DEV] Verification URL for ${data.user.email}: ${data.url}`);
}
},
sendOnSignUp: true,
},
...(options?.social?.googleClientId && options?.social?.googleClientSecret
? {
socialProviders: {
google: {
clientId: social.googleClientId,
clientSecret: social.googleClientSecret,
clientId: options.social.googleClientId,
clientSecret: options.social.googleClientSecret,
},
},
}