create-nara 1.0.36 → 1.0.38

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.36",
3
+ "version": "1.0.38",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * OAuthController
3
+ *
4
+ * Handles OAuth authentication flows:
5
+ * - Google OAuth redirect
6
+ * - Google OAuth callback
7
+ */
8
+ import { BaseController } from '@nara-web/core';
9
+ import type { NaraRequest, NaraResponse } from '@nara-web/core';
10
+ import { UserModel } from '../models/User.js';
11
+ import { redirectParamsURL } from '../services/GoogleAuth.js';
12
+ import bcrypt from 'bcrypt';
13
+ import jwt from 'jsonwebtoken';
14
+ import { randomUUID } from 'crypto';
15
+
16
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
17
+ const JWT_EXPIRES_SECONDS = 7 * 24 * 60 * 60; // 7 days in seconds
18
+
19
+ // Cookie options for auth token
20
+ const COOKIE_OPTIONS = {
21
+ httpOnly: true,
22
+ secure: process.env.NODE_ENV === 'production',
23
+ sameSite: 'lax' as const,
24
+ path: '/',
25
+ };
26
+
27
+ interface GoogleTokenResponse {
28
+ access_token: string;
29
+ expires_in: number;
30
+ token_type: string;
31
+ scope: string;
32
+ refresh_token?: string;
33
+ }
34
+
35
+ interface GoogleUserInfo {
36
+ id: string;
37
+ email: string;
38
+ verified_email: boolean;
39
+ name: string;
40
+ given_name?: string;
41
+ family_name?: string;
42
+ picture?: string;
43
+ }
44
+
45
+ export class OAuthController extends BaseController {
46
+ /**
47
+ * Redirect to Google OAuth
48
+ */
49
+ async googleRedirect(req: NaraRequest, res: NaraResponse) {
50
+ const params = redirectParamsURL();
51
+ const googleLoginUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
52
+ return res.redirect(googleLoginUrl);
53
+ }
54
+
55
+ /**
56
+ * Handle Google OAuth callback
57
+ */
58
+ async googleCallback(req: NaraRequest, res: NaraResponse) {
59
+ const { code } = req.query;
60
+
61
+ if (!code) {
62
+ res.cookie('error', 'Authorization code not provided', 5000);
63
+ return res.redirect('/login');
64
+ }
65
+
66
+ try {
67
+ // Exchange authorization code for access token
68
+ const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/x-www-form-urlencoded',
72
+ },
73
+ body: new URLSearchParams({
74
+ client_id: process.env.GOOGLE_CLIENT_ID || '',
75
+ client_secret: process.env.GOOGLE_CLIENT_SECRET || '',
76
+ redirect_uri: process.env.GOOGLE_REDIRECT_URI || '',
77
+ grant_type: 'authorization_code',
78
+ code: code as string,
79
+ }),
80
+ });
81
+
82
+ if (!tokenResponse.ok) {
83
+ res.cookie('error', 'Failed to exchange authorization code', 5000);
84
+ return res.redirect('/login');
85
+ }
86
+
87
+ const tokenData: GoogleTokenResponse = await tokenResponse.json();
88
+
89
+ // Get user info from Google
90
+ const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
91
+ headers: {
92
+ Authorization: `Bearer ${tokenData.access_token}`,
93
+ },
94
+ });
95
+
96
+ if (!userResponse.ok) {
97
+ res.cookie('error', 'Failed to get user info from Google', 5000);
98
+ return res.redirect('/login');
99
+ }
100
+
101
+ const userData: GoogleUserInfo = await userResponse.json();
102
+ const email = userData.email.toLowerCase();
103
+ const name = userData.name;
104
+
105
+ // Check if user exists
106
+ let user = await UserModel.findByEmail(email);
107
+
108
+ if (!user) {
109
+ // Create new user
110
+ const userId = randomUUID();
111
+ const hashedPassword = await bcrypt.hash(email + Date.now(), 10);
112
+
113
+ await UserModel.create({
114
+ id: userId,
115
+ email,
116
+ password: hashedPassword,
117
+ name,
118
+ role: 'user',
119
+ email_verified_at: userData.verified_email ? new Date().toISOString() : null,
120
+ });
121
+
122
+ user = await UserModel.findById(userId);
123
+ }
124
+
125
+ if (!user) {
126
+ res.cookie('error', 'Failed to create or find user', 5000);
127
+ return res.redirect('/login');
128
+ }
129
+
130
+ // Generate JWT token
131
+ const token = jwt.sign(
132
+ { userId: user.id, email: user.email, name: user.name },
133
+ JWT_SECRET,
134
+ { expiresIn: JWT_EXPIRES_SECONDS }
135
+ );
136
+
137
+ // Set auth cookie
138
+ res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
139
+
140
+ // Redirect to dashboard
141
+ return res.redirect('/dashboard');
142
+ } catch (error) {
143
+ console.error('Google OAuth error:', error);
144
+ res.cookie('error', 'Authentication failed', 5000);
145
+ return res.redirect('/login');
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Google OAuth Authentication Service
3
+ * Handles URL parameter generation for Google OAuth authentication flow.
4
+ */
5
+
6
+ /**
7
+ * Generates the URL parameters required for initiating Google OAuth authentication.
8
+ *
9
+ * @returns A URL-encoded string containing all necessary OAuth parameters
10
+ *
11
+ * Parameters included:
12
+ * - client_id: Your application's Google Client ID
13
+ * - redirect_uri: The URL where Google will redirect after authentication
14
+ * - scope: The permissions requested from the user
15
+ * - userinfo.email: Access to user's email
16
+ * - userinfo.profile: Access to user's basic profile info
17
+ * - response_type: Set to 'code' for authorization code flow
18
+ * - access_type: Set to 'offline' to receive a refresh token
19
+ * - prompt: Set to 'consent' to always show the consent screen
20
+ */
21
+ export function redirectParamsURL(): string {
22
+ return new URLSearchParams({
23
+ client_id: process.env.GOOGLE_CLIENT_ID || '',
24
+ redirect_uri: process.env.GOOGLE_REDIRECT_URI || '',
25
+ scope: [
26
+ 'https://www.googleapis.com/auth/userinfo.email',
27
+ 'https://www.googleapis.com/auth/userinfo.profile',
28
+ ].join(' '),
29
+ response_type: 'code',
30
+ access_type: 'offline',
31
+ prompt: 'consent',
32
+ }).toString();
33
+ }
@@ -18,3 +18,8 @@ JWT_EXPIRES_IN=7d
18
18
  # Session
19
19
  SESSION_SECRET=change-me-to-random-session-secret
20
20
  SESSION_NAME=nara_session
21
+
22
+ # Google OAuth (optional)
23
+ GOOGLE_CLIENT_ID=
24
+ GOOGLE_CLIENT_SECRET=
25
+ GOOGLE_REDIRECT_URI=http://localhost:3000/google/callback
Binary file
@@ -3,6 +3,7 @@ import { AuthController } from '../app/controllers/AuthController.js';
3
3
  import { ProfileController } from '../app/controllers/ProfileController.js';
4
4
  import { UserController } from '../app/controllers/UserController.js';
5
5
  import { UploadController } from '../app/controllers/UploadController.js';
6
+ import { OAuthController } from '../app/controllers/OAuthController.js';
6
7
  import { authMiddleware, webAuthMiddleware, guestMiddleware } from '../app/middlewares/auth.js';
7
8
  import { wrapHandler } from '../app/utils/route-helper.js';
8
9
  import { db } from '../app/config/database.js';
@@ -17,6 +18,7 @@ export function registerRoutes(app: NaraApp) {
17
18
  const profile = new ProfileController();
18
19
  const users = new UserController();
19
20
  const upload = new UploadController();
21
+ const oauth = new OAuthController();
20
22
 
21
23
  // --- Page Routes (Inertia) ---
22
24
 
@@ -112,6 +114,10 @@ export function registerRoutes(app: NaraApp) {
112
114
  (res as InertiaResponse).inertia('profile');
113
115
  });
114
116
 
117
+ // --- Google OAuth Routes ---
118
+ app.get('/google/redirect', wrapHandler((req, res) => oauth.googleRedirect(req, res)));
119
+ app.get('/google/callback', wrapHandler((req, res) => oauth.googleCallback(req, res)));
120
+
115
121
 
116
122
  // --- API Routes ---
117
123
 
@@ -18,3 +18,8 @@ JWT_EXPIRES_IN=7d
18
18
  # Session
19
19
  SESSION_SECRET=change-me-to-random-session-secret
20
20
  SESSION_NAME=nara_session
21
+
22
+ # Google OAuth (optional)
23
+ GOOGLE_CLIENT_ID=
24
+ GOOGLE_CLIENT_SECRET=
25
+ GOOGLE_REDIRECT_URI=http://localhost:3000/google/callback
Binary file
@@ -3,6 +3,7 @@ import { AuthController } from '../app/controllers/AuthController.js';
3
3
  import { ProfileController } from '../app/controllers/ProfileController.js';
4
4
  import { UserController } from '../app/controllers/UserController.js';
5
5
  import { UploadController } from '../app/controllers/UploadController.js';
6
+ import { OAuthController } from '../app/controllers/OAuthController.js';
6
7
  import { authMiddleware, webAuthMiddleware, guestMiddleware } from '../app/middlewares/auth.js';
7
8
  import { wrapHandler } from '../app/utils/route-helper.js';
8
9
  import { db } from '../app/config/database.js';
@@ -17,6 +18,7 @@ export function registerRoutes(app: NaraApp) {
17
18
  const profile = new ProfileController();
18
19
  const users = new UserController();
19
20
  const upload = new UploadController();
21
+ const oauth = new OAuthController();
20
22
 
21
23
  // --- Page Routes (Inertia) ---
22
24
 
@@ -119,6 +121,10 @@ export function registerRoutes(app: NaraApp) {
119
121
  (res as InertiaResponse).inertia('profile');
120
122
  });
121
123
 
124
+ // --- Google OAuth Routes ---
125
+ app.get('/google/redirect', wrapHandler((req, res) => oauth.googleRedirect(req, res)));
126
+ app.get('/google/callback', wrapHandler((req, res) => oauth.googleCallback(req, res)));
127
+
122
128
  // Protected Form Actions (same path pattern - for Inertia form handling)
123
129
  app.post('/change-profile', webAuthMiddleware as any, wrapHandler((req, res) => profile.update(req, res)));
124
130
  app.post('/change-password', webAuthMiddleware as any, wrapHandler((req, res) => profile.changePassword(req, res)));