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
JavaScript
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,
};