feat: sase.tr v2 full application implementation
Complete rewrite of sase.tr VIN lookup platform with modern stack: Backend (NestJS 10 + Drizzle ORM + PostgreSQL + Redis + BullMQ): - 34 DB models (core + PL24 + EMEX schemas) - Auth via Better Auth (email/password + social) - Brands, Plans, Subscriptions, Payments (iyzico + EFT) - VIN decode orchestration (Corgi + PL24 + EMEX + NHTSA) - Interactive schema viewer backend (MinIO storage) - EMEX scraping integration (Puppeteer + BullMQ workers) - Translation module (EN→TR automotive dictionary) - Admin dashboard API (stats, user mgmt, payment approval) - Rate limiting, Helmet security, file upload validation Frontend (Next.js 15 + Tailwind v4 + shadcn/ui + TanStack Query + Zustand): - 20 routes: auth, dashboard, VIN search, schema viewer, admin - Interactive schema viewer with zoom/pan/hotspot highlighting - Subscription management with brand selector - Payment flow (iyzico 3D Secure + EFT with receipt upload) - i18n support (TR/EN) - Error boundaries, loading skeletons, 404 page Infrastructure: - 85 tests (52 backend + 33 frontend, Vitest) - CI/CD (GitHub Actions: lint, typecheck, test, build, deploy) - Zero-downtime deploy script (PM2) - Env validation script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
apps/api/src/auth/auth.controller.ts
Normal file
14
apps/api/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { All, Controller, Req, Res } from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { getAuth } from "./auth";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
@All("*path")
|
||||
async handleAuth(@Req() req: Request, @Res() res: Response) {
|
||||
const auth = getAuth();
|
||||
const handler = toNodeHandler(auth);
|
||||
return handler(req, res);
|
||||
}
|
||||
}
|
||||
21
apps/api/src/auth/auth.module.ts
Normal file
21
apps/api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { createAuth } from "./auth";
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule implements OnModuleInit {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
onModuleInit() {
|
||||
const databaseUrl = this.configService.get<string>("database.url")!;
|
||||
const secret = this.configService.get<string>("auth.secret")!;
|
||||
const baseUrl = this.configService.get<string>("auth.url")!;
|
||||
createAuth(databaseUrl, secret, baseUrl);
|
||||
}
|
||||
}
|
||||
15
apps/api/src/auth/auth.service.ts
Normal file
15
apps/api/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { getAuth } from "./auth";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
getAuthInstance() {
|
||||
return getAuth();
|
||||
}
|
||||
|
||||
async getSession(headers: Headers) {
|
||||
const auth = getAuth();
|
||||
const session = await auth.api.getSession({ headers });
|
||||
return session;
|
||||
}
|
||||
}
|
||||
70
apps/api/src/auth/auth.ts
Normal file
70
apps/api/src/auth/auth.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "../database/schema/core";
|
||||
|
||||
let authInstance: ReturnType<typeof betterAuth> | null = null;
|
||||
|
||||
export function createAuth(databaseUrl: string, secret: string, baseUrl: string) {
|
||||
if (authInstance) return authInstance;
|
||||
|
||||
const client = postgres(databaseUrl, { max: 5 });
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
authInstance = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
usePlural: true,
|
||||
}),
|
||||
secret,
|
||||
baseURL: baseUrl,
|
||||
basePath: "/api/auth",
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8,
|
||||
},
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
||||
enabled: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),
|
||||
},
|
||||
},
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 60 * 5, // 5 minutes
|
||||
},
|
||||
},
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
defaultValue: "user",
|
||||
input: false,
|
||||
},
|
||||
referralCode: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
referredBy: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedOrigins: (process.env.CORS_ORIGIN || "http://localhost:3000").split(","),
|
||||
});
|
||||
|
||||
return authInstance;
|
||||
}
|
||||
|
||||
export function getAuth() {
|
||||
if (!authInstance) {
|
||||
throw new Error("Auth not initialized. Call createAuth first.");
|
||||
}
|
||||
return authInstance;
|
||||
}
|
||||
Reference in New Issue
Block a user