UNPKG

codalware-auth

Version:

Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.

631 lines (517 loc) 17.6 kB
#!/usr/bin/env node import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const JS_EXTENSIONS = new Set([".js", ".cjs", ".mjs", ".ts", ".tsx", ".jsx"]); function isScriptFile(filePath) { return JS_EXTENSIONS.has(path.extname(filePath)); } function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function readFile(filePath, encoding = "utf8") { return fs.readFileSync(filePath, encoding); } function writeFile(filePath, content, { overwrite = true } = {}) { if (!overwrite && fs.existsSync(filePath)) { return false; } ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, content); return true; } function copyFile(sourcePath, targetPath, options = {}) { const { transform } = options; ensureDir(path.dirname(targetPath)); let buffer = fs.readFileSync(sourcePath); if (typeof transform === "function") { const transformed = transform({ sourcePath, targetPath, content: buffer, isScript: isScriptFile(sourcePath), }); if (typeof transformed === "string") { buffer = Buffer.from(transformed, "utf8"); } else if (Buffer.isBuffer(transformed)) { buffer = transformed; } } fs.writeFileSync(targetPath, buffer); return targetPath; } function copyDirectoryRecursive(sourceDir, targetDir, options = {}) { const { filter } = options; if (!fs.existsSync(sourceDir)) { return []; } ensureDir(targetDir); const copied = []; const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name); const targetPath = path.join(targetDir, entry.name); if ( typeof filter === "function" && !filter({ entry, sourcePath, targetPath }) ) { continue; } if (entry.isDirectory()) { copied.push(...copyDirectoryRecursive(sourcePath, targetPath, options)); } else if (entry.isFile()) { copied.push(copyFile(sourcePath, targetPath, options)); } } return copied; } function rewriteImportsToLocal(code, replacements) { if (!Array.isArray(replacements) || replacements.length === 0) { return code; } const ordered = [...replacements].sort( (a, b) => b.from.length - a.from.length, ); const normalize = (value) => value.replace(/\\/g, "/"); const replaceSpecifier = (specifier) => { const normalized = normalize(specifier); for (const entry of ordered) { const { from, to } = entry; if (normalized === from) { return to; } if (normalized.startsWith(`${from}/`)) { const remainder = normalized.slice(from.length + 1); return `${to}/${remainder}`; } } return specifier; }; const esmPattern = /(from\s+['"])([^'"\n]+)(['"])/g; const dynamicPattern = /(import\s*\(\s*['"])([^'"\n]+)(['"]\s*\))/g; const requirePattern = /(require\s*\(\s*['"])([^'"\n]+)(['"]\s*\))/g; let output = code.replace(esmPattern, (match, prefix, specifier, suffix) => { const next = replaceSpecifier(specifier); return next === specifier ? match : `${prefix}${next}${suffix}`; }); output = output.replace( dynamicPattern, (match, prefix, specifier, suffix) => { const next = replaceSpecifier(specifier); return next === specifier ? match : `${prefix}${next}${suffix}`; }, ); output = output.replace( requirePattern, (match, prefix, specifier, suffix) => { const next = replaceSpecifier(specifier); return next === specifier ? match : `${prefix}${next}${suffix}`; }, ); return output; } const PACKAGE_ROOT = path.resolve(__dirname, ".."); const SOURCE_ROOT = path.join(PACKAGE_ROOT, "src"); const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "templates"); function pathExists(targetPath) { try { fs.accessSync(targetPath); return true; } catch (error) { return false; } } function removeDirectory(targetPath) { if (pathExists(targetPath)) { fs.rmSync(targetPath, { recursive: true, force: true }); } } function detectProjectStructure(projectRoot) { const srcDir = path.join(projectRoot, "src"); const usesSrcDirectory = pathExists(srcDir); const baseTarget = usesSrcDirectory ? srcDir : projectRoot; return { projectRoot, baseTarget, usesSrcDirectory, }; } function detectRouterType(structure) { const { projectRoot, usesSrcDirectory } = structure; const base = usesSrcDirectory ? path.join(projectRoot, "src") : projectRoot; if (pathExists(path.join(base, "app"))) { return "app"; } if (pathExists(path.join(base, "pages"))) { return "pages"; } return "app"; } function getImportReplacements(mode, aliasPrefix = "@/") { const prefix = aliasPrefix.endsWith("/") ? aliasPrefix : `${aliasPrefix}/`; const namespace = mode === "full" ? `${prefix}authcore` : prefix; const componentsTarget = mode === "full" ? `${namespace}/components` : `${prefix}components`; const hooksTarget = mode === "full" ? `${namespace}/hooks` : `${prefix}hooks`; const libTarget = mode === "full" ? `${namespace}/lib` : `${prefix}lib`; const validationTarget = mode === "full" ? `${namespace}/validation` : `${prefix}validation`; const utilsTarget = mode === "full" ? `${namespace}/utils` : `${prefix}utils`; const emailTarget = mode === "full" ? `${namespace}/email` : `${prefix}email`; const i18nTarget = mode === "full" ? `${namespace}/i18n` : `${prefix}i18n`; const typesTarget = mode === "full" ? `${namespace}/types` : `${prefix}types`; const rootIndexTarget = mode === "full" ? `${namespace}/index` : `${prefix}index`; const stylesTarget = `${prefix}styles`; return [ { from: "codalware-auth/styles/tailwind-preset", to: `${stylesTarget}/tailwind-preset`, }, { from: "codalware-auth/styles/theme.css", to: `${stylesTarget}/theme.css`, }, { from: "codalware-auth/styles", to: stylesTarget }, { from: "codalware-auth/server", to: `${libTarget}/auth` }, { from: "codalware-auth/validation", to: validationTarget }, { from: "codalware-auth/utils", to: utilsTarget }, { from: "codalware-auth/hooks", to: hooksTarget }, { from: "codalware-auth/types", to: typesTarget }, { from: "codalware-auth/email", to: emailTarget }, { from: "codalware-auth/i18n", to: i18nTarget }, { from: "codalware-auth/components", to: componentsTarget }, { from: "codalware-auth", to: rootIndexTarget }, ]; } function createImportTransform({ mode, alias = "@/" } = {}) { const replacements = getImportReplacements(mode, alias); return (params) => { const { content, isScript } = params; if (!isScript) { return content; } return rewriteImportsToLocal(content.toString("utf8"), replacements); }; } function copySupplementaryResources(structure, options = {}) { const { projectRoot } = structure; const { transform } = options; const directoryMappings = [ { source: path.join(PACKAGE_ROOT, "config"), target: path.join(projectRoot, "config"), }, { source: path.join(PACKAGE_ROOT, "locales"), target: path.join(projectRoot, "locales"), }, { source: path.join(PACKAGE_ROOT, "styles"), target: path.join(projectRoot, "styles"), }, { source: path.join(PACKAGE_ROOT, "prisma"), target: path.join(projectRoot, "prisma"), }, ]; const fileMappings = [ { source: path.join(PACKAGE_ROOT, "config.ts"), target: path.join(projectRoot, "config.ts"), }, { source: path.join(PACKAGE_ROOT, "env.ts"), target: path.join(projectRoot, "env.ts"), }, ]; const copied = []; for (const mapping of directoryMappings) { if (!pathExists(mapping.source)) { continue; } copied.push( ...copyDirectoryRecursive( mapping.source, mapping.target, transform ? { transform } : {}, ), ); } for (const mapping of fileMappings) { if (!pathExists(mapping.source)) { continue; } copied.push( copyFile(mapping.source, mapping.target, transform ? { transform } : {}), ); } return copied.filter(Boolean); } function copyRouterTemplates(structure, options = {}) { const { baseTarget } = structure; const { router, transform } = options; const copied = []; if (router === "app") { const appTemplate = path.join(TEMPLATE_ROOT, "app"); if (!pathExists(appTemplate)) { return copied; } const entries = fs.readdirSync(appTemplate, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(appTemplate, entry.name); const targetPath = path.join(baseTarget, "app", entry.name); if (entry.isDirectory()) { copied.push( ...copyDirectoryRecursive( sourcePath, targetPath, transform ? { transform } : {}, ), ); } else if (entry.isFile()) { copied.push( copyFile(sourcePath, targetPath, transform ? { transform } : {}), ); } } return copied.filter(Boolean); } if (router === "pages") { const pagesTemplate = path.join(TEMPLATE_ROOT, "pages"); if (!pathExists(pagesTemplate)) { return copied; } const entries = fs.readdirSync(pagesTemplate, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(pagesTemplate, entry.name); const targetPath = path.join(baseTarget, "pages", entry.name); if (entry.isDirectory()) { copied.push( ...copyDirectoryRecursive( sourcePath, targetPath, transform ? { transform } : {}, ), ); } else if (entry.isFile()) { copied.push( copyFile(sourcePath, targetPath, transform ? { transform } : {}), ); } } } return copied.filter(Boolean); } function createEnvExample(projectRoot) { const envPath = path.join(projectRoot, ".env.example"); if (pathExists(envPath)) { return false; } const envContent = String.raw`# AuthCore environment variables # Copy this file to .env.local and set the values for your environment. # Database connection DATABASE_URL="postgresql://username:password@localhost:5432/authcore" # Email configuration (choose one provider) GMAIL_ACCOUNT_EMAIL="" GOOGLE_PASS="" EMAIL_SERVER_HOST="" EMAIL_SERVER_PORT="587" EMAIL_SERVER_USER="" EMAIL_SERVER_PASSWORD="" # Initial admin user ADMIN_EMAIL="admin@example.com" ADMIN_PASSWORD="change-me" ADMIN_NAME="Admin User" # Optional NextAuth configuration NEXTAUTH_SECRET="" NEXTAUTH_URL="http://localhost:3000" # Optional provider credentials # GOOGLE_CLIENT_ID="" # GOOGLE_CLIENT_SECRET="" # GITHUB_CLIENT_ID="" # GITHUB_CLIENT_SECRET="" # Optional billing configuration # STRIPE_SECRET_KEY="" # STRIPE_WEBHOOK_SECRET="" # NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" # Optional Resend configuration # RESEND_API_KEY="" `; writeFile(envPath, envContent, { overwrite: false }); return true; } function createTemplateReadme(projectRoot, mode) { const readmePath = path.join(projectRoot, "AUTHCORE_TEMPLATE_README.md"); if (mode === "full") { const content = String.raw`# AuthCore Full Template AuthCore has been installed in Full Template Mode. The complete source code now lives in your project under \`src/authcore\`. ## What You Received - All React components in \`src/authcore/components\` - Authentication hooks in \`src/authcore/hooks\` - Server utilities in \`src/authcore/lib\` and \`src/authcore/auth\` - Validation schemas in \`src/authcore/validation\` - TypeScript definitions in \`src/authcore/types\` - Email templates in \`src/authcore/email\` - Internationalization utilities in \`src/authcore/i18n\` ## Imports Instead of importing from \`codalware-auth\`, reference your local code: \`\`\`ts import { AuthForm } from '@/authcore/components'; import { useAuth } from '@/authcore/hooks'; import { AuthService } from '@/authcore/lib/auth'; import type { AuthUser } from '@/authcore/types/auth'; \`\`\` Ensure your TypeScript configuration maps \`@/*\` to the \`src\` directory (Next.js does this by default). ## Customization Tips - Update UI components under \`src/authcore/components\` - Adjust authentication flows in \`src/authcore/lib/auth\` - Modify validation in \`src/authcore/validation\` - Extend Prisma schema in \`prisma/schema.prisma\` and run migrations ## Supporting Files - Tailwind preset and global styles in \`styles/\` - Localization dictionaries in \`locales/\` - Prisma setup in \`prisma/\` - Environment template in \`.env.example\` ## Next Steps 1. Copy \`.env.example\` to \`.env.local\` and set your secrets. 2. Run Prisma migrations. 3. Start the application and verify the authentication flows. `; writeFile(readmePath, content, { overwrite: true }); return; } const content = String.raw`# AuthCore Main Template AuthCore has been installed in Main Template Mode. The AuthCore source now lives directly inside your application \`src\` directory. ## What You Received - Components in \`src/components\` - Hooks in \`src/hooks\` - Server utilities in \`src/lib\` and \`src/auth\` - Validation schemas in \`src/validation\` - Type definitions in \`src/types\` - Email templates in \`src/email\` - Internationalization in \`src/i18n\` ## Imports Use your local modules instead of \`codalware-auth\`: \`\`\`ts import { AuthForm } from '@/components'; import { useAuth } from '@/hooks'; import { AuthService } from '@/lib/auth'; import type { AuthUser } from '@/types/auth'; \`\`\` ## Next Steps 1. Copy \`.env.example\` to \`.env.local\` and set connection details. 2. Adjust the UI and server logic to match your requirements. 3. Run \`npm run migrate\` or the equivalent Prisma command to set up the database. `; writeFile(readmePath, content, { overwrite: true }); } function copyMainTemplate(options = {}) { const structure = detectProjectStructure(process.cwd()); const router = options.router ?? detectRouterType(structure); const transform = createImportTransform({ mode: "main", alias: options.alias ?? "@/", }); const transformOptions = { transform }; console.log("Copying AuthCore main template..."); console.log(`Source: ${SOURCE_ROOT}`); console.log(`Target: ${structure.baseTarget}`); const copied = []; copied.push( ...copyDirectoryRecursive( SOURCE_ROOT, structure.baseTarget, transformOptions, ), ); copied.push(...copySupplementaryResources(structure, transformOptions)); copied.push(...copyRouterTemplates(structure, { router, transform })); createTemplateReadme(structure.projectRoot, "main"); createEnvExample(structure.projectRoot); console.log(`Copied ${copied.length} files for main template.`); return copied; } function copyFullTemplate(options = {}) { const structure = detectProjectStructure(process.cwd()); const router = options.router ?? detectRouterType(structure); const namespaceTarget = path.join(structure.baseTarget, "authcore"); const transform = createImportTransform({ mode: "full", alias: options.alias ?? "@/", }); const transformOptions = { transform }; console.log("Copying AuthCore full template..."); console.log(`Source: ${SOURCE_ROOT}`); console.log(`Namespace target: ${namespaceTarget}`); removeDirectory(namespaceTarget); ensureDir(namespaceTarget); const copied = []; copied.push( ...copyDirectoryRecursive(SOURCE_ROOT, namespaceTarget, transformOptions), ); copied.push(...copySupplementaryResources(structure, transformOptions)); copied.push(...copyRouterTemplates(structure, { router, transform })); createTemplateReadme(structure.projectRoot, "full"); createEnvExample(structure.projectRoot); console.log(`Copied ${copied.length} files for full template.`); return copied; } function parseArguments(argv) { const options = { mode: "full" }; for (const arg of argv) { if (arg === "--main" || arg === "-m") { options.mode = "main"; } else if (arg === "--full" || arg === "-f") { options.mode = "full"; } else if (arg.startsWith("--mode=")) { const value = arg.split("=")[1]; if (value === "main" || value === "full") { options.mode = value; } } else if (arg.startsWith("--router=")) { const value = arg.split("=")[1]; if (value === "app" || value === "pages") { options.router = value; } } else if (arg.startsWith("--alias=")) { const value = arg.split("=")[1]; if (value) { options.alias = value; } } } return options; } function runCli() { const args = parseArguments(process.argv.slice(2)); const action = args.mode === "main" ? copyMainTemplate : copyFullTemplate; try { action(args); console.log("Template copy complete."); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Failed to copy template:", message); process.exitCode = 1; } } if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { runCli(); } export { __dirname, copyFullTemplate, copyMainTemplate, copyDirectoryRecursive, copyFile, ensureDir, isScriptFile, readFile, rewriteImportsToLocal, createImportTransform, writeFile, };