create-nara 1.0.35 → 1.0.36

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.

Potentially problematic release.


This version of create-nara might be problematic. Click here for more details.

package/dist/template.js CHANGED
@@ -102,6 +102,9 @@ function createPackageJson(name, mode, features) {
102
102
  pkg.devDependencies['vite'] = '^6.0.0';
103
103
  pkg.devDependencies['@vitejs/plugin-vue'] = '^5.0.0';
104
104
  pkg.devDependencies['concurrently'] = '^9.0.0';
105
+ pkg.devDependencies['tailwindcss'] = '^4.0.0';
106
+ pkg.devDependencies['@tailwindcss/postcss'] = '^4.0.0';
107
+ pkg.devDependencies['autoprefixer'] = '^10.4.20';
105
108
  }
106
109
  if (features.includes('db')) {
107
110
  pkg.dependencies['knex'] = '^3.1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { BaseController, jsonSuccess, jsonError, ValidationError } from '@nara-web/core';
1
+ import { BaseController, jsonSuccess, jsonError } from '@nara-web/core';
2
2
  import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
3
  import { UserModel } from '../models/User.js';
4
4
  import bcrypt from 'bcrypt';
@@ -20,13 +20,16 @@ export class AuthController extends BaseController {
20
20
  const { email, password } = await req.json();
21
21
 
22
22
  if (!email || !password) {
23
- throw new ValidationError({ email: ['Email and password are required'] });
23
+ // Set error cookie and redirect back (Inertia pattern)
24
+ res.cookie('error', 'Email and password are required', 5000);
25
+ return res.redirect('/login');
24
26
  }
25
27
 
26
28
  // Find user by email
27
29
  const user = await UserModel.findByEmail(email);
28
30
  if (!user || !await bcrypt.compare(password, user.password)) {
29
- throw new ValidationError({ email: ['Invalid credentials'] });
31
+ res.cookie('error', 'Invalid credentials', 5000);
32
+ return res.redirect('/login');
30
33
  }
31
34
 
32
35
  // Generate JWT token with user info
@@ -39,27 +42,23 @@ export class AuthController extends BaseController {
39
42
  // Set auth cookie for web routes (maxAge in ms)
40
43
  res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
41
44
 
42
- return jsonSuccess(res, {
43
- user: { id: user.id, email: user.email, name: user.name },
44
- redirect: '/dashboard'
45
- }, 'Login successful');
45
+ // Redirect to dashboard (Inertia will handle it)
46
+ return res.redirect('/dashboard');
46
47
  }
47
48
 
48
49
  async register(req: NaraRequest, res: NaraResponse) {
49
50
  const { name, email, password } = await req.json();
50
51
 
51
52
  if (!name || !email || !password) {
52
- throw new ValidationError({
53
- name: !name ? ['Name is required'] : [],
54
- email: !email ? ['Email is required'] : [],
55
- password: !password ? ['Password is required'] : [],
56
- });
53
+ res.cookie('error', 'Name, email and password are required', 5000);
54
+ return res.redirect('/register');
57
55
  }
58
56
 
59
57
  // Check if email already exists
60
58
  const existing = await UserModel.findByEmail(email);
61
59
  if (existing) {
62
- throw new ValidationError({ email: ['Email already registered'] });
60
+ res.cookie('error', 'Email already registered', 5000);
61
+ return res.redirect('/register');
63
62
  }
64
63
 
65
64
  // Hash password
@@ -78,10 +77,8 @@ export class AuthController extends BaseController {
78
77
  // Set auth cookie for web routes (maxAge in ms)
79
78
  res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
80
79
 
81
- return jsonSuccess(res, {
82
- user: { id: userId, email, name },
83
- redirect: '/dashboard'
84
- }, 'Registration successful');
80
+ // Redirect to dashboard
81
+ return res.redirect('/dashboard');
85
82
  }
86
83
 
87
84
  async me(req: NaraRequest, res: NaraResponse) {
@@ -98,46 +95,36 @@ export class AuthController extends BaseController {
98
95
  // Clear auth cookie (set maxAge to 0)
99
96
  res.cookie('auth_token', '', 0, COOKIE_OPTIONS);
100
97
 
101
- return jsonSuccess(res, { redirect: '/login' }, 'Logged out successfully');
98
+ // Redirect to login
99
+ return res.redirect('/login');
102
100
  }
103
101
 
104
102
  async forgotPassword(req: NaraRequest, res: NaraResponse) {
105
103
  const { email } = await req.json();
106
104
 
107
105
  if (!email) {
108
- throw new ValidationError({ email: ['Email is required'] });
106
+ res.cookie('error', 'Email is required', 5000);
107
+ return res.redirect('/forgot-password');
109
108
  }
110
109
 
111
110
  // TODO: Implement actual password reset email sending
112
- // const user = await UserModel.findByEmail(email);
113
- // if (user) {
114
- // const token = generateResetToken();
115
- // await sendResetEmail(user.email, token);
116
- // }
117
-
118
- // Always return success to prevent email enumeration
119
- return jsonSuccess(res, {}, 'If an account exists with this email, a reset link has been sent.');
111
+
112
+ // Set success message and redirect
113
+ res.cookie('success', 'If an account exists with this email, a reset link has been sent.', 5000);
114
+ return res.redirect('/login');
120
115
  }
121
116
 
122
117
  async resetPassword(req: NaraRequest, res: NaraResponse) {
123
118
  const { token, password } = await req.json();
124
119
 
125
120
  if (!token || !password) {
126
- throw new ValidationError({
127
- token: !token ? ['Reset token is required'] : [],
128
- password: !password ? ['Password is required'] : [],
129
- });
121
+ res.cookie('error', 'Reset token and password are required', 5000);
122
+ return res.redirect('/forgot-password');
130
123
  }
131
124
 
132
125
  // TODO: Implement actual password reset
133
- // const resetRecord = await PasswordReset.findByToken(token);
134
- // if (!resetRecord || resetRecord.expired) {
135
- // throw new ValidationError({ token: ['Invalid or expired reset token'] });
136
- // }
137
- // const hashedPassword = await bcrypt.hash(password, 10);
138
- // await UserModel.updatePassword(resetRecord.userId, hashedPassword);
139
- // await PasswordReset.delete(token);
140
-
141
- return jsonSuccess(res, { redirect: '/login' }, 'Password has been reset successfully.');
126
+
127
+ res.cookie('success', 'Password has been reset successfully.', 5000);
128
+ return res.redirect('/login');
142
129
  }
143
130
  }
@@ -1,4 +1,4 @@
1
- import { BaseController, jsonSuccess, jsonError, ValidationError } from '@nara-web/core';
1
+ import { BaseController, jsonSuccess, jsonError } from '@nara-web/core';
2
2
  import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
3
  import { UserModel } from '../models/User.js';
4
4
  import bcrypt from 'bcrypt';
@@ -21,62 +21,58 @@ export class ProfileController extends BaseController {
21
21
  async update(req: NaraRequest, res: NaraResponse) {
22
22
  const user = req.user;
23
23
  if (!user) {
24
- return res.status(401).json({ success: false, message: 'Unauthorized' });
24
+ return res.redirect('/login');
25
25
  }
26
26
 
27
27
  const { name, email, phone } = await req.json();
28
28
 
29
29
  if (!name || !email) {
30
- throw new ValidationError({
31
- name: !name ? ['Name is required'] : [],
32
- email: !email ? ['Email is required'] : [],
33
- });
30
+ res.cookie('error', 'Name and email are required', 5000);
31
+ return res.redirect('/profile');
34
32
  }
35
33
 
36
34
  // Update user in database
37
35
  await UserModel.update(user.id, { name, email, phone });
38
36
 
39
- return jsonSuccess(res, {
40
- user: { ...user, name, email, phone }
41
- }, 'Profile updated successfully');
37
+ res.cookie('success', 'Profile updated successfully', 5000);
38
+ return res.redirect('/profile');
42
39
  }
43
40
 
44
41
  async changePassword(req: NaraRequest, res: NaraResponse) {
45
42
  const user = req.user;
46
43
  if (!user) {
47
- return res.status(401).json({ success: false, message: 'Unauthorized' });
44
+ return res.redirect('/login');
48
45
  }
49
46
 
50
47
  const { current_password, new_password } = await req.json();
51
48
 
52
49
  if (!current_password || !new_password) {
53
- throw new ValidationError({
54
- current_password: !current_password ? ['Current password is required'] : [],
55
- new_password: !new_password ? ['New password is required'] : [],
56
- });
50
+ res.cookie('error', 'Current password and new password are required', 5000);
51
+ return res.redirect('/profile');
57
52
  }
58
53
 
59
54
  if (new_password.length < 6) {
60
- throw new ValidationError({
61
- new_password: ['Password must be at least 6 characters'],
62
- });
55
+ res.cookie('error', 'Password must be at least 6 characters', 5000);
56
+ return res.redirect('/profile');
63
57
  }
64
58
 
65
59
  // Verify current password and update
66
60
  const dbUser = await UserModel.findById(user.id);
67
61
  if (!dbUser || !await bcrypt.compare(current_password, dbUser.password)) {
68
- throw new ValidationError({ current_password: ['Current password is incorrect'] });
62
+ res.cookie('error', 'Current password is incorrect', 5000);
63
+ return res.redirect('/profile');
69
64
  }
70
65
  const hashedPassword = await bcrypt.hash(new_password, 10);
71
66
  await UserModel.update(user.id, { password: hashedPassword });
72
67
 
73
- return jsonSuccess(res, {}, 'Password changed successfully');
68
+ res.cookie('success', 'Password changed successfully', 5000);
69
+ return res.redirect('/profile');
74
70
  }
75
71
 
76
72
  async uploadAvatar(req: NaraRequest, res: NaraResponse) {
77
73
  const user = req.user;
78
74
  if (!user) {
79
- return res.status(401).json({ success: false, message: 'Unauthorized' });
75
+ return res.redirect('/login');
80
76
  }
81
77
 
82
78
  try {
@@ -84,13 +80,15 @@ export class ProfileController extends BaseController {
84
80
  const buffer = await req.buffer();
85
81
 
86
82
  if (!buffer || buffer.length === 0) {
87
- return jsonError(res, 'No file uploaded', 400);
83
+ res.cookie('error', 'No file uploaded', 5000);
84
+ return res.redirect('/profile');
88
85
  }
89
86
 
90
87
  // Extract image from multipart form data
91
88
  const boundary = req.headers['content-type']?.split('boundary=')[1];
92
89
  if (!boundary) {
93
- return jsonError(res, 'Invalid multipart form data', 400);
90
+ res.cookie('error', 'Invalid multipart form data', 5000);
91
+ return res.redirect('/profile');
94
92
  }
95
93
 
96
94
  // Find image data in multipart
@@ -109,7 +107,8 @@ export class ProfileController extends BaseController {
109
107
  }
110
108
 
111
109
  if (!imageBuffer) {
112
- return jsonError(res, 'No image found in upload', 400);
110
+ res.cookie('error', 'No image found in upload', 5000);
111
+ return res.redirect('/profile');
113
112
  }
114
113
 
115
114
  // Generate unique filename
@@ -127,6 +126,7 @@ export class ProfileController extends BaseController {
127
126
  // Update user avatar in database
128
127
  await UserModel.update(user.id, { avatar: avatarUrl });
129
128
 
129
+ // Return JSON for AJAX upload (not redirect)
130
130
  return jsonSuccess(res, { url: avatarUrl }, 'Avatar uploaded successfully');
131
131
  } catch (error) {
132
132
  console.error('Avatar upload error:', error);
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from "fs";
2
+ import path from "path";
3
+
4
+ const templateCache: { [key: string]: string } = {};
5
+
6
+ function getViewsDirectory() {
7
+ return process.env.NODE_ENV === 'development' ? "resources/views" : "dist/views";
8
+ }
9
+
10
+ function loadTemplate(filename: string): string {
11
+ const directory = getViewsDirectory();
12
+ const key = path.join(directory, filename);
13
+ if (!templateCache[key]) {
14
+ templateCache[key] = readFileSync(key, "utf8");
15
+ }
16
+ return templateCache[key];
17
+ }
18
+
19
+ function escapeHtml(value: string): string {
20
+ return value
21
+ .replace(/&/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/\"/g, "&quot;")
25
+ .replace(/'/g, "&#039;");
26
+ }
27
+
28
+ function getViteScripts(): string {
29
+ const isDev = process.env.NODE_ENV !== 'production';
30
+ if (isDev) {
31
+ return `
32
+ <script type="module" src="http://localhost:5173/@vite/client"></script>
33
+ <script type="module" src="http://localhost:5173/resources/js/app.ts"></script>
34
+ `;
35
+ }
36
+ // For production, read manifest and generate script tags
37
+ try {
38
+ const manifest = JSON.parse(readFileSync('public/build/.vite/manifest.json', 'utf8'));
39
+ const entry = manifest['resources/js/app.ts'];
40
+ if (entry) {
41
+ return `<script type="module" src="/build/${entry.file}"></script>`;
42
+ }
43
+ } catch {
44
+ // Fallback
45
+ }
46
+ return '';
47
+ }
48
+
49
+ export function view(filename: string, view_data?: any) {
50
+ const baseTemplate = loadTemplate(filename);
51
+ let html = baseTemplate;
52
+
53
+ // Replace @vite placeholder with Vite scripts
54
+ html = html.replace('@vite', getViteScripts());
55
+
56
+ // Replace @inertia placeholder with inertia root div
57
+ if (view_data?.page) {
58
+ html = html.replace('@inertia', `<div id="app" data-page='${view_data.page}'></div>`);
59
+ } else {
60
+ html = html.replace('@inertia', '<div id="app"></div>');
61
+ }
62
+
63
+ // Handle other template variables
64
+ if (view_data) {
65
+ if (typeof view_data.title === "string") {
66
+ html = html.replace("{{it.title}}", escapeHtml(view_data.title));
67
+ }
68
+ }
69
+
70
+ return html;
71
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
@@ -1,5 +1,5 @@
1
1
  <template>
2
2
  <span class="flex gap-2 items-center">
3
- <img class="h-12 w-12 rounded-2xl" src="/public/nara.png" alt="Nara logo" />
3
+ <img class="h-12 w-12 rounded-2xl" src="/nara.png" alt="Nara logo" />
4
4
  </span>
5
5
  </template>
@@ -25,7 +25,7 @@ export function registerRoutes(app: NaraApp) {
25
25
  (res as InertiaResponse).inertia('landing', { title: 'Welcome to NARA' });
26
26
  });
27
27
 
28
- // Guest only
28
+ // Guest only - Pages
29
29
  app.get('/login', guestMiddleware as any, (req, res: any) => {
30
30
  (res as InertiaResponse).inertia('auth/login');
31
31
  });
@@ -39,7 +39,14 @@ export function registerRoutes(app: NaraApp) {
39
39
  (res as InertiaResponse).inertia('auth/reset-password', { token: req.params.token });
40
40
  });
41
41
 
42
- // Protected
42
+ // Auth Form Actions (same path as pages - for Inertia form handling)
43
+ app.post('/login', wrapHandler((req, res) => auth.login(req, res)));
44
+ app.post('/register', wrapHandler((req, res) => auth.register(req, res)));
45
+ app.post('/logout', wrapHandler((req, res) => auth.logout(req, res)));
46
+ app.post('/forgot-password', wrapHandler((req, res) => auth.forgotPassword(req, res)));
47
+ app.post('/reset-password', wrapHandler((req, res) => auth.resetPassword(req, res)));
48
+
49
+ // Protected - Pages
43
50
  app.get('/dashboard', webAuthMiddleware as any, (req, res: any) => {
44
51
  (res as InertiaResponse).inertia('dashboard');
45
52
  });
@@ -82,13 +89,13 @@ export function registerRoutes(app: NaraApp) {
82
89
  const total = Number(totalCount);
83
90
 
84
91
  // Apply pagination and ordering
85
- const users = await query
92
+ const usersData = await query
86
93
  .orderBy('created_at', 'desc')
87
94
  .limit(limit)
88
95
  .offset(offset);
89
96
 
90
97
  // Transform users to include is_admin and is_verified flags
91
- const transformedUsers = users.map((user: any) => ({
98
+ const transformedUsers = usersData.map((user: any) => ({
92
99
  ...user,
93
100
  is_admin: user.role === 'admin',
94
101
  is_verified: !!user.email_verified_at
@@ -112,8 +119,16 @@ export function registerRoutes(app: NaraApp) {
112
119
  (res as InertiaResponse).inertia('profile');
113
120
  });
114
121
 
122
+ // Protected Form Actions (same path pattern - for Inertia form handling)
123
+ app.post('/change-profile', webAuthMiddleware as any, wrapHandler((req, res) => profile.update(req, res)));
124
+ app.post('/change-password', webAuthMiddleware as any, wrapHandler((req, res) => profile.changePassword(req, res)));
125
+ app.post('/users', webAuthMiddleware as any, wrapHandler((req, res) => users.store(req, res)));
126
+ app.put('/users/:id', webAuthMiddleware as any, wrapHandler((req, res) => users.update(req, res)));
127
+ app.delete('/users', webAuthMiddleware as any, wrapHandler((req, res) => users.destroy(req, res)));
128
+ app.post('/assets/avatar', webAuthMiddleware as any, wrapHandler((req, res) => profile.uploadAvatar(req, res)));
129
+
115
130
 
116
- // --- API Routes ---
131
+ // --- API Routes (for programmatic access) ---
117
132
 
118
133
  // Auth
119
134
  app.post('/api/auth/login', wrapHandler((req, res) => auth.login(req, res)));
@@ -143,4 +158,12 @@ export function registerRoutes(app: NaraApp) {
143
158
  const filePath = req.path.replace('/uploads/', '');
144
159
  res.sendFile(`uploads/${filePath}`);
145
160
  });
161
+ app.get('/public/*', (req, res) => {
162
+ const filePath = req.path.replace('/public/', '');
163
+ res.sendFile(`public/${filePath}`);
164
+ });
165
+ app.get('/storage/*', (req, res) => {
166
+ const filePath = req.path.replace('/storage/', '');
167
+ res.sendFile(`storage/${filePath}`);
168
+ });
146
169
  }
@@ -2,10 +2,11 @@ import 'dotenv/config';
2
2
  import { createApp, jsonError } from '@nara-web/core';
3
3
  import { vueAdapter } from '@nara-web/inertia-vue';
4
4
  import { registerRoutes } from './routes/web.js';
5
+ import { view } from './app/services/View.js';
5
6
 
6
7
  const app = createApp({
7
8
  port: Number(process.env.PORT) || 3000,
8
- adapter: vueAdapter()
9
+ adapter: vueAdapter({ viewFn: view } as any)
9
10
  });
10
11
 
11
12
  // Global error handler