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 +1 -1
- package/templates/features/auth/app/controllers/OAuthController.ts +148 -0
- package/templates/features/auth/app/services/GoogleAuth.ts +33 -0
- package/templates/svelte/env.example +5 -0
- package/templates/svelte/public/nara.png +0 -0
- package/templates/svelte/routes/web.ts +6 -0
- package/templates/vue/env.example +5 -0
- package/templates/vue/public/nara.png +0 -0
- package/templates/vue/routes/web.ts +6 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
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
|
|
|
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)));
|