create-nara 0.1.0 → 0.1.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.js CHANGED
File without changes
package/dist/template.js CHANGED
@@ -16,10 +16,26 @@ export async function setupProject(options) {
16
16
  if (fs.existsSync(modeTemplateDir)) {
17
17
  copyDir(modeTemplateDir, targetDir);
18
18
  }
19
- // 3. Ensure required directories exist
19
+ // 3. Copy feature-specific templates
20
+ const featuresDir = path.join(templatesDir, 'features');
21
+ for (const feature of features) {
22
+ const featureDir = path.join(featuresDir, feature);
23
+ if (fs.existsSync(featureDir)) {
24
+ copyDir(featureDir, targetDir);
25
+ }
26
+ }
27
+ // 4. Ensure required directories exist
20
28
  fs.mkdirSync(path.join(targetDir, 'app/controllers'), { recursive: true });
21
29
  fs.mkdirSync(path.join(targetDir, 'app/models'), { recursive: true });
22
- // 4. Generate package.json (dynamic content)
30
+ // Create database directory if db feature is selected
31
+ if (features.includes('db')) {
32
+ fs.mkdirSync(path.join(targetDir, 'database'), { recursive: true });
33
+ }
34
+ // Create uploads directory if uploads feature is selected
35
+ if (features.includes('uploads')) {
36
+ fs.mkdirSync(path.join(targetDir, 'uploads'), { recursive: true });
37
+ }
38
+ // 5. Generate package.json (dynamic content)
23
39
  const pkg = createPackageJson(projectName, mode, features);
24
40
  fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
25
41
  }
@@ -63,6 +79,20 @@ function createPackageJson(name, mode, features) {
63
79
  if (features.includes('db')) {
64
80
  pkg.dependencies['knex'] = '^3.1.0';
65
81
  pkg.dependencies['better-sqlite3'] = '^11.0.0';
82
+ pkg.devDependencies['@types/better-sqlite3'] = '^7.6.0';
83
+ pkg.scripts['db:migrate'] = 'knex migrate:latest';
84
+ pkg.scripts['db:rollback'] = 'knex migrate:rollback';
85
+ pkg.scripts['db:make'] = 'knex migrate:make';
86
+ }
87
+ if (features.includes('auth')) {
88
+ pkg.dependencies['bcrypt'] = '^5.1.0';
89
+ pkg.dependencies['jsonwebtoken'] = '^9.0.0';
90
+ pkg.devDependencies['@types/bcrypt'] = '^5.0.0';
91
+ pkg.devDependencies['@types/jsonwebtoken'] = '^9.0.0';
92
+ }
93
+ if (features.includes('uploads')) {
94
+ pkg.dependencies['sharp'] = '^0.33.0';
95
+ pkg.devDependencies['@types/sharp'] = '^0.32.0';
66
96
  }
67
97
  return pkg;
68
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,65 @@
1
+ import { BaseController, jsonSuccess, jsonError, ValidationError } from '@nara-web/core';
2
+ import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
+ import bcrypt from 'bcrypt';
4
+ import jwt from 'jsonwebtoken';
5
+
6
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
7
+
8
+ export class AuthController extends BaseController {
9
+ async login(req: NaraRequest, res: NaraResponse) {
10
+ const { email, password } = await req.json();
11
+
12
+ if (!email || !password) {
13
+ throw new ValidationError({ email: ['Email and password are required'] });
14
+ }
15
+
16
+ // TODO: Replace with your actual user lookup
17
+ // const user = await User.findByEmail(email);
18
+ // if (!user || !await bcrypt.compare(password, user.password)) {
19
+ // return jsonError(res, 'Invalid credentials', 401);
20
+ // }
21
+
22
+ // Example: Generate JWT token
23
+ const token = jwt.sign({ userId: 1, email }, JWT_SECRET, { expiresIn: '7d' });
24
+
25
+ return jsonSuccess(res, { token, message: 'Login successful' });
26
+ }
27
+
28
+ async register(req: NaraRequest, res: NaraResponse) {
29
+ const { name, email, password } = await req.json();
30
+
31
+ if (!name || !email || !password) {
32
+ throw new ValidationError({
33
+ name: !name ? ['Name is required'] : [],
34
+ email: !email ? ['Email is required'] : [],
35
+ password: !password ? ['Password is required'] : [],
36
+ });
37
+ }
38
+
39
+ // Hash password
40
+ const hashedPassword = await bcrypt.hash(password, 10);
41
+
42
+ // TODO: Replace with your actual user creation
43
+ // const user = await User.create({ name, email, password: hashedPassword });
44
+
45
+ const token = jwt.sign({ userId: 1, email }, JWT_SECRET, { expiresIn: '7d' });
46
+
47
+ return jsonSuccess(res, { token, message: 'Registration successful' }, 201);
48
+ }
49
+
50
+ async me(req: NaraRequest, res: NaraResponse) {
51
+ // TODO: Get user from JWT token in auth middleware
52
+ const user = req.user;
53
+
54
+ if (!user) {
55
+ return jsonError(res, 'Unauthorized', 401);
56
+ }
57
+
58
+ return jsonSuccess(res, { user });
59
+ }
60
+
61
+ async logout(req: NaraRequest, res: NaraResponse) {
62
+ // For JWT, logout is typically handled client-side by removing the token
63
+ return jsonSuccess(res, { message: 'Logged out successfully' });
64
+ }
65
+ }
@@ -0,0 +1,23 @@
1
+ import type { NaraRequest, NaraResponse } from '@nara-web/core';
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
5
+
6
+ export function authMiddleware(req: NaraRequest, res: NaraResponse, next: () => void) {
7
+ const authHeader = req.headers.authorization;
8
+
9
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
10
+ res.status(401).json({ error: 'Unauthorized' });
11
+ return;
12
+ }
13
+
14
+ const token = authHeader.substring(7);
15
+
16
+ try {
17
+ const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
18
+ req.user = { id: decoded.userId, email: decoded.email, name: '' };
19
+ next();
20
+ } catch (error) {
21
+ res.status(401).json({ error: 'Invalid token' });
22
+ }
23
+ }
@@ -0,0 +1,15 @@
1
+ import type { NaraApp } from '@nara-web/core';
2
+ import { AuthController } from '../app/controllers/AuthController.js';
3
+ import { authMiddleware } from '../app/middlewares/auth.js';
4
+
5
+ export function registerAuthRoutes(app: NaraApp) {
6
+ const auth = new AuthController();
7
+
8
+ // Public routes
9
+ app.post('/api/auth/login', (req, res) => auth.login(req, res));
10
+ app.post('/api/auth/register', (req, res) => auth.register(req, res));
11
+
12
+ // Protected routes
13
+ app.get('/api/auth/me', authMiddleware, (req, res) => auth.me(req, res));
14
+ app.post('/api/auth/logout', authMiddleware, (req, res) => auth.logout(req, res));
15
+ }
@@ -0,0 +1,16 @@
1
+ import Knex from 'knex';
2
+
3
+ const config = {
4
+ client: 'better-sqlite3',
5
+ connection: {
6
+ filename: './database/dev.sqlite3',
7
+ },
8
+ useNullAsDefault: true,
9
+ migrations: {
10
+ directory: './migrations',
11
+ extension: 'ts',
12
+ },
13
+ };
14
+
15
+ export const db = Knex(config);
16
+ export default config;
@@ -0,0 +1,40 @@
1
+ import { db } from '../app/config/database.js';
2
+
3
+ export interface User {
4
+ id: number;
5
+ name: string;
6
+ email: string;
7
+ password: string;
8
+ role: string;
9
+ email_verified_at: string | null;
10
+ created_at: string;
11
+ updated_at: string;
12
+ }
13
+
14
+ export class UserModel {
15
+ static tableName = 'users';
16
+
17
+ static async findById(id: number): Promise<User | undefined> {
18
+ return db(this.tableName).where({ id }).first();
19
+ }
20
+
21
+ static async findByEmail(email: string): Promise<User | undefined> {
22
+ return db(this.tableName).where({ email }).first();
23
+ }
24
+
25
+ static async create(data: Partial<User>): Promise<number[]> {
26
+ return db(this.tableName).insert(data);
27
+ }
28
+
29
+ static async update(id: number, data: Partial<User>): Promise<number> {
30
+ return db(this.tableName).where({ id }).update(data);
31
+ }
32
+
33
+ static async delete(id: number): Promise<number> {
34
+ return db(this.tableName).where({ id }).delete();
35
+ }
36
+
37
+ static async all(): Promise<User[]> {
38
+ return db(this.tableName).select('*');
39
+ }
40
+ }
@@ -0,0 +1,28 @@
1
+ import type { Knex } from 'knex';
2
+
3
+ const config: { [key: string]: Knex.Config } = {
4
+ development: {
5
+ client: 'better-sqlite3',
6
+ connection: {
7
+ filename: './database/dev.sqlite3',
8
+ },
9
+ useNullAsDefault: true,
10
+ migrations: {
11
+ directory: './migrations',
12
+ extension: 'ts',
13
+ },
14
+ },
15
+
16
+ production: {
17
+ client: 'better-sqlite3',
18
+ connection: {
19
+ filename: './database/production.sqlite3',
20
+ },
21
+ useNullAsDefault: true,
22
+ migrations: {
23
+ directory: './migrations',
24
+ },
25
+ },
26
+ };
27
+
28
+ export default config;
@@ -0,0 +1,17 @@
1
+ import type { Knex } from 'knex';
2
+
3
+ export async function up(knex: Knex): Promise<void> {
4
+ await knex.schema.createTable('users', (table) => {
5
+ table.increments('id').primary();
6
+ table.string('name').notNullable();
7
+ table.string('email').notNullable().unique();
8
+ table.string('password').notNullable();
9
+ table.string('role').defaultTo('user');
10
+ table.timestamp('email_verified_at').nullable();
11
+ table.timestamps(true, true);
12
+ });
13
+ }
14
+
15
+ export async function down(knex: Knex): Promise<void> {
16
+ await knex.schema.dropTableIfExists('users');
17
+ }
@@ -0,0 +1,62 @@
1
+ import { BaseController, jsonSuccess, jsonError } from '@nara-web/core';
2
+ import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
+ import sharp from 'sharp';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import crypto from 'crypto';
7
+
8
+ const UPLOAD_DIR = './uploads';
9
+
10
+ export class UploadController extends BaseController {
11
+ constructor() {
12
+ super();
13
+ // Ensure upload directory exists
14
+ if (!fs.existsSync(UPLOAD_DIR)) {
15
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true });
16
+ }
17
+ }
18
+
19
+ async upload(req: NaraRequest, res: NaraResponse) {
20
+ try {
21
+ const contentType = req.headers['content-type'] || '';
22
+
23
+ if (!contentType.includes('multipart/form-data')) {
24
+ return jsonError(res, 'Content-Type must be multipart/form-data', 400);
25
+ }
26
+
27
+ // Get raw body as buffer
28
+ const buffer = await req.buffer();
29
+
30
+ // Generate unique filename
31
+ const filename = `${crypto.randomUUID()}.webp`;
32
+ const filepath = path.join(UPLOAD_DIR, filename);
33
+
34
+ // Process and save image with sharp
35
+ await sharp(buffer)
36
+ .resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
37
+ .webp({ quality: 80 })
38
+ .toFile(filepath);
39
+
40
+ return jsonSuccess(res, {
41
+ filename,
42
+ path: `/uploads/${filename}`,
43
+ message: 'File uploaded successfully',
44
+ }, 201);
45
+ } catch (error) {
46
+ console.error('Upload error:', error);
47
+ return jsonError(res, 'Failed to upload file', 500);
48
+ }
49
+ }
50
+
51
+ async delete(req: NaraRequest, res: NaraResponse) {
52
+ const { filename } = req.params;
53
+ const filepath = path.join(UPLOAD_DIR, filename);
54
+
55
+ if (!fs.existsSync(filepath)) {
56
+ return jsonError(res, 'File not found', 404);
57
+ }
58
+
59
+ fs.unlinkSync(filepath);
60
+ return jsonSuccess(res, { message: 'File deleted successfully' });
61
+ }
62
+ }
@@ -0,0 +1,9 @@
1
+ import type { NaraApp } from '@nara-web/core';
2
+ import { UploadController } from '../app/controllers/UploadController.js';
3
+
4
+ export function registerUploadRoutes(app: NaraApp) {
5
+ const upload = new UploadController();
6
+
7
+ app.post('/api/uploads', (req, res) => upload.upload(req, res));
8
+ app.delete('/api/uploads/:filename', (req, res) => upload.delete(req, res));
9
+ }