create-nara 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,16 @@ import bcrypt from 'bcrypt';
4
4
  import jwt from 'jsonwebtoken';
5
5
 
6
6
  const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
7
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
8
+
9
+ // Cookie options for auth token
10
+ const COOKIE_OPTIONS = {
11
+ httpOnly: true,
12
+ secure: process.env.NODE_ENV === 'production',
13
+ sameSite: 'lax' as const,
14
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
15
+ path: '/',
16
+ };
7
17
 
8
18
  export class AuthController extends BaseController {
9
19
  async login(req: NaraRequest, res: NaraResponse) {
@@ -14,15 +24,25 @@ export class AuthController extends BaseController {
14
24
  }
15
25
 
16
26
  // TODO: Replace with your actual user lookup
17
- // const user = await User.findByEmail(email);
27
+ // const user = await UserModel.findByEmail(email);
18
28
  // if (!user || !await bcrypt.compare(password, user.password)) {
19
- // return jsonError(res, 'Invalid credentials', 401);
29
+ // throw new ValidationError({ email: ['Invalid credentials'] });
20
30
  // }
21
31
 
22
- // Example: Generate JWT token
23
- const token = jwt.sign({ userId: 1, email }, JWT_SECRET, { expiresIn: '7d' });
32
+ // Example: Generate JWT token with user info
33
+ const token = jwt.sign(
34
+ { userId: 1, email, name: 'Demo User' },
35
+ JWT_SECRET,
36
+ { expiresIn: JWT_EXPIRES_IN }
37
+ );
38
+
39
+ // Set auth cookie for web routes
40
+ res.cookie('auth_token', token, COOKIE_OPTIONS);
24
41
 
25
- return jsonSuccess(res, { token, message: 'Login successful' });
42
+ return jsonSuccess(res, {
43
+ user: { id: 1, email, name: 'Demo User' },
44
+ redirect: '/dashboard'
45
+ }, 'Login successful');
26
46
  }
27
47
 
28
48
  async register(req: NaraRequest, res: NaraResponse) {
@@ -40,15 +60,25 @@ export class AuthController extends BaseController {
40
60
  const hashedPassword = await bcrypt.hash(password, 10);
41
61
 
42
62
  // 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 }, 'Registration successful');
63
+ // const user = await UserModel.create({ name, email, password: hashedPassword });
64
+
65
+ // Generate JWT token
66
+ const token = jwt.sign(
67
+ { userId: 1, email, name },
68
+ JWT_SECRET,
69
+ { expiresIn: JWT_EXPIRES_IN }
70
+ );
71
+
72
+ // Set auth cookie for web routes
73
+ res.cookie('auth_token', token, COOKIE_OPTIONS);
74
+
75
+ return jsonSuccess(res, {
76
+ user: { id: 1, email, name },
77
+ redirect: '/dashboard'
78
+ }, 'Registration successful');
48
79
  }
49
80
 
50
81
  async me(req: NaraRequest, res: NaraResponse) {
51
- // TODO: Get user from JWT token in auth middleware
52
82
  const user = req.user;
53
83
 
54
84
  if (!user) {
@@ -59,7 +89,9 @@ export class AuthController extends BaseController {
59
89
  }
60
90
 
61
91
  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' });
92
+ // Clear auth cookie
93
+ res.cookie('auth_token', '', { ...COOKIE_OPTIONS, maxAge: 0 });
94
+
95
+ return jsonSuccess(res, { redirect: '/login' }, 'Logged out successfully');
64
96
  }
65
97
  }
@@ -3,6 +3,9 @@ import jwt from 'jsonwebtoken';
3
3
 
4
4
  const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
5
5
 
6
+ /**
7
+ * Auth middleware for API routes (Bearer token)
8
+ */
6
9
  export function authMiddleware(req: NaraRequest, res: NaraResponse, next: () => void) {
7
10
  const authHeader = req.headers.authorization;
8
11
 
@@ -21,3 +24,62 @@ export function authMiddleware(req: NaraRequest, res: NaraResponse, next: () =>
21
24
  res.status(401).json({ error: 'Invalid token' });
22
25
  }
23
26
  }
27
+
28
+ /**
29
+ * Auth middleware for web/Inertia routes (cookie-based)
30
+ * Redirects to login if not authenticated
31
+ */
32
+ export function webAuthMiddleware(req: NaraRequest, res: NaraResponse, next: () => void) {
33
+ // Check for auth token in cookie
34
+ const token = req.cookies?.auth_token;
35
+
36
+ if (!token) {
37
+ // For Inertia requests, redirect to login
38
+ if (req.headers['x-inertia']) {
39
+ res.status(409).setHeader('X-Inertia-Location', '/login').send('');
40
+ } else {
41
+ res.redirect('/login');
42
+ }
43
+ return;
44
+ }
45
+
46
+ try {
47
+ const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; email: string; name: string };
48
+ req.user = { id: decoded.userId, email: decoded.email, name: decoded.name };
49
+ next();
50
+ } catch (error) {
51
+ // Clear invalid token
52
+ res.cookie('auth_token', '', { maxAge: 0 });
53
+ if (req.headers['x-inertia']) {
54
+ res.status(409).setHeader('X-Inertia-Location', '/login').send('');
55
+ } else {
56
+ res.redirect('/login');
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Guest middleware - only allow unauthenticated users
63
+ * Redirects to dashboard if already logged in
64
+ */
65
+ export function guestMiddleware(req: NaraRequest, res: NaraResponse, next: () => void) {
66
+ const token = req.cookies?.auth_token;
67
+
68
+ if (token) {
69
+ try {
70
+ jwt.verify(token, JWT_SECRET);
71
+ // User is authenticated, redirect to dashboard
72
+ if (req.headers['x-inertia']) {
73
+ res.status(409).setHeader('X-Inertia-Location', '/dashboard').send('');
74
+ } else {
75
+ res.redirect('/dashboard');
76
+ }
77
+ return;
78
+ } catch {
79
+ // Invalid token, clear it and continue
80
+ res.cookie('auth_token', '', { maxAge: 0 });
81
+ }
82
+ }
83
+
84
+ next();
85
+ }
@@ -1,29 +1,37 @@
1
1
  import type { NaraApp } from '@nara-web/core';
2
+ import { webAuthMiddleware, guestMiddleware } from '../app/middlewares/auth.js';
2
3
 
3
4
  export function registerRoutes(app: NaraApp) {
5
+ // Public routes
4
6
  app.get('/', (req, res) => {
5
7
  res.inertia?.('landing', {
6
8
  title: 'Welcome to NARA'
7
9
  });
8
10
  });
9
11
 
10
- app.get('/dashboard', (req, res) => {
11
- res.inertia?.('dashboard');
12
- });
13
-
14
- app.get('/login', (req, res) => {
12
+ // Guest only routes (redirect to dashboard if logged in)
13
+ app.get('/login', guestMiddleware as any, (req, res) => {
15
14
  res.inertia?.('auth/login');
16
15
  });
17
16
 
18
- app.get('/register', (req, res) => {
17
+ app.get('/register', guestMiddleware as any, (req, res) => {
19
18
  res.inertia?.('auth/register');
20
19
  });
21
20
 
22
- app.get('/users', (req, res) => {
21
+ app.get('/forgot-password', guestMiddleware as any, (req, res) => {
22
+ res.inertia?.('auth/forgot-password');
23
+ });
24
+
25
+ // Protected routes (redirect to login if not authenticated)
26
+ app.get('/dashboard', webAuthMiddleware as any, (req, res) => {
27
+ res.inertia?.('dashboard');
28
+ });
29
+
30
+ app.get('/users', webAuthMiddleware as any, (req, res) => {
23
31
  res.inertia?.('users');
24
32
  });
25
33
 
26
- app.get('/profile', (req, res) => {
34
+ app.get('/profile', webAuthMiddleware as any, (req, res) => {
27
35
  res.inertia?.('profile');
28
36
  });
29
37
  }