create-nara 1.0.37 → 1.0.40
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/AuthController.ts +123 -30
- package/templates/features/auth/app/controllers/OAuthController.ts +148 -0
- package/templates/features/auth/app/middlewares/rateLimit.ts +40 -8
- package/templates/features/auth/app/services/GoogleAuth.ts +33 -0
- package/templates/features/auth/app/services/LoginThrottle.ts +308 -0
- package/templates/features/db/app/models/Asset.ts +135 -0
- package/templates/features/db/migrations/20240101000004_create_assets.ts +20 -0
- package/templates/features/uploads/app/controllers/UploadController.ts +64 -11
- package/templates/svelte/app/config/constants.ts +251 -0
- package/templates/svelte/app/config/env.ts +110 -0
- package/templates/svelte/app/config/index.ts +8 -0
- package/templates/svelte/app/middlewares/csrf.ts +197 -0
- package/templates/svelte/app/middlewares/requestLogger.ts +200 -0
- package/templates/svelte/app/middlewares/securityHeaders.ts +291 -0
- package/templates/svelte/app/services/Logger.ts +161 -0
- package/templates/svelte/app/utils/route-helper.ts +2 -1
- package/templates/svelte/env.example +5 -0
- package/templates/svelte/routes/web.ts +8 -2
- package/templates/svelte/server.ts +6 -1
- package/templates/vue/env.example +5 -0
- package/templates/vue/routes/web.ts +8 -2
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
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
|
+
import LoginThrottle from '../services/LoginThrottle.js';
|
|
5
|
+
import Logger from '../services/Logger.js';
|
|
4
6
|
import bcrypt from 'bcrypt';
|
|
5
7
|
import jwt from 'jsonwebtoken';
|
|
8
|
+
import { AUTH, ERROR_MESSAGES } from '../config/index.js';
|
|
6
9
|
|
|
7
10
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
8
|
-
const JWT_EXPIRES_SECONDS =
|
|
11
|
+
const JWT_EXPIRES_SECONDS = AUTH.JWT_EXPIRY_SECONDS;
|
|
9
12
|
|
|
10
13
|
// Cookie options for auth token
|
|
11
14
|
const COOKIE_OPTIONS = {
|
|
@@ -18,25 +21,86 @@ const COOKIE_OPTIONS = {
|
|
|
18
21
|
export class AuthController extends BaseController {
|
|
19
22
|
async login(req: NaraRequest, res: NaraResponse) {
|
|
20
23
|
const { email, password } = await req.json();
|
|
24
|
+
const ip = req.ip || 'unknown';
|
|
21
25
|
|
|
22
26
|
if (!email || !password) {
|
|
23
|
-
|
|
24
|
-
res.cookie('error', 'Email and password are required', 5000);
|
|
27
|
+
res.cookie('error', 'Email and password are required', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
25
28
|
return res.redirect('/login');
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
// Check if locked out due to too many failed attempts
|
|
32
|
+
if (LoginThrottle.isLockedOut(email, ip)) {
|
|
33
|
+
const remainingMs = LoginThrottle.getRemainingLockoutTime(email, ip);
|
|
34
|
+
const remainingMinutes = Math.ceil(remainingMs / 60000);
|
|
35
|
+
|
|
36
|
+
Logger.logSecurity('Login blocked - account locked', {
|
|
37
|
+
email,
|
|
38
|
+
ip,
|
|
39
|
+
remainingMinutes,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
res.cookie('error', `Terlalu banyak percobaan login. Coba lagi dalam ${remainingMinutes} menit.`, AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
43
|
+
return res.redirect('/login');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Logger.info('Login attempt', {
|
|
47
|
+
email,
|
|
48
|
+
ip,
|
|
49
|
+
userAgent: req.headers['user-agent'],
|
|
50
|
+
});
|
|
51
|
+
|
|
28
52
|
// Find user by email
|
|
29
53
|
const user = await UserModel.findByEmail(email);
|
|
30
|
-
if (!user
|
|
31
|
-
|
|
54
|
+
if (!user) {
|
|
55
|
+
const throttleResult = LoginThrottle.recordFailedAttempt(email, ip);
|
|
56
|
+
|
|
57
|
+
Logger.logSecurity('Login failed - user not found', {
|
|
58
|
+
email,
|
|
59
|
+
ip,
|
|
60
|
+
remainingAttempts: throttleResult.remainingAttempts,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const errorMsg = throttleResult.isLocked
|
|
64
|
+
? `Terlalu banyak percobaan login. Coba lagi dalam ${Math.ceil(throttleResult.lockoutMs / 60000)} menit.`
|
|
65
|
+
: ERROR_MESSAGES.INVALID_CREDENTIALS;
|
|
66
|
+
|
|
67
|
+
res.cookie('error', errorMsg, AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
68
|
+
return res.redirect('/login');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const passwordMatch = await bcrypt.compare(password, user.password);
|
|
72
|
+
if (!passwordMatch) {
|
|
73
|
+
const throttleResult = LoginThrottle.recordFailedAttempt(email, ip);
|
|
74
|
+
|
|
75
|
+
Logger.logSecurity('Login failed - invalid password', {
|
|
76
|
+
userId: user.id,
|
|
77
|
+
email: user.email,
|
|
78
|
+
ip,
|
|
79
|
+
remainingAttempts: throttleResult.remainingAttempts,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const errorMsg = throttleResult.isLocked
|
|
83
|
+
? `Terlalu banyak percobaan login. Coba lagi dalam ${Math.ceil(throttleResult.lockoutMs / 60000)} menit.`
|
|
84
|
+
: ERROR_MESSAGES.INVALID_CREDENTIALS;
|
|
85
|
+
|
|
86
|
+
res.cookie('error', errorMsg, AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
32
87
|
return res.redirect('/login');
|
|
33
88
|
}
|
|
34
89
|
|
|
90
|
+
// Successful login - clear attempts
|
|
91
|
+
LoginThrottle.clearAttempts(email, ip);
|
|
92
|
+
|
|
93
|
+
Logger.logAuth('login_success', {
|
|
94
|
+
userId: user.id,
|
|
95
|
+
email: user.email,
|
|
96
|
+
ip,
|
|
97
|
+
});
|
|
98
|
+
|
|
35
99
|
// Generate JWT token with user info
|
|
36
100
|
const token = jwt.sign(
|
|
37
101
|
{ userId: user.id, email: user.email, name: user.name },
|
|
38
102
|
JWT_SECRET,
|
|
39
|
-
{ expiresIn:
|
|
103
|
+
{ expiresIn: AUTH.JWT_EXPIRY_SECONDS }
|
|
40
104
|
);
|
|
41
105
|
|
|
42
106
|
// Set auth cookie for web routes (maxAge in ms)
|
|
@@ -50,42 +114,64 @@ export class AuthController extends BaseController {
|
|
|
50
114
|
const { name, email, password } = await req.json();
|
|
51
115
|
|
|
52
116
|
if (!name || !email || !password) {
|
|
53
|
-
res.cookie('error', 'Name, email and password are required',
|
|
117
|
+
res.cookie('error', 'Name, email and password are required', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
54
118
|
return res.redirect('/register');
|
|
55
119
|
}
|
|
56
120
|
|
|
121
|
+
Logger.info('Registration attempt', {
|
|
122
|
+
email,
|
|
123
|
+
name,
|
|
124
|
+
ip: req.ip,
|
|
125
|
+
});
|
|
126
|
+
|
|
57
127
|
// Check if email already exists
|
|
58
128
|
const existing = await UserModel.findByEmail(email);
|
|
59
129
|
if (existing) {
|
|
60
|
-
|
|
130
|
+
Logger.logSecurity('Registration failed - duplicate email', {
|
|
131
|
+
email,
|
|
132
|
+
ip: req.ip,
|
|
133
|
+
});
|
|
134
|
+
res.cookie('error', ERROR_MESSAGES.EMAIL_EXISTS, AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
61
135
|
return res.redirect('/register');
|
|
62
136
|
}
|
|
63
137
|
|
|
64
138
|
// Hash password
|
|
65
|
-
const hashedPassword = await bcrypt.hash(password,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
const hashedPassword = await bcrypt.hash(password, AUTH.BCRYPT_SALT_ROUNDS);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Create user in database
|
|
143
|
+
const [userId] = await UserModel.create({ name, email, password: hashedPassword });
|
|
144
|
+
|
|
145
|
+
Logger.logAuth('registration_success', {
|
|
146
|
+
userId,
|
|
147
|
+
email,
|
|
148
|
+
ip: req.ip,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Generate JWT token
|
|
152
|
+
const token = jwt.sign(
|
|
153
|
+
{ userId, email, name },
|
|
154
|
+
JWT_SECRET,
|
|
155
|
+
{ expiresIn: AUTH.JWT_EXPIRY_SECONDS }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Set auth cookie for web routes (maxAge in ms)
|
|
159
|
+
res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
|
|
160
|
+
|
|
161
|
+
// Redirect to dashboard
|
|
162
|
+
return res.redirect('/dashboard');
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
Logger.error('Registration failed', error);
|
|
165
|
+
res.cookie('error', ERROR_MESSAGES.INTERNAL_ERROR, AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
166
|
+
return res.redirect('/register');
|
|
167
|
+
}
|
|
82
168
|
}
|
|
83
169
|
|
|
84
170
|
async me(req: NaraRequest, res: NaraResponse) {
|
|
85
171
|
const user = req.user;
|
|
86
172
|
|
|
87
173
|
if (!user) {
|
|
88
|
-
return jsonError(res,
|
|
174
|
+
return jsonError(res, ERROR_MESSAGES.UNAUTHORIZED, 401);
|
|
89
175
|
}
|
|
90
176
|
|
|
91
177
|
return jsonSuccess(res, { user });
|
|
@@ -95,6 +181,11 @@ export class AuthController extends BaseController {
|
|
|
95
181
|
// Clear auth cookie (set maxAge to 0)
|
|
96
182
|
res.cookie('auth_token', '', 0, COOKIE_OPTIONS);
|
|
97
183
|
|
|
184
|
+
Logger.logAuth('logout', {
|
|
185
|
+
userId: req.user?.id,
|
|
186
|
+
ip: req.ip,
|
|
187
|
+
});
|
|
188
|
+
|
|
98
189
|
// Redirect to login
|
|
99
190
|
return res.redirect('/login');
|
|
100
191
|
}
|
|
@@ -103,14 +194,14 @@ export class AuthController extends BaseController {
|
|
|
103
194
|
const { email } = await req.json();
|
|
104
195
|
|
|
105
196
|
if (!email) {
|
|
106
|
-
res.cookie('error', 'Email is required',
|
|
197
|
+
res.cookie('error', 'Email is required', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
107
198
|
return res.redirect('/forgot-password');
|
|
108
199
|
}
|
|
109
200
|
|
|
110
201
|
// TODO: Implement actual password reset email sending
|
|
111
202
|
|
|
112
203
|
// Set success message and redirect
|
|
113
|
-
res.cookie('success', 'If an account exists with this email, a reset link has been sent.',
|
|
204
|
+
res.cookie('success', 'If an account exists with this email, a reset link has been sent.', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
114
205
|
return res.redirect('/login');
|
|
115
206
|
}
|
|
116
207
|
|
|
@@ -118,13 +209,15 @@ export class AuthController extends BaseController {
|
|
|
118
209
|
const { token, password } = await req.json();
|
|
119
210
|
|
|
120
211
|
if (!token || !password) {
|
|
121
|
-
res.cookie('error', 'Reset token and password are required',
|
|
212
|
+
res.cookie('error', 'Reset token and password are required', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
122
213
|
return res.redirect('/forgot-password');
|
|
123
214
|
}
|
|
124
215
|
|
|
125
216
|
// TODO: Implement actual password reset
|
|
126
217
|
|
|
127
|
-
res.cookie('success', 'Password has been reset successfully.',
|
|
218
|
+
res.cookie('success', 'Password has been reset successfully.', AUTH.ERROR_COOKIE_EXPIRY_MS);
|
|
128
219
|
return res.redirect('/login');
|
|
129
220
|
}
|
|
130
221
|
}
|
|
222
|
+
|
|
223
|
+
export default new AuthController();
|
|
@@ -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
|
+
}
|
|
@@ -3,8 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* In-memory rate limiting middleware using sliding window algorithm.
|
|
5
5
|
* Tracks requests per key (IP, user, or custom) and returns 429 when limit exceeded.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Configurable max requests and window duration
|
|
9
|
+
* - Custom key generator (default: IP address)
|
|
10
|
+
* - Automatic cleanup of expired entries
|
|
11
|
+
* - Skip function for whitelisting certain requests
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Basic usage (100 requests per 15 minutes per IP)
|
|
15
|
+
* Route.use(rateLimit());
|
|
16
|
+
*
|
|
17
|
+
* // Custom configuration
|
|
18
|
+
* Route.use(rateLimit({
|
|
19
|
+
* maxRequests: 10,
|
|
20
|
+
* windowMs: 60 * 1000, // 1 minute
|
|
21
|
+
* keyGenerator: (req) => req.user?.id || req.ip,
|
|
22
|
+
* }));
|
|
23
|
+
*
|
|
24
|
+
* // Per-route rate limiting
|
|
25
|
+
* Route.post('/api/upload', rateLimit({ maxRequests: 5, windowMs: 60000 }), uploadHandler);
|
|
6
26
|
*/
|
|
7
27
|
|
|
28
|
+
import { RATE_LIMIT } from "../config/index.js";
|
|
29
|
+
import Logger from "../services/Logger.js";
|
|
8
30
|
import type { NaraRequest, NaraResponse, NaraMiddleware } from '@nara-web/core';
|
|
9
31
|
import { jsonError } from '@nara-web/core';
|
|
10
32
|
|
|
@@ -121,14 +143,17 @@ function getResetTime(key: string, windowMs: number): number {
|
|
|
121
143
|
|
|
122
144
|
/**
|
|
123
145
|
* Create rate limit middleware
|
|
146
|
+
*
|
|
147
|
+
* @param options - Rate limit configuration
|
|
148
|
+
* @returns Middleware function
|
|
124
149
|
*/
|
|
125
150
|
export function rateLimit(options: RateLimitOptions = {}): NaraMiddleware {
|
|
126
151
|
const {
|
|
127
|
-
maxRequests =
|
|
128
|
-
windowMs =
|
|
152
|
+
maxRequests = RATE_LIMIT.MAX_REQUESTS,
|
|
153
|
+
windowMs = RATE_LIMIT.WINDOW_MS,
|
|
129
154
|
keyGenerator = (req: NaraRequest) => req.ip || 'unknown',
|
|
130
155
|
skip,
|
|
131
|
-
message = '
|
|
156
|
+
message = 'Terlalu banyak permintaan, coba lagi nanti',
|
|
132
157
|
headers = true,
|
|
133
158
|
name = 'default',
|
|
134
159
|
} = options;
|
|
@@ -156,13 +181,14 @@ export function rateLimit(options: RateLimitOptions = {}): NaraMiddleware {
|
|
|
156
181
|
|
|
157
182
|
// Check if limit exceeded
|
|
158
183
|
if (currentCount >= maxRequests) {
|
|
159
|
-
|
|
184
|
+
Logger.logSecurity('Rate limit exceeded', {
|
|
160
185
|
key,
|
|
161
186
|
name,
|
|
162
187
|
currentCount,
|
|
163
188
|
maxRequests,
|
|
164
189
|
ip: req.ip,
|
|
165
190
|
path: req.path,
|
|
191
|
+
method: req.method,
|
|
166
192
|
});
|
|
167
193
|
|
|
168
194
|
if (headers) {
|
|
@@ -181,11 +207,14 @@ export function rateLimit(options: RateLimitOptions = {}): NaraMiddleware {
|
|
|
181
207
|
|
|
182
208
|
/**
|
|
183
209
|
* Create a strict rate limiter for sensitive endpoints
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* Route.post('/api/login', strictRateLimit(), AuthController.processLogin);
|
|
184
213
|
*/
|
|
185
214
|
export function strictRateLimit(options: RateLimitOptions = {}): NaraMiddleware {
|
|
186
215
|
return rateLimit({
|
|
187
|
-
maxRequests:
|
|
188
|
-
windowMs:
|
|
216
|
+
maxRequests: RATE_LIMIT.STRICT_MAX_REQUESTS,
|
|
217
|
+
windowMs: RATE_LIMIT.STRICT_WINDOW_MS,
|
|
189
218
|
name: 'strict',
|
|
190
219
|
...options,
|
|
191
220
|
});
|
|
@@ -193,11 +222,14 @@ export function strictRateLimit(options: RateLimitOptions = {}): NaraMiddleware
|
|
|
193
222
|
|
|
194
223
|
/**
|
|
195
224
|
* Create an API rate limiter
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* Route.use('/api', apiRateLimit());
|
|
196
228
|
*/
|
|
197
229
|
export function apiRateLimit(options: RateLimitOptions = {}): NaraMiddleware {
|
|
198
230
|
return rateLimit({
|
|
199
|
-
maxRequests:
|
|
200
|
-
windowMs:
|
|
231
|
+
maxRequests: RATE_LIMIT.API_MAX_REQUESTS,
|
|
232
|
+
windowMs: RATE_LIMIT.API_WINDOW_MS,
|
|
201
233
|
name: 'api',
|
|
202
234
|
...options,
|
|
203
235
|
});
|
|
@@ -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
|
+
}
|