yantr-js 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1023 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/registry/registry.json +58 -0
- package/registry/templates/auth/auth.controller.ts +125 -0
- package/registry/templates/auth/auth.middleware.ts +116 -0
- package/registry/templates/auth/auth.routes.ts +34 -0
- package/registry/templates/auth/auth.service.ts +140 -0
- package/registry/templates/base/error-handler.ts +127 -0
- package/registry/templates/base/zod-middleware.ts +104 -0
- package/registry/templates/database/db.ts +83 -0
- package/registry/templates/database/prisma.ts +47 -0
- package/registry/templates/logger/http-logger.ts +61 -0
- package/registry/templates/logger/logger.ts +60 -0
- package/registry/templates/security/helmet.ts +88 -0
- package/registry/templates/security/rate-limiter.ts +79 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/lib/installer.ts","../src/lib/config.ts","../src/utils/fs.ts","../src/commands/add.ts","../src/lib/registry.ts","../src/commands/generate.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { init } from './commands/init.js';\nimport { add } from './commands/add.js';\nimport { generate } from './commands/generate.js';\n\nconst program = new Command();\n\nprogram\n .name('yantr')\n .description('A Shadcn for Backend - Production-grade backend scaffolding CLI')\n .version('0.1.0-beta.2');\n\nprogram\n .command('init')\n .description('Initialize Setu in your project')\n .option('-y, --yes', 'Skip prompts and use defaults')\n .action(init);\n\nprogram\n .command('add')\n .description('Add a component to your project')\n .argument('<component>', 'Component to add (auth, logger, database, security)')\n .option('-o, --overwrite', 'Overwrite existing files')\n .action(add);\n\nprogram\n .command('generate')\n .alias('g')\n .description('Generate boilerplate code')\n .argument('<type>', 'Type of code to generate (route)')\n .argument('<name>', 'Name of the resource')\n .action(generate);\n\nprogram.parse();\n","import * as p from '@clack/prompts';\nimport chalk from 'chalk';\nimport path from 'path';\nimport fs from 'fs-extra';\nimport { \n detectPackageManager, \n type PackageManager,\n installDependencies \n} from '../lib/installer.js';\nimport { \n configExists, \n createConfig, \n writeConfig \n} from '../lib/config.js';\nimport { \n isNodeProject, \n getProjectName, \n hasSrcDirectory,\n writeFile,\n ensureDir\n} from '../utils/fs.js';\nimport logger from '../utils/logger.js';\n\ninterface InitOptions {\n yes?: boolean;\n}\n\nexport async function init(options: InitOptions) {\n console.clear();\n p.intro(chalk.bgCyan.black(' 🪛 yantr init '));\n\n const cwd = process.cwd();\n\n // Step 1: Check if this is a Node.js project\n const isNode = await isNodeProject(cwd);\n \n if (!isNode) {\n p.log.error('No package.json found in current directory.');\n p.log.info('Please run this command in an existing Node.js project, or run:');\n p.log.info(chalk.cyan(' npm init -y'));\n p.outro(chalk.red('Initialization cancelled.'));\n process.exit(1);\n }\n\n // Step 2: Check if setu.json already exists\n const hasConfig = await configExists(cwd);\n \n if (hasConfig && !options.yes) {\n const overwrite = await p.confirm({\n message: 'yantr.json already exists. Overwrite?',\n initialValue: false,\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.outro(chalk.yellow('Initialization cancelled.'));\n process.exit(0);\n }\n }\n\n // Step 3: Detect or ask for configuration\n let projectName: string;\n let srcDir: string;\n let packageManager: PackageManager;\n\n if (options.yes) {\n // Use defaults in non-interactive mode\n projectName = (await getProjectName(cwd)) || path.basename(cwd);\n srcDir = (await hasSrcDirectory(cwd)) ? './src' : '.';\n packageManager = (await detectPackageManager(cwd)) || 'npm';\n } else {\n // Interactive prompts\n const detectedPm = await detectPackageManager(cwd);\n const detectedName = (await getProjectName(cwd)) || path.basename(cwd);\n const hasSrc = await hasSrcDirectory(cwd);\n\n const responses = await p.group(\n {\n projectName: () =>\n p.text({\n message: 'Project name:',\n defaultValue: detectedName,\n placeholder: detectedName,\n }),\n srcDir: () =>\n p.select({\n message: 'Where should Yantr put generated files?',\n initialValue: hasSrc ? './src' : '.',\n options: [\n { value: './src', label: './src (recommended)' },\n { value: '.', label: '. (project root)' },\n { value: './lib', label: './lib' },\n ],\n }),\n packageManager: () =>\n p.select({\n message: 'Package manager:',\n initialValue: detectedPm || 'npm',\n options: [\n { value: 'npm', label: 'npm' },\n { value: 'pnpm', label: 'pnpm' },\n { value: 'yarn', label: 'yarn' },\n { value: 'bun', label: 'bun' },\n ],\n }),\n },\n {\n onCancel: () => {\n p.cancel('Initialization cancelled.');\n process.exit(0);\n },\n }\n );\n\n projectName = responses.projectName as string;\n srcDir = responses.srcDir as string;\n packageManager = responses.packageManager as PackageManager;\n }\n\n // Step 4: Create setu.json\n const spinner = p.spinner();\n spinner.start('Creating configuration...');\n\n const config = createConfig(projectName, srcDir, packageManager);\n await writeConfig(cwd, config);\n \n spinner.stop('Created yantr.json');\n\n // Step 5: Copy base templates\n spinner.start('Setting up base templates...');\n\n const templatesDir = path.join(srcDir, 'lib', 'yantr');\n await ensureDir(path.join(cwd, templatesDir));\n\n // Get the registry templates path (relative to the CLI package)\n const cliDir = path.dirname(new URL(import.meta.url).pathname);\n const registryPath = path.join(cliDir, '..', '..', 'registry', 'templates', 'base');\n\n // Check if running from source or built version\n const baseTemplatesPath = await fs.pathExists(registryPath)\n ? registryPath\n : path.join(cliDir, 'registry', 'templates', 'base');\n\n // For now, we'll create the files inline since registry fetching comes in Phase 3\n // This will be replaced with actual registry fetching later\n\n const errorHandlerContent = `import type { Request, Response, NextFunction } from 'express';\n\n/**\n * Custom error class for application errors\n */\nexport class AppError extends Error {\n public readonly statusCode: number;\n public readonly isOperational: boolean;\n public readonly code?: string;\n\n constructor(\n message: string,\n statusCode: number = 500,\n isOperational: boolean = true,\n code?: string\n ) {\n super(message);\n this.statusCode = statusCode;\n this.isOperational = isOperational;\n this.code = code;\n\n Object.setPrototypeOf(this, AppError.prototype);\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Common HTTP error classes\n */\nexport class NotFoundError extends AppError {\n constructor(message: string = 'Resource not found') {\n super(message, 404, true, 'NOT_FOUND');\n }\n}\n\nexport class BadRequestError extends AppError {\n constructor(message: string = 'Bad request') {\n super(message, 400, true, 'BAD_REQUEST');\n }\n}\n\nexport class UnauthorizedError extends AppError {\n constructor(message: string = 'Unauthorized') {\n super(message, 401, true, 'UNAUTHORIZED');\n }\n}\n\nexport class ForbiddenError extends AppError {\n constructor(message: string = 'Forbidden') {\n super(message, 403, true, 'FORBIDDEN');\n }\n}\n\nexport class ConflictError extends AppError {\n constructor(message: string = 'Conflict') {\n super(message, 409, true, 'CONFLICT');\n }\n}\n\nexport class ValidationError extends AppError {\n public readonly errors: Record<string, string[]>;\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed', 422, true, 'VALIDATION_ERROR');\n this.errors = errors;\n }\n}\n\n/**\n * Global error handler middleware\n */\nexport function errorHandler(\n err: Error,\n _req: Request,\n res: Response,\n _next: NextFunction\n): void {\n console.error('[Error]', err);\n\n if (err instanceof AppError) {\n const response: Record<string, unknown> = {\n success: false,\n error: {\n message: err.message,\n code: err.code,\n },\n };\n\n if (err instanceof ValidationError) {\n response.error = {\n ...response.error as object,\n details: err.errors,\n };\n }\n\n res.status(err.statusCode).json(response);\n return;\n }\n\n const statusCode = 500;\n const message = process.env.NODE_ENV === 'production'\n ? 'Internal server error'\n : err.message;\n\n res.status(statusCode).json({\n success: false,\n error: {\n message,\n code: 'INTERNAL_ERROR',\n },\n });\n}\n\n/**\n * Async handler wrapper to catch errors in async route handlers\n */\nexport function asyncHandler(\n fn: (req: Request, res: Response, next: NextFunction) => Promise<void>\n) {\n return (req: Request, res: Response, next: NextFunction) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n`;\n\n const zodMiddlewareContent = `import type { Request, Response, NextFunction } from 'express';\nimport { z, ZodError, ZodSchema } from 'zod';\nimport { ValidationError } from './error-handler';\n\ntype RequestLocation = 'body' | 'query' | 'params';\n\ninterface ValidateOptions {\n body?: ZodSchema;\n query?: ZodSchema;\n params?: ZodSchema;\n}\n\n/**\n * Format Zod errors into a readable format\n */\nfunction formatZodErrors(error: ZodError): Record<string, string[]> {\n const errors: Record<string, string[]> = {};\n\n for (const issue of error.issues) {\n const path = issue.path.join('.');\n const key = path || 'root';\n\n if (!errors[key]) {\n errors[key] = [];\n }\n\n errors[key].push(issue.message);\n }\n\n return errors;\n}\n\n/**\n * Validate request data against Zod schemas\n */\nexport function validate(schemas: ValidateOptions) {\n return async (req: Request, _res: Response, next: NextFunction) => {\n const allErrors: Record<string, string[]> = {};\n const locations: RequestLocation[] = ['body', 'query', 'params'];\n\n for (const location of locations) {\n const schema = schemas[location];\n\n if (schema) {\n const result = await schema.safeParseAsync(req[location]);\n\n if (!result.success) {\n const errors = formatZodErrors(result.error);\n\n for (const [key, messages] of Object.entries(errors)) {\n const prefixedKey = \\`\\${location}.\\${key}\\`;\n allErrors[prefixedKey] = messages;\n }\n } else {\n req[location] = result.data;\n }\n }\n }\n\n if (Object.keys(allErrors).length > 0) {\n return next(new ValidationError(allErrors));\n }\n\n next();\n };\n}\n\nexport function validateBody<T extends ZodSchema>(schema: T) {\n return validate({ body: schema });\n}\n\nexport function validateQuery<T extends ZodSchema>(schema: T) {\n return validate({ query: schema });\n}\n\nexport function validateParams<T extends ZodSchema>(schema: T) {\n return validate({ params: schema });\n}\n\nexport { z };\n`;\n\n await writeFile(\n path.join(cwd, templatesDir, 'error-handler.ts'),\n errorHandlerContent\n );\n\n await writeFile(\n path.join(cwd, templatesDir, 'zod-middleware.ts'),\n zodMiddlewareContent\n );\n\n spinner.stop('Base templates created');\n\n // Step 6: Install zod dependency\n spinner.start('Installing dependencies...');\n\n try {\n await installDependencies(packageManager, ['zod'], cwd, false);\n spinner.stop('Dependencies installed');\n } catch (error) {\n spinner.stop('Could not install dependencies automatically');\n p.log.warning(`Please run: ${chalk.cyan(`${packageManager} add zod`)}`);\n }\n\n // Summary\n p.note(\n `${chalk.green('✓')} yantr.json created\n${chalk.green('✓')} ${templatesDir}/error-handler.ts\n${chalk.green('✓')} ${templatesDir}/zod-middleware.ts`,\n 'Files created'\n );\n\n p.log.info('Next steps:');\n p.log.step(1, `Add error handler to your Express app:`);\n console.log(chalk.gray(` import { errorHandler } from '${templatesDir}/error-handler';`));\n console.log(chalk.gray(` app.use(errorHandler);`));\n p.log.step(2, `Add components: ${chalk.cyan('yantr add auth')}`);\n p.log.step(3, `Generate routes: ${chalk.cyan('yantr generate route users')}`);\n\n p.outro(chalk.green('Yantr initialized successfully! 🪛'));\n}\n","import { execa } from 'execa';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';\n\n/**\n * Detect the package manager used in a project by checking lockfiles\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager | null> {\n const lockFiles: Record<string, PackageManager> = {\n 'pnpm-lock.yaml': 'pnpm',\n 'yarn.lock': 'yarn',\n 'package-lock.json': 'npm',\n 'bun.lockb': 'bun',\n };\n\n for (const [lockFile, pm] of Object.entries(lockFiles)) {\n if (await fs.pathExists(path.join(cwd, lockFile))) {\n return pm;\n }\n }\n\n return null;\n}\n\n/**\n * Get the install command for a package manager\n */\nexport function getInstallCommand(pm: PackageManager): string {\n const commands: Record<PackageManager, string> = {\n npm: 'npm install',\n pnpm: 'pnpm install',\n yarn: 'yarn',\n bun: 'bun install',\n };\n return commands[pm];\n}\n\n/**\n * Get the add command for installing specific packages\n */\nexport function getAddCommand(\n pm: PackageManager,\n packages: string[],\n isDev: boolean = false\n): string {\n const pkgList = packages.join(' ');\n \n const commands: Record<PackageManager, string> = {\n npm: `npm install ${isDev ? '-D' : ''} ${pkgList}`.trim(),\n pnpm: `pnpm add ${isDev ? '-D' : ''} ${pkgList}`.trim(),\n yarn: `yarn add ${isDev ? '-D' : ''} ${pkgList}`.trim(),\n bun: `bun add ${isDev ? '-d' : ''} ${pkgList}`.trim(),\n };\n \n return commands[pm];\n}\n\n/**\n * Run a package manager command\n */\nexport async function runPackageManager(\n pm: PackageManager,\n args: string[],\n cwd: string\n): Promise<void> {\n await execa(pm, args, { cwd, stdio: 'inherit' });\n}\n\n/**\n * Install dependencies using the specified package manager\n */\nexport async function installDependencies(\n pm: PackageManager,\n packages: string[],\n cwd: string,\n isDev: boolean = false\n): Promise<void> {\n const args: string[] = [];\n\n switch (pm) {\n case 'npm':\n args.push('install', isDev ? '-D' : '', ...packages);\n break;\n case 'pnpm':\n args.push('add', isDev ? '-D' : '', ...packages);\n break;\n case 'yarn':\n args.push('add', isDev ? '-D' : '', ...packages);\n break;\n case 'bun':\n args.push('add', isDev ? '-d' : '', ...packages);\n break;\n }\n\n // Filter out empty strings\n const filteredArgs = args.filter(Boolean);\n \n await runPackageManager(pm, filteredArgs, cwd);\n}\n\n/**\n * Get the executable command for running scripts\n */\nexport function getRunCommand(pm: PackageManager, script: string): string {\n const commands: Record<PackageManager, string> = {\n npm: `npm run ${script}`,\n pnpm: `pnpm ${script}`,\n yarn: `yarn ${script}`,\n bun: `bun run ${script}`,\n };\n return commands[pm];\n}\n","import fs from 'fs-extra';\nimport path from 'path';\nimport { z } from 'zod';\nimport type { PackageManager } from './installer.js';\n\n/**\n * Schema for setu.json configuration file\n */\nexport const SetuConfigSchema = z.object({\n $schema: z.string().optional(),\n projectName: z.string(),\n srcDir: z.string().default('./src'),\n packageManager: z.enum(['npm', 'pnpm', 'yarn', 'bun']),\n installedComponents: z.array(z.string()).default([]),\n});\n\nexport type SetuConfig = z.infer<typeof SetuConfigSchema>;\n\nconst CONFIG_FILE = 'yantr.json';\n\n/**\n * Check if yantr.json exists in the given directory\n */\nexport async function configExists(cwd: string): Promise<boolean> {\n return fs.pathExists(path.join(cwd, CONFIG_FILE));\n}\n\n/**\n * Read and parse yantr.json\n */\nexport async function readConfig(cwd: string): Promise<SetuConfig> {\n const configPath = path.join(cwd, CONFIG_FILE);\n \n if (!(await fs.pathExists(configPath))) {\n throw new Error('yantr.json not found. Run \"setu init\" first.');\n }\n\n const content = await fs.readJson(configPath);\n return SetuConfigSchema.parse(content);\n}\n\n/**\n * Write yantr.json configuration\n */\nexport async function writeConfig(cwd: string, config: SetuConfig): Promise<void> {\n const configPath = path.join(cwd, CONFIG_FILE);\n await fs.writeJson(configPath, config, { spaces: 2 });\n}\n\n/**\n * Create initial yantr.json configuration\n */\nexport function createConfig(\n projectName: string,\n srcDir: string,\n packageManager: PackageManager\n): SetuConfig {\n return {\n $schema: 'https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/schema.json',\n projectName,\n srcDir,\n packageManager,\n installedComponents: [],\n };\n}\n\n/**\n * Add a component to the installed list\n */\nexport async function addInstalledComponent(cwd: string, component: string): Promise<void> {\n const config = await readConfig(cwd);\n \n if (!config.installedComponents.includes(component)) {\n config.installedComponents.push(component);\n await writeConfig(cwd, config);\n }\n}\n\n/**\n * Check if a component is already installed\n */\nexport async function isComponentInstalled(cwd: string, component: string): Promise<boolean> {\n const config = await readConfig(cwd);\n return config.installedComponents.includes(component);\n}\n","import fs from 'fs-extra';\nimport path from 'path';\n\n/**\n * Ensure a directory exists, creating it if necessary\n */\nexport async function ensureDir(dirPath: string): Promise<void> {\n await fs.ensureDir(dirPath);\n}\n\n/**\n * Copy a file, creating parent directories if needed\n */\nexport async function copyFile(src: string, dest: string): Promise<void> {\n await fs.ensureDir(path.dirname(dest));\n await fs.copy(src, dest);\n}\n\n/**\n * Write content to a file, creating parent directories if needed\n */\nexport async function writeFile(filePath: string, content: string): Promise<void> {\n await fs.ensureDir(path.dirname(filePath));\n await fs.writeFile(filePath, content, 'utf-8');\n}\n\n/**\n * Check if a file exists\n */\nexport async function fileExists(filePath: string): Promise<boolean> {\n return fs.pathExists(filePath);\n}\n\n/**\n * Read file content as string\n */\nexport async function readFile(filePath: string): Promise<string> {\n return fs.readFile(filePath, 'utf-8');\n}\n\n/**\n * Check if a directory contains a package.json\n */\nexport async function isNodeProject(cwd: string): Promise<boolean> {\n return fs.pathExists(path.join(cwd, 'package.json'));\n}\n\n/**\n * Get project name from package.json\n */\nexport async function getProjectName(cwd: string): Promise<string | null> {\n const pkgPath = path.join(cwd, 'package.json');\n \n if (await fs.pathExists(pkgPath)) {\n const pkg = await fs.readJson(pkgPath);\n return pkg.name || null;\n }\n \n return null;\n}\n\n/**\n * Check if src directory exists\n */\nexport async function hasSrcDirectory(cwd: string): Promise<boolean> {\n return fs.pathExists(path.join(cwd, 'src'));\n}\n","import * as p from '@clack/prompts';\nimport chalk from 'chalk';\nimport path from 'path';\nimport { \n getComponent, \n listComponents, \n fetchComponentFiles \n} from '../lib/registry.js';\nimport { \n readConfig, \n configExists, \n addInstalledComponent,\n isComponentInstalled \n} from '../lib/config.js';\nimport { installDependencies } from '../lib/installer.js';\nimport { writeFile, fileExists } from '../utils/fs.js';\n\ninterface AddOptions {\n overwrite?: boolean;\n}\n\nexport async function add(componentName: string, options: AddOptions) {\n console.clear();\n p.intro(chalk.bgCyan.black(` 🪛 yantr add ${componentName} `));\n\n try {\n const cwd = process.cwd();\n\n // Step 1: Check if setu.json exists\n if (!(await configExists(cwd))) {\n p.log.error('yantr.json not found.');\n p.log.info('Please run ' + chalk.cyan('yantr init') + ' first.');\n p.outro(chalk.red('Add cancelled.'));\n process.exit(1);\n }\n\n // Step 2: Read config\n const config = await readConfig(cwd);\n\n // Step 3: Validate component name\n const spinner = p.spinner();\n spinner.start('Fetching component registry...');\n\n const availableComponents = await listComponents();\n \n if (!availableComponents.includes(componentName)) {\n spinner.stop('Component not found');\n p.log.error(`Unknown component: ${chalk.red(componentName)}`);\n p.log.info(`Available components: ${chalk.cyan(availableComponents.join(', '))}`);\n p.outro(chalk.red('Add cancelled.'));\n process.exit(1);\n }\n\n // Step 4: Check if already installed\n const alreadyInstalled = await isComponentInstalled(cwd, componentName);\n \n if (alreadyInstalled && !options.overwrite) {\n spinner.stop('Component already installed');\n \n const overwrite = await p.confirm({\n message: `${componentName} is already installed. Overwrite?`,\n initialValue: false,\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.outro(chalk.yellow('Add cancelled.'));\n process.exit(0);\n }\n }\n\n // Step 5: Fetch component metadata and files\n spinner.start(`Downloading ${componentName}...`);\n \n const component = await getComponent(componentName);\n \n if (!component) {\n spinner.stop('Failed to fetch component');\n p.log.error('Failed to get component metadata');\n process.exit(1);\n }\n\n const files = await fetchComponentFiles(componentName);\n spinner.stop(`Downloaded ${files.size} files`);\n\n // Step 6: Write files to project\n spinner.start('Installing files...');\n \n const installedFiles: string[] = [];\n const baseDir = path.join(cwd, config.srcDir, 'lib', 'yantr');\n\n for (const [filePath, content] of files) {\n // Extract just the filename from paths like \"auth/auth.controller.ts\"\n const fileName = path.basename(filePath);\n const targetPath = path.join(baseDir, componentName, fileName);\n \n // Check if file exists and we're not overwriting\n if ((await fileExists(targetPath)) && !options.overwrite && !alreadyInstalled) {\n const shouldOverwrite = await p.confirm({\n message: `File ${fileName} already exists. Overwrite?`,\n initialValue: false,\n });\n\n if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {\n continue;\n }\n }\n\n await writeFile(targetPath, content);\n installedFiles.push(path.relative(cwd, targetPath));\n }\n\n spinner.stop('Files installed');\n\n // Step 7: Install dependencies\n if (component.dependencies.length > 0 || component.devDependencies.length > 0) {\n spinner.start('Installing dependencies...');\n\n try {\n if (component.dependencies.length > 0) {\n await installDependencies(\n config.packageManager,\n component.dependencies,\n cwd,\n false\n );\n }\n\n if (component.devDependencies.length > 0) {\n await installDependencies(\n config.packageManager,\n component.devDependencies,\n cwd,\n true\n );\n }\n\n spinner.stop('Dependencies installed');\n } catch (error) {\n spinner.stop('Could not install dependencies automatically');\n \n if (component.dependencies.length > 0) {\n p.log.warning(\n `Please run: ${chalk.cyan(`${config.packageManager} add ${component.dependencies.join(' ')}`)}`\n );\n }\n if (component.devDependencies.length > 0) {\n p.log.warning(\n `Please run: ${chalk.cyan(`${config.packageManager} add -D ${component.devDependencies.join(' ')}`)}`\n );\n }\n }\n }\n\n // Step 8: Update setu.json\n await addInstalledComponent(cwd, componentName);\n\n // Summary\n const filesNote = installedFiles.map(f => `${chalk.green('✓')} ${f}`).join('\\n');\n \n p.note(filesNote, 'Files created');\n\n if (component.dependencies.length > 0) {\n p.log.info(`Dependencies: ${chalk.cyan(component.dependencies.join(', '))}`);\n }\n\n // Component-specific usage hints\n const usageHints: Record<string, string[]> = {\n auth: [\n `Import auth routes: ${chalk.gray(`import authRoutes from '${config.srcDir}/lib/yantr/auth/auth.routes';`)}`,\n `Add to app: ${chalk.gray('app.use(\"/api/auth\", authRoutes);')}`,\n ],\n logger: [\n `Import logger: ${chalk.gray(`import { logger } from '${config.srcDir}/lib/yantr/logger/logger';`)}`,\n `Use HTTP logging: ${chalk.gray('app.use(httpLogger);')}`,\n ],\n database: [\n `Import prisma: ${chalk.gray(`import { prisma } from '${config.srcDir}/lib/yantr/database/prisma';`)}`,\n `Initialize: ${chalk.gray('npx prisma init')}`,\n ],\n security: [\n `Import security: ${chalk.gray(`import { rateLimiter, helmetConfig } from '${config.srcDir}/lib/yantr/security';`)}`,\n `Add to app: ${chalk.gray('app.use(helmetConfig); app.use(rateLimiter);')}`,\n ],\n };\n\n if (usageHints[componentName]) {\n p.log.info('Usage:');\n usageHints[componentName].forEach((hint, i) => {\n console.log(` ${i + 1}. ${hint}`);\n });\n }\n\n p.outro(chalk.green(`${component.name} added successfully! 🌉`));\n } catch (error) {\n p.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);\n console.error(error);\n process.exit(1);\n }\n}\n","import { z } from 'zod';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\n/**\n * Schema for a single component in the registry\n */\nexport const ComponentSchema = z.object({\n name: z.string(),\n description: z.string(),\n files: z.array(z.string()),\n dependencies: z.array(z.string()).default([]),\n devDependencies: z.array(z.string()).default([]),\n});\n\nexport type Component = z.infer<typeof ComponentSchema>;\n\n/**\n * Schema for the full registry\n */\nexport const RegistrySchema = z.object({\n version: z.string(),\n baseUrl: z.string(),\n components: z.record(z.string(), ComponentSchema),\n});\n\nexport type Registry = z.infer<typeof RegistrySchema>;\n\n// Registry configuration\nconst REGISTRY_URL = 'https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/registry/registry.json';\nconst CACHE_DIR = '.yantr-cache';\nconst CACHE_TTL_MS = 1000 * 60 * 60; // 1 hour\n\n/**\n * Get the cache directory path\n */\nfunction getCacheDir(): string {\n const homeDir = process.env.HOME || process.env.USERPROFILE || '.';\n return path.join(homeDir, CACHE_DIR);\n}\n\n/**\n * Get cached registry if valid\n */\nasync function getCachedRegistry(): Promise<Registry | null> {\n const cacheDir = getCacheDir();\n const cachePath = path.join(cacheDir, 'registry.json');\n \n try {\n if (!(await fs.pathExists(cachePath))) {\n return null;\n }\n\n const stats = await fs.stat(cachePath);\n const age = Date.now() - stats.mtimeMs;\n \n if (age > CACHE_TTL_MS) {\n return null; // Cache expired\n }\n\n const content = await fs.readJson(cachePath);\n return RegistrySchema.parse(content);\n } catch {\n return null;\n }\n}\n\n/**\n * Save registry to cache\n */\nasync function cacheRegistry(registry: Registry): Promise<void> {\n const cacheDir = getCacheDir();\n const cachePath = path.join(cacheDir, 'registry.json');\n \n try {\n await fs.ensureDir(cacheDir);\n await fs.writeJson(cachePath, registry);\n } catch {\n // Ignore cache write errors\n }\n}\n\n/**\n * Fetch registry from GitHub\n */\nasync function fetchRemoteRegistry(): Promise<Registry> {\n const response = await fetch(REGISTRY_URL);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`);\n }\n\n const data = await response.json();\n return RegistrySchema.parse(data);\n}\n\n/**\n * Load local registry from CLI package (fallback)\n */\nasync function loadLocalRegistry(): Promise<Registry> {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n \n // Try different paths based on dev vs built\n // When bundled, the dist/index.js is in cli/dist/\n // Registry is in cli/registry/\n const possiblePaths = [\n // From source: cli/src/lib -> cli/registry\n path.join(__dirname, '..', '..', 'registry', 'registry.json'),\n // From dist: cli/dist -> cli/registry \n path.join(__dirname, '..', 'registry', 'registry.json'),\n // Direct path from built bundle\n path.resolve(__dirname, '..', 'registry', 'registry.json'),\n ];\n\n for (const registryPath of possiblePaths) {\n if (await fs.pathExists(registryPath)) {\n const content = await fs.readJson(registryPath);\n return RegistrySchema.parse(content);\n }\n }\n\n throw new Error(`Local registry not found. Tried: ${possiblePaths.join(', ')}`);\n}\n\n/**\n * Load local template file (for when running from local install)\n */\nasync function loadLocalTemplateFile(filePath: string): Promise<string | null> {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n \n const possiblePaths = [\n path.join(__dirname, '..', '..', 'registry', 'templates', filePath),\n path.join(__dirname, '..', 'registry', 'templates', filePath),\n ];\n\n for (const templatePath of possiblePaths) {\n if (await fs.pathExists(templatePath)) {\n return fs.readFile(templatePath, 'utf-8');\n }\n }\n\n return null;\n}\n\n/**\n * Get the registry (with caching and fallback)\n */\nexport async function getRegistry(): Promise<Registry> {\n // Try cache first\n const cached = await getCachedRegistry();\n if (cached) {\n return cached;\n }\n\n // Try local first (faster when CLI is installed globally)\n try {\n const local = await loadLocalRegistry();\n return local;\n } catch {\n // Local not found, try remote\n }\n\n // Try fetching from remote with timeout\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n \n const response = await fetch(REGISTRY_URL, { signal: controller.signal });\n clearTimeout(timeoutId);\n \n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n const remote = RegistrySchema.parse(data);\n await cacheRegistry(remote);\n return remote;\n } catch (error) {\n throw new Error(`Failed to load registry: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\n/**\n * Get a specific component from the registry\n */\nexport async function getComponent(name: string): Promise<Component | null> {\n const registry = await getRegistry();\n return registry.components[name] || null;\n}\n\n/**\n * List all available components\n */\nexport async function listComponents(): Promise<string[]> {\n const registry = await getRegistry();\n return Object.keys(registry.components);\n}\n\n/**\n * Fetch a template file from the registry (local first, then remote)\n */\nexport async function fetchTemplateFile(filePath: string): Promise<string> {\n // Try local first\n const local = await loadLocalTemplateFile(filePath);\n if (local) {\n return local;\n }\n\n // Try remote\n const registry = await getRegistry();\n const url = `${registry.baseUrl}/${filePath}`;\n \n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n \n const response = await fetch(url, { signal: controller.signal });\n clearTimeout(timeoutId);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch template: ${url} (${response.status})`);\n }\n\n return response.text();\n}\n\n/**\n * Fetch all files for a component\n */\nexport async function fetchComponentFiles(\n componentName: string\n): Promise<Map<string, string>> {\n const component = await getComponent(componentName);\n \n if (!component) {\n throw new Error(`Component not found: ${componentName}`);\n }\n\n const files = new Map<string, string>();\n \n for (const filePath of component.files) {\n const content = await fetchTemplateFile(filePath);\n files.set(filePath, content);\n }\n\n return files;\n}\n\n","import * as p from '@clack/prompts';\nimport chalk from 'chalk';\nimport path from 'path';\nimport fs from 'fs-extra';\nimport { readConfig, configExists } from '../lib/config.js';\nimport { writeFile, fileExists } from '../utils/fs.js';\n\ninterface GenerateOptions {\n // Future options can be added here\n}\n\nexport async function generate(type: string, name: string, _options?: GenerateOptions) {\n console.clear();\n p.intro(chalk.bgCyan.black(` 🪛 yantr generate ${type} ${name} `));\n\n try {\n const cwd = process.cwd();\n\n // Step 1: Check if yantr.json exists\n if (!(await configExists(cwd))) {\n p.log.error('yantr.json not found.');\n p.log.info('Please run ' + chalk.cyan('yantr init') + ' first.');\n p.outro(chalk.red('Generate cancelled.'));\n process.exit(1);\n }\n\n // Step 2: Read config\n const config = await readConfig(cwd);\n\n // Step 3: Validate type\n const validTypes = ['route'];\n \n if (!validTypes.includes(type)) {\n p.log.error(`Unknown type: ${chalk.red(type)}`);\n p.log.info(`Available types: ${chalk.cyan(validTypes.join(', '))}`);\n p.outro(chalk.red('Generate cancelled.'));\n process.exit(1);\n }\n\n // Step 4: Generate based on type\n if (type === 'route') {\n await generateRoute(cwd, config.srcDir, name);\n }\n\n p.outro(chalk.green(`Generated ${type} \"${name}\" successfully! 🪛`));\n } catch (error) {\n p.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n }\n}\n\n/**\n * Generate route, controller, and service files\n */\nasync function generateRoute(cwd: string, srcDir: string, name: string) {\n const spinner = p.spinner();\n \n // Normalize the name (e.g., \"users\" -> \"users\", \"UserProfile\" -> \"user-profile\")\n const normalizedName = name\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .toLowerCase();\n \n const pascalName = toPascalCase(normalizedName);\n const camelName = toCamelCase(normalizedName);\n\n // Define file paths\n const baseDir = path.join(cwd, srcDir);\n const routesDir = path.join(baseDir, 'routes');\n const controllersDir = path.join(baseDir, 'controllers');\n const servicesDir = path.join(baseDir, 'services');\n\n const files = {\n route: path.join(routesDir, `${normalizedName}.routes.ts`),\n controller: path.join(controllersDir, `${normalizedName}.controller.ts`),\n service: path.join(servicesDir, `${normalizedName}.service.ts`),\n };\n\n // Check if files already exist\n for (const [type, filePath] of Object.entries(files)) {\n if (await fileExists(filePath)) {\n const overwrite = await p.confirm({\n message: `${type} file already exists. Overwrite?`,\n initialValue: false,\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.outro(chalk.yellow('Generate cancelled.'));\n process.exit(0);\n }\n }\n }\n\n spinner.start('Generating files...');\n\n // Generate service file\n const serviceContent = generateServiceTemplate(pascalName, camelName);\n await writeFile(files.service, serviceContent);\n\n // Generate controller file\n const controllerContent = generateControllerTemplate(pascalName, camelName, normalizedName);\n await writeFile(files.controller, controllerContent);\n\n // Generate route file\n const routeContent = generateRouteTemplate(pascalName, camelName, normalizedName);\n await writeFile(files.route, routeContent);\n\n spinner.stop('Files generated');\n\n // Show created files\n const createdFiles = Object.values(files).map(f => \n `${chalk.green('✓')} ${path.relative(cwd, f)}`\n ).join('\\n');\n\n p.note(createdFiles, 'Files created');\n\n // Usage hint\n p.log.info('Add to your app:');\n console.log(chalk.gray(` import ${camelName}Routes from './${srcDir}/routes/${normalizedName}.routes';`));\n console.log(chalk.gray(` app.use('/api/${normalizedName}', ${camelName}Routes);`));\n}\n\n/**\n * Generate service template\n */\nfunction generateServiceTemplate(pascalName: string, _camelName: string): string {\n return `/**\n * ${pascalName} Service\n * \n * Business logic for ${pascalName.toLowerCase()} operations.\n */\n\nexport interface ${pascalName} {\n id: string;\n // Add your fields here\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Create${pascalName}Input {\n // Add your input fields here\n}\n\nexport interface Update${pascalName}Input {\n // Add your update fields here\n}\n\n/**\n * Get all ${pascalName.toLowerCase()}s\n */\nexport async function getAll(): Promise<${pascalName}[]> {\n // TODO: Implement database query\n // Example: return prisma.${_camelName}.findMany();\n return [];\n}\n\n/**\n * Get ${pascalName.toLowerCase()} by ID\n */\nexport async function getById(id: string): Promise<${pascalName} | null> {\n // TODO: Implement database query\n // Example: return prisma.${_camelName}.findUnique({ where: { id } });\n return null;\n}\n\n/**\n * Create a new ${pascalName.toLowerCase()}\n */\nexport async function create(input: Create${pascalName}Input): Promise<${pascalName}> {\n // TODO: Implement database insert\n // Example: return prisma.${_camelName}.create({ data: input });\n throw new Error('Not implemented');\n}\n\n/**\n * Update ${pascalName.toLowerCase()} by ID\n */\nexport async function update(id: string, input: Update${pascalName}Input): Promise<${pascalName}> {\n // TODO: Implement database update\n // Example: return prisma.${_camelName}.update({ where: { id }, data: input });\n throw new Error('Not implemented');\n}\n\n/**\n * Delete ${pascalName.toLowerCase()} by ID\n */\nexport async function remove(id: string): Promise<void> {\n // TODO: Implement database delete\n // Example: await prisma.${_camelName}.delete({ where: { id } });\n throw new Error('Not implemented');\n}\n`;\n}\n\n/**\n * Generate controller template\n */\nfunction generateControllerTemplate(pascalName: string, camelName: string, normalizedName: string): string {\n return `import type { Request, Response } from 'express';\nimport { z } from 'zod';\nimport * as ${camelName}Service from '../services/${normalizedName}.service';\nimport { asyncHandler } from '../lib/yantr/error-handler';\nimport { NotFoundError } from '../lib/yantr/error-handler';\n\n/**\n * ${pascalName} Controller\n * \n * Request handlers for ${pascalName.toLowerCase()} endpoints.\n */\n\n// Validation schemas\nexport const create${pascalName}Schema = z.object({\n // Add your validation rules here\n});\n\nexport const update${pascalName}Schema = z.object({\n // Add your validation rules here\n});\n\n/**\n * GET /api/${normalizedName}\n * Get all ${pascalName.toLowerCase()}s\n */\nexport const getAll = asyncHandler(async (_req: Request, res: Response) => {\n const items = await ${camelName}Service.getAll();\n \n res.json({\n success: true,\n data: items,\n });\n});\n\n/**\n * GET /api/${normalizedName}/:id\n * Get ${pascalName.toLowerCase()} by ID\n */\nexport const getById = asyncHandler(async (req: Request, res: Response) => {\n const { id } = req.params;\n const item = await ${camelName}Service.getById(id);\n \n if (!item) {\n throw new NotFoundError('${pascalName} not found');\n }\n \n res.json({\n success: true,\n data: item,\n });\n});\n\n/**\n * POST /api/${normalizedName}\n * Create a new ${pascalName.toLowerCase()}\n */\nexport const create = asyncHandler(async (req: Request, res: Response) => {\n const input = create${pascalName}Schema.parse(req.body);\n const item = await ${camelName}Service.create(input);\n \n res.status(201).json({\n success: true,\n data: item,\n message: '${pascalName} created successfully',\n });\n});\n\n/**\n * PUT /api/${normalizedName}/:id\n * Update ${pascalName.toLowerCase()} by ID\n */\nexport const update = asyncHandler(async (req: Request, res: Response) => {\n const { id } = req.params;\n const input = update${pascalName}Schema.parse(req.body);\n const item = await ${camelName}Service.update(id, input);\n \n res.json({\n success: true,\n data: item,\n message: '${pascalName} updated successfully',\n });\n});\n\n/**\n * DELETE /api/${normalizedName}/:id\n * Delete ${pascalName.toLowerCase()} by ID\n */\nexport const remove = asyncHandler(async (req: Request, res: Response) => {\n const { id } = req.params;\n await ${camelName}Service.remove(id);\n \n res.json({\n success: true,\n message: '${pascalName} deleted successfully',\n });\n});\n`;\n}\n\n/**\n * Generate route template\n */\nfunction generateRouteTemplate(pascalName: string, camelName: string, normalizedName: string): string {\n return `import { Router } from 'express';\nimport * as ${camelName}Controller from '../controllers/${normalizedName}.controller';\nimport { validateBody } from '../lib/yantr/zod-middleware';\nimport { \n create${pascalName}Schema, \n update${pascalName}Schema \n} from '../controllers/${normalizedName}.controller';\n\nconst router = Router();\n\n/**\n * ${pascalName} Routes\n * \n * GET /api/${normalizedName} - Get all ${pascalName.toLowerCase()}s\n * GET /api/${normalizedName}/:id - Get ${pascalName.toLowerCase()} by ID\n * POST /api/${normalizedName} - Create ${pascalName.toLowerCase()}\n * PUT /api/${normalizedName}/:id - Update ${pascalName.toLowerCase()}\n * DELETE /api/${normalizedName}/:id - Delete ${pascalName.toLowerCase()}\n */\n\nrouter.get('/', ${camelName}Controller.getAll);\nrouter.get('/:id', ${camelName}Controller.getById);\nrouter.post('/', validateBody(create${pascalName}Schema), ${camelName}Controller.create);\nrouter.put('/:id', validateBody(update${pascalName}Schema), ${camelName}Controller.update);\nrouter.delete('/:id', ${camelName}Controller.remove);\n\nexport default router;\n`;\n}\n\n/**\n * Convert string to PascalCase\n */\nfunction toPascalCase(str: string): string {\n return str\n .split('-')\n .map(part => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Convert string to camelCase\n */\nfunction toCamelCase(str: string): string {\n const pascal = toPascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n"],"mappings":"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","names":["path","fs","fs","path","fs","path","fs","path","fs","path","path","spinner","fs","p","chalk","path","z","fs","path","__filename","__dirname","path","fs","chalk","spinner","path","p","chalk","path","chalk","spinner","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yantr-js",
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"description": "A Shadcn for Backend - Production-grade backend scaffolding CLI for Express.js",
|
|
5
|
+
"author": "SibilSoren",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"yantr": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"registry"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/SibilSoren/setu-js.git",
|
|
19
|
+
"directory": "cli"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"express",
|
|
24
|
+
"backend",
|
|
25
|
+
"scaffolding",
|
|
26
|
+
"typescript",
|
|
27
|
+
"shadcn",
|
|
28
|
+
"boilerplate",
|
|
29
|
+
"api"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"start": "node dist/index.js",
|
|
35
|
+
"lint": "eslint src/",
|
|
36
|
+
"test": "vitest",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^0.7.0",
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"commander": "^12.1.0",
|
|
43
|
+
"execa": "^9.3.0",
|
|
44
|
+
"fs-extra": "^11.2.0",
|
|
45
|
+
"zod": "^3.23.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/fs-extra": "^11.0.4",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"tsup": "^8.2.0",
|
|
51
|
+
"typescript": "^5.5.0",
|
|
52
|
+
"vitest": "^2.0.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"baseUrl": "https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/registry/templates",
|
|
4
|
+
"components": {
|
|
5
|
+
"base": {
|
|
6
|
+
"name": "Base",
|
|
7
|
+
"description": "Global error handler and Zod validation middleware",
|
|
8
|
+
"files": [
|
|
9
|
+
"base/error-handler.ts",
|
|
10
|
+
"base/zod-middleware.ts"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": ["zod"],
|
|
13
|
+
"devDependencies": []
|
|
14
|
+
},
|
|
15
|
+
"auth": {
|
|
16
|
+
"name": "Authentication",
|
|
17
|
+
"description": "JWT authentication with refresh tokens and cookie support",
|
|
18
|
+
"files": [
|
|
19
|
+
"auth/auth.controller.ts",
|
|
20
|
+
"auth/auth.service.ts",
|
|
21
|
+
"auth/auth.middleware.ts",
|
|
22
|
+
"auth/auth.routes.ts"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": ["jsonwebtoken", "bcryptjs", "cookie-parser"],
|
|
25
|
+
"devDependencies": ["@types/jsonwebtoken", "@types/bcryptjs", "@types/cookie-parser"]
|
|
26
|
+
},
|
|
27
|
+
"logger": {
|
|
28
|
+
"name": "Logger",
|
|
29
|
+
"description": "Structured logging with Pino",
|
|
30
|
+
"files": [
|
|
31
|
+
"logger/logger.ts",
|
|
32
|
+
"logger/http-logger.ts"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": ["pino", "pino-http", "pino-pretty"],
|
|
35
|
+
"devDependencies": []
|
|
36
|
+
},
|
|
37
|
+
"database": {
|
|
38
|
+
"name": "Database",
|
|
39
|
+
"description": "Prisma ORM setup with connection pooling",
|
|
40
|
+
"files": [
|
|
41
|
+
"database/prisma.ts",
|
|
42
|
+
"database/db.ts"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": ["@prisma/client"],
|
|
45
|
+
"devDependencies": ["prisma"]
|
|
46
|
+
},
|
|
47
|
+
"security": {
|
|
48
|
+
"name": "Security",
|
|
49
|
+
"description": "Rate limiting with Helmet security headers",
|
|
50
|
+
"files": [
|
|
51
|
+
"security/rate-limiter.ts",
|
|
52
|
+
"security/helmet.ts"
|
|
53
|
+
],
|
|
54
|
+
"dependencies": ["helmet", "express-rate-limit"],
|
|
55
|
+
"devDependencies": ["@types/express-rate-limit"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { AuthRequest } from './auth.middleware';
|
|
4
|
+
import { registerUser, loginUser, refreshAccessToken } from './auth.service';
|
|
5
|
+
import { asyncHandler } from '../base/error-handler';
|
|
6
|
+
import { BadRequestError } from '../base/error-handler';
|
|
7
|
+
|
|
8
|
+
// Validation schemas
|
|
9
|
+
export const registerSchema = z.object({
|
|
10
|
+
email: z.string().email('Invalid email address'),
|
|
11
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
12
|
+
name: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const loginSchema = z.object({
|
|
16
|
+
email: z.string().email('Invalid email address'),
|
|
17
|
+
password: z.string().min(1, 'Password is required'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Cookie options
|
|
21
|
+
const COOKIE_OPTIONS = {
|
|
22
|
+
httpOnly: true,
|
|
23
|
+
secure: process.env.NODE_ENV === 'production',
|
|
24
|
+
sameSite: 'lax' as const,
|
|
25
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a new user
|
|
30
|
+
* POST /api/auth/register
|
|
31
|
+
*/
|
|
32
|
+
export const register = asyncHandler(async (req: AuthRequest, res: Response) => {
|
|
33
|
+
const validatedData = registerSchema.parse(req.body);
|
|
34
|
+
|
|
35
|
+
const user = await registerUser(validatedData);
|
|
36
|
+
|
|
37
|
+
res.status(201).json({
|
|
38
|
+
success: true,
|
|
39
|
+
data: {
|
|
40
|
+
user: {
|
|
41
|
+
id: user.id,
|
|
42
|
+
email: user.email,
|
|
43
|
+
name: user.name,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
message: 'Registration successful',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Login user
|
|
52
|
+
* POST /api/auth/login
|
|
53
|
+
*/
|
|
54
|
+
export const login = asyncHandler(async (req: AuthRequest, res: Response) => {
|
|
55
|
+
const validatedData = loginSchema.parse(req.body);
|
|
56
|
+
|
|
57
|
+
const { user, tokens } = await loginUser(validatedData);
|
|
58
|
+
|
|
59
|
+
// Set refresh token as httpOnly cookie
|
|
60
|
+
res.cookie('refreshToken', tokens.refreshToken, COOKIE_OPTIONS);
|
|
61
|
+
|
|
62
|
+
res.json({
|
|
63
|
+
success: true,
|
|
64
|
+
data: {
|
|
65
|
+
user: {
|
|
66
|
+
id: user.id,
|
|
67
|
+
email: user.email,
|
|
68
|
+
name: user.name,
|
|
69
|
+
},
|
|
70
|
+
accessToken: tokens.accessToken,
|
|
71
|
+
},
|
|
72
|
+
message: 'Login successful',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Refresh access token
|
|
78
|
+
* POST /api/auth/refresh
|
|
79
|
+
*/
|
|
80
|
+
export const refresh = asyncHandler(async (req: AuthRequest, res: Response) => {
|
|
81
|
+
const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
|
|
82
|
+
|
|
83
|
+
if (!refreshToken) {
|
|
84
|
+
throw new BadRequestError('Refresh token required');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokens = refreshAccessToken(refreshToken);
|
|
88
|
+
|
|
89
|
+
// Update refresh token cookie
|
|
90
|
+
res.cookie('refreshToken', tokens.refreshToken, COOKIE_OPTIONS);
|
|
91
|
+
|
|
92
|
+
res.json({
|
|
93
|
+
success: true,
|
|
94
|
+
data: {
|
|
95
|
+
accessToken: tokens.accessToken,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Logout user
|
|
102
|
+
* POST /api/auth/logout
|
|
103
|
+
*/
|
|
104
|
+
export const logout = asyncHandler(async (_req: AuthRequest, res: Response) => {
|
|
105
|
+
// Clear refresh token cookie
|
|
106
|
+
res.clearCookie('refreshToken', COOKIE_OPTIONS);
|
|
107
|
+
|
|
108
|
+
res.json({
|
|
109
|
+
success: true,
|
|
110
|
+
message: 'Logout successful',
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get current user
|
|
116
|
+
* GET /api/auth/me
|
|
117
|
+
*/
|
|
118
|
+
export const getMe = asyncHandler(async (req: AuthRequest, res: Response) => {
|
|
119
|
+
res.json({
|
|
120
|
+
success: true,
|
|
121
|
+
data: {
|
|
122
|
+
user: req.user,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { UnauthorizedError } from '../base/error-handler';
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export interface TokenPayload {
|
|
7
|
+
userId: string;
|
|
8
|
+
email: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AuthRequest extends Request {
|
|
13
|
+
user?: TokenPayload;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Environment variables (should be set in .env)
|
|
17
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
|
18
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m';
|
|
19
|
+
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '7d';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate access token
|
|
23
|
+
*/
|
|
24
|
+
export function generateAccessToken(payload: TokenPayload): string {
|
|
25
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate refresh token
|
|
30
|
+
*/
|
|
31
|
+
export function generateRefreshToken(payload: TokenPayload): string {
|
|
32
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify and decode token
|
|
37
|
+
*/
|
|
38
|
+
export function verifyToken(token: string): TokenPayload {
|
|
39
|
+
try {
|
|
40
|
+
return jwt.verify(token, JWT_SECRET) as TokenPayload;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new UnauthorizedError('Invalid or expired token');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Auth middleware - validates JWT from Authorization header or cookies
|
|
48
|
+
*/
|
|
49
|
+
export function authenticate(
|
|
50
|
+
req: AuthRequest,
|
|
51
|
+
_res: Response,
|
|
52
|
+
next: NextFunction
|
|
53
|
+
): void {
|
|
54
|
+
// Try to get token from Authorization header
|
|
55
|
+
let token = req.headers.authorization?.replace('Bearer ', '');
|
|
56
|
+
|
|
57
|
+
// Fallback to cookies
|
|
58
|
+
if (!token && req.cookies?.accessToken) {
|
|
59
|
+
token = req.cookies.accessToken;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!token) {
|
|
63
|
+
throw new UnauthorizedError('No token provided');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const payload = verifyToken(token);
|
|
68
|
+
req.user = payload;
|
|
69
|
+
next();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new UnauthorizedError('Invalid or expired token');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Optional auth middleware - attaches user if token exists, but doesn't require it
|
|
77
|
+
*/
|
|
78
|
+
export function optionalAuth(
|
|
79
|
+
req: AuthRequest,
|
|
80
|
+
_res: Response,
|
|
81
|
+
next: NextFunction
|
|
82
|
+
): void {
|
|
83
|
+
let token = req.headers.authorization?.replace('Bearer ', '');
|
|
84
|
+
|
|
85
|
+
if (!token && req.cookies?.accessToken) {
|
|
86
|
+
token = req.cookies.accessToken;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (token) {
|
|
90
|
+
try {
|
|
91
|
+
const payload = verifyToken(token);
|
|
92
|
+
req.user = payload;
|
|
93
|
+
} catch {
|
|
94
|
+
// Token invalid, but that's okay for optional auth
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
next();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Role-based authorization middleware
|
|
103
|
+
*/
|
|
104
|
+
export function authorize(...allowedRoles: string[]) {
|
|
105
|
+
return (req: AuthRequest, _res: Response, next: NextFunction): void => {
|
|
106
|
+
if (!req.user) {
|
|
107
|
+
throw new UnauthorizedError('Authentication required');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (allowedRoles.length > 0 && !allowedRoles.includes(req.user.role || '')) {
|
|
111
|
+
throw new UnauthorizedError('Insufficient permissions');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
next();
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
register,
|
|
4
|
+
login,
|
|
5
|
+
refresh,
|
|
6
|
+
logout,
|
|
7
|
+
getMe
|
|
8
|
+
} from './auth.controller';
|
|
9
|
+
import { authenticate } from './auth.middleware';
|
|
10
|
+
import { validateBody } from '../base/zod-middleware';
|
|
11
|
+
import { registerSchema, loginSchema } from './auth.controller';
|
|
12
|
+
|
|
13
|
+
const router = Router();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Auth Routes
|
|
17
|
+
*
|
|
18
|
+
* POST /api/auth/register - Register new user
|
|
19
|
+
* POST /api/auth/login - Login user
|
|
20
|
+
* POST /api/auth/refresh - Refresh access token
|
|
21
|
+
* POST /api/auth/logout - Logout user
|
|
22
|
+
* GET /api/auth/me - Get current user (protected)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Public routes
|
|
26
|
+
router.post('/register', validateBody(registerSchema), register);
|
|
27
|
+
router.post('/login', validateBody(loginSchema), login);
|
|
28
|
+
router.post('/refresh', refresh);
|
|
29
|
+
router.post('/logout', logout);
|
|
30
|
+
|
|
31
|
+
// Protected routes
|
|
32
|
+
router.get('/me', authenticate, getMe);
|
|
33
|
+
|
|
34
|
+
export default router;
|