@feardread/fear 1.1.5 → 1.1.7
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.
- package/FEAR.js +76 -63
- package/FEARServer.js +5 -6
- package/controllers/address.js +9 -0
- package/controllers/auth/index.js +499 -92
- package/controllers/order.js +0 -1
- package/controllers/payment.js +5 -185
- package/libs/db/index.js +5 -0
- package/libs/emailer/info.js +22 -34
- package/libs/emailer/smtp.js +511 -65
- package/libs/passport/index.js +137 -0
- package/libs/passport.js +22 -0
- package/libs/paypal/index.js +82 -0
- package/libs/stripe/index.js +306 -0
- package/libs/validator/index.js +2 -2
- package/models/address.js +37 -0
- package/models/order.js +29 -154
- package/models/payment.js +18 -79
- package/models/user.js +116 -51
- package/package.json +1 -1
- package/routes/address.js +16 -0
- package/routes/auth.js +6 -0
- package/routes/mail.js +10 -165
- package/routes/order.js +7 -4
- package/routes/payment.js +4 -8
- package/routes/paypal.js +12 -0
- package/routes/stripe.js +27 -0
- package/libs/passport/passport.js +0 -109
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const User = require("../../models/user");
|
|
2
2
|
const TokenService = require('./token');
|
|
3
|
-
const handler = require('../../libs/handler');
|
|
4
3
|
const validator = require('../../libs/validator');
|
|
5
4
|
const logger = require('../../libs/logger');
|
|
6
5
|
|
|
@@ -18,15 +17,8 @@ const response = {
|
|
|
18
17
|
* @param {string} message - Success message
|
|
19
18
|
*/
|
|
20
19
|
success: (res, user, token, statusCode = 200, message = "Authentication successful") => {
|
|
21
|
-
//
|
|
22
|
-
const userResponse =
|
|
23
|
-
_id: user._id,
|
|
24
|
-
email: user.email,
|
|
25
|
-
name: user.name,
|
|
26
|
-
role: user.role,
|
|
27
|
-
createdAt: user.createdAt,
|
|
28
|
-
updatedAt: user.updatedAt
|
|
29
|
-
};
|
|
20
|
+
// Use the model's toJSON method which automatically removes sensitive data
|
|
21
|
+
const userResponse = user.toJSON();
|
|
30
22
|
|
|
31
23
|
return res
|
|
32
24
|
.status(statusCode)
|
|
@@ -64,34 +56,72 @@ const response = {
|
|
|
64
56
|
* @description Validates user credentials (email/password) and returns JWT token for authenticated user
|
|
65
57
|
* @tags authentication
|
|
66
58
|
*/
|
|
67
|
-
exports.login =
|
|
59
|
+
exports.login = (req, res) => {
|
|
68
60
|
const { email, password } = req.body;
|
|
69
61
|
|
|
70
62
|
// Validate input
|
|
71
63
|
const validation = validator.input.login({ email, password });
|
|
72
64
|
if (!validation.isValid) {
|
|
73
|
-
return response.
|
|
65
|
+
return response.error(res, 400, validation.message);
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
logger.info('Authentication attempt for:', email);
|
|
69
|
+
|
|
70
|
+
// Find user and include password field
|
|
71
|
+
User.findOne({
|
|
72
|
+
email: email.toLowerCase(),
|
|
73
|
+
status: { $ne: 'deleted' }
|
|
74
|
+
})
|
|
75
|
+
.select('+password')
|
|
76
|
+
.then(user => {
|
|
77
|
+
if (!user) {
|
|
78
|
+
return response.error(res, 401, "Invalid credentials");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if account is locked
|
|
82
|
+
if (user.isLocked()) {
|
|
83
|
+
return response.error(
|
|
84
|
+
res,
|
|
85
|
+
423,
|
|
86
|
+
"Account temporarily locked due to too many failed login attempts. Please try again later."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if account is suspended or inactive
|
|
91
|
+
if (user.status === 'suspended') {
|
|
92
|
+
return response.error(res, 403, "Your account has been suspended. Please contact support.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (user.status === 'inactive') {
|
|
96
|
+
return response.error(res, 403, "Your account is inactive. Please contact support to reactivate.");
|
|
97
|
+
}
|
|
80
98
|
|
|
81
99
|
// Verify password
|
|
82
|
-
user.
|
|
83
|
-
.then(
|
|
100
|
+
return user.comparePassword(password)
|
|
101
|
+
.then(isPasswordValid => {
|
|
84
102
|
if (!isPasswordValid) {
|
|
85
|
-
return
|
|
103
|
+
return user.incLoginAttempts()
|
|
104
|
+
.then(() => {
|
|
105
|
+
return response.error(res, 401, "Invalid credentials");
|
|
106
|
+
});
|
|
86
107
|
}
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
// Reset login attempts and update last login
|
|
110
|
+
return user.resetLoginAttempts()
|
|
111
|
+
.then(() => {
|
|
112
|
+
return user.updateOne({
|
|
113
|
+
lastLoginAt: new Date(),
|
|
114
|
+
lastLoginIP: req.ip || req.connection.remoteAddress
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
.then(() => {
|
|
118
|
+
// Generate token
|
|
119
|
+
const token = TokenService.generateToken(user);
|
|
120
|
+
return response.success(res, user, token, 200, "Login successful");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
93
123
|
})
|
|
94
|
-
.catch(
|
|
124
|
+
.catch(error => {
|
|
95
125
|
logger.error('Login error:', error);
|
|
96
126
|
return response.error(res, 500, "Authentication failed", error.message);
|
|
97
127
|
});
|
|
@@ -103,50 +133,77 @@ exports.login = async (req, res) => {
|
|
|
103
133
|
* @description Creates a new user account with provided information and returns JWT token
|
|
104
134
|
* @tags authentication
|
|
105
135
|
*/
|
|
106
|
-
exports.register =
|
|
107
|
-
const {
|
|
136
|
+
exports.register = (req, res) => {
|
|
137
|
+
const {
|
|
138
|
+
email,
|
|
139
|
+
password,
|
|
140
|
+
firstName,
|
|
141
|
+
lastName,
|
|
142
|
+
displayName,
|
|
143
|
+
phoneNumber,
|
|
144
|
+
dateOfBirth,
|
|
145
|
+
...otherFields
|
|
146
|
+
} = req.body;
|
|
108
147
|
|
|
109
148
|
// Validate input
|
|
110
149
|
const validation = validator.input.register({
|
|
111
|
-
email,
|
|
150
|
+
email,
|
|
151
|
+
password,
|
|
152
|
+
firstName,
|
|
153
|
+
lastName
|
|
112
154
|
});
|
|
113
155
|
|
|
114
156
|
if (!validation.isValid) {
|
|
115
157
|
return response.error(res, 400, validation.message);
|
|
116
158
|
}
|
|
117
159
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
// Check if user already exists
|
|
161
|
+
User.findOne({ email: email.toLowerCase() })
|
|
162
|
+
.then(existingUser => {
|
|
163
|
+
if (existingUser) {
|
|
164
|
+
return response.error(res, 409, "User with this email already exists");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create new user with nested profile structure
|
|
168
|
+
const userData = {
|
|
169
|
+
email: email.toLowerCase(),
|
|
170
|
+
password,
|
|
171
|
+
firstName: firstName.trim(),
|
|
172
|
+
lastName: lastName.trim(),
|
|
173
|
+
displayName: displayName || `${firstName} ${lastName}`,
|
|
174
|
+
mobile: phoneNumber || undefined,
|
|
175
|
+
dateOfBirth: dateOfBirth || undefined,
|
|
176
|
+
role: otherFields.role || 'user',
|
|
177
|
+
status: 'active'
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return User.create(userData);
|
|
181
|
+
})
|
|
182
|
+
.then(user => {
|
|
183
|
+
if (!user) return; // Already handled in previous then
|
|
133
184
|
|
|
134
|
-
|
|
185
|
+
logger.info('New user registered:', user.email);
|
|
135
186
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
// Generate token and send response
|
|
188
|
+
const token = TokenService.generateToken(user);
|
|
189
|
+
return response.success(res, user, token, 201, "Registration successful");
|
|
190
|
+
})
|
|
191
|
+
.catch(error => {
|
|
192
|
+
logger.error('Registration error:', error);
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
194
|
+
// Handle duplicate key error (in case of race condition)
|
|
195
|
+
if (error.code === 11000) {
|
|
196
|
+
return response.error(res, 409, "User with this email already exists");
|
|
197
|
+
}
|
|
142
198
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
199
|
+
// Handle validation errors
|
|
200
|
+
if (error.name === 'ValidationError') {
|
|
201
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
202
|
+
return response.error(res, 400, messages.join(', '));
|
|
203
|
+
}
|
|
147
204
|
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
return response.error(res, 500, "Registration failed", error.message);
|
|
206
|
+
});
|
|
150
207
|
};
|
|
151
208
|
|
|
152
209
|
/**
|
|
@@ -175,18 +232,12 @@ exports.logout = async (req, res) => {
|
|
|
175
232
|
* @description Returns the current user's profile information
|
|
176
233
|
* @tags authentication
|
|
177
234
|
*/
|
|
178
|
-
exports.getCurrentUser =
|
|
235
|
+
exports.getCurrentUser = (req, res) => {
|
|
179
236
|
// req.user is set by isAuthorized middleware
|
|
180
237
|
const user = req.user;
|
|
181
238
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
email: user.email,
|
|
185
|
-
name: user.name,
|
|
186
|
-
role: user.role,
|
|
187
|
-
createdAt: user.createdAt,
|
|
188
|
-
updatedAt: user.updatedAt
|
|
189
|
-
};
|
|
239
|
+
// Use toJSON to automatically remove sensitive fields
|
|
240
|
+
const userResponse = user.toJSON();
|
|
190
241
|
|
|
191
242
|
return res.status(200).json({
|
|
192
243
|
success: true,
|
|
@@ -200,24 +251,116 @@ exports.getCurrentUser = async (req, res) => {
|
|
|
200
251
|
* @description Generates a new JWT token for authenticated user
|
|
201
252
|
* @tags authentication
|
|
202
253
|
*/
|
|
203
|
-
exports.refreshToken =
|
|
254
|
+
exports.refreshToken = (req, res) => {
|
|
204
255
|
const user = req.user; // Set by isAuthorized middleware
|
|
205
256
|
|
|
206
257
|
const newToken = TokenService.generateToken(user);
|
|
207
258
|
return response.success(res, user, newToken, 200, "Token refreshed successfully");
|
|
208
259
|
};
|
|
209
260
|
|
|
261
|
+
/**
|
|
262
|
+
* PUT /fear/api/auth/update-profile
|
|
263
|
+
* @summary Update user profile information
|
|
264
|
+
* @description Updates the authenticated user's profile data
|
|
265
|
+
* @tags authentication
|
|
266
|
+
*/
|
|
267
|
+
exports.updateProfile = (req, res) => {
|
|
268
|
+
const { firstName, lastName, displayName, phoneNumber, bio, dateOfBirth, avatar } = req.body;
|
|
269
|
+
|
|
270
|
+
User.findById(req.user._id)
|
|
271
|
+
.then(user => {
|
|
272
|
+
if (!user) {
|
|
273
|
+
return response.error(res, 404, "User not found");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update profile fields if provided
|
|
277
|
+
if (firstName) user.firstName = firstName.trim();
|
|
278
|
+
if (lastName) user.lastName = lastName.trim();
|
|
279
|
+
if (displayName !== undefined) user.displayName = displayName.trim();
|
|
280
|
+
if (phoneNumber !== undefined) user.phoneNumber = phoneNumber;
|
|
281
|
+
if (bio !== undefined) user.bio = bio;
|
|
282
|
+
if (dateOfBirth !== undefined) user.dateOfBirth = dateOfBirth;
|
|
283
|
+
if (avatar !== undefined) user.avatar = avatar;
|
|
284
|
+
|
|
285
|
+
return user.save();
|
|
286
|
+
})
|
|
287
|
+
.then(user => {
|
|
288
|
+
if (!user) return; // Already handled
|
|
289
|
+
|
|
290
|
+
return res.status(200).json({
|
|
291
|
+
success: true,
|
|
292
|
+
message: "Profile updated successfully",
|
|
293
|
+
data: { user: user.toJSON() }
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
.catch(error => {
|
|
297
|
+
logger.error('Profile update error:', error);
|
|
298
|
+
|
|
299
|
+
if (error.name === 'ValidationError') {
|
|
300
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
301
|
+
return response.error(res, 400, messages.join(', '));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return response.error(res, 500, "Profile update failed", error.message);
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* PUT /fear/api/auth/update-preferences
|
|
310
|
+
* @summary Update user preferences
|
|
311
|
+
* @description Updates the authenticated user's preferences (language, timezone, notifications, theme)
|
|
312
|
+
* @tags authentication
|
|
313
|
+
*/
|
|
314
|
+
exports.updatePreferences = (req, res) => {
|
|
315
|
+
const { language, timezone, notifications, theme } = req.body;
|
|
316
|
+
|
|
317
|
+
User.findById(req.user._id)
|
|
318
|
+
.then(user => {
|
|
319
|
+
if (!user) {
|
|
320
|
+
return response.error(res, 404, "User not found");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Update preferences
|
|
324
|
+
if (language) user.preferences.language = language;
|
|
325
|
+
if (timezone) user.preferences.timezone = timezone;
|
|
326
|
+
if (theme) user.preferences.theme = theme;
|
|
327
|
+
if (notifications) {
|
|
328
|
+
user.preferences.notifications = {
|
|
329
|
+
...user.preferences.notifications,
|
|
330
|
+
...notifications
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return user.save();
|
|
335
|
+
})
|
|
336
|
+
.then(user => {
|
|
337
|
+
if (!user) return; // Already handled
|
|
338
|
+
|
|
339
|
+
return res.status(200).json({
|
|
340
|
+
success: true,
|
|
341
|
+
message: "Preferences updated successfully",
|
|
342
|
+
data: {
|
|
343
|
+
preferences: user.preferences
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
})
|
|
347
|
+
.catch(error => {
|
|
348
|
+
logger.error('Preferences update error:', error);
|
|
349
|
+
return response.error(res, 500, "Preferences update failed", error.message);
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
|
|
210
353
|
/**
|
|
211
354
|
* Middleware: Verify JWT token and authenticate user
|
|
212
355
|
* @summary Checks if user has valid JWT token in cookies or Authorization header
|
|
213
356
|
* @description Validates JWT token and attaches user object to request
|
|
214
357
|
* @tags middleware, authentication
|
|
215
358
|
*/
|
|
216
|
-
exports.isAuthorized =
|
|
359
|
+
exports.isAuthorized = (req, res, next) => {
|
|
217
360
|
let token;
|
|
218
361
|
|
|
219
362
|
// Check for token in cookies first, then Authorization header
|
|
220
|
-
if (req.cookies?.jwt) {
|
|
363
|
+
if (req && req.cookies?.jwt) {
|
|
221
364
|
token = req.cookies.jwt;
|
|
222
365
|
} else if (req.headers.authorization?.startsWith('Bearer ')) {
|
|
223
366
|
token = req.headers.authorization.split(' ')[1];
|
|
@@ -232,23 +375,37 @@ exports.isAuthorized = async (req, res, next) => {
|
|
|
232
375
|
const decodedToken = TokenService.verifyToken(token);
|
|
233
376
|
|
|
234
377
|
// Get user from database
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
378
|
+
User.findById(decodedToken.id)
|
|
379
|
+
.then(user => {
|
|
380
|
+
if (!user) {
|
|
381
|
+
return response.error(res, 401, "Invalid token. User not found.");
|
|
382
|
+
}
|
|
239
383
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
384
|
+
// Check user status
|
|
385
|
+
if (user.status === 'deleted') {
|
|
386
|
+
return response.error(res, 401, "Account has been deleted.");
|
|
387
|
+
}
|
|
244
388
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
389
|
+
if (user.status === 'suspended') {
|
|
390
|
+
return response.error(res, 403, "Account has been suspended.");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (user.status === 'inactive') {
|
|
394
|
+
return response.error(res, 403, "Account is inactive.");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Attach user to request
|
|
398
|
+
req.user = user;
|
|
399
|
+
req.token = token;
|
|
400
|
+
next();
|
|
401
|
+
})
|
|
402
|
+
.catch(error => {
|
|
403
|
+
logger.error('Authorization error:', error);
|
|
404
|
+
return response.error(res, 500, "Authentication failed", error.message);
|
|
405
|
+
});
|
|
249
406
|
|
|
250
407
|
} catch (error) {
|
|
251
|
-
|
|
408
|
+
logger.error('Authorization error:', error);
|
|
252
409
|
|
|
253
410
|
if (error.name === 'JsonWebTokenError') {
|
|
254
411
|
return response.error(res, 401, "Invalid token.");
|
|
@@ -268,7 +425,7 @@ exports.isAuthorized = async (req, res, next) => {
|
|
|
268
425
|
* @description Checks if req.user.role equals 'admin'. Must be used after isAuthorized middleware
|
|
269
426
|
* @tags middleware, authorization
|
|
270
427
|
*/
|
|
271
|
-
exports.isAdmin =
|
|
428
|
+
exports.isAdmin = (req, res, next) => {
|
|
272
429
|
if (!req.user) {
|
|
273
430
|
return response.error(res, 401, "Authentication required");
|
|
274
431
|
}
|
|
@@ -289,7 +446,7 @@ exports.isAdmin = async (req, res, next) => {
|
|
|
289
446
|
* @returns {function} Express middleware function
|
|
290
447
|
*/
|
|
291
448
|
exports.authorizeRoles = (...roles) => {
|
|
292
|
-
return
|
|
449
|
+
return (req, res, next) => {
|
|
293
450
|
if (!req.user) {
|
|
294
451
|
return response.error(res, 401, "Authentication required");
|
|
295
452
|
}
|
|
@@ -303,7 +460,7 @@ exports.authorizeRoles = (...roles) => {
|
|
|
303
460
|
}
|
|
304
461
|
|
|
305
462
|
next();
|
|
306
|
-
}
|
|
463
|
+
};
|
|
307
464
|
};
|
|
308
465
|
|
|
309
466
|
/**
|
|
@@ -312,7 +469,7 @@ exports.authorizeRoles = (...roles) => {
|
|
|
312
469
|
* @description Useful for routes that behave differently for authenticated vs anonymous users
|
|
313
470
|
* @tags middleware, authentication
|
|
314
471
|
*/
|
|
315
|
-
exports.optionalAuth =
|
|
472
|
+
exports.optionalAuth = (req, res, next) => {
|
|
316
473
|
let token;
|
|
317
474
|
|
|
318
475
|
if (req.cookies?.jwt) {
|
|
@@ -327,19 +484,269 @@ exports.optionalAuth = async (req, res, next) => {
|
|
|
327
484
|
|
|
328
485
|
try {
|
|
329
486
|
const decodedToken = TokenService.verifyToken(token);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
487
|
+
|
|
488
|
+
User.findById(decodedToken.id)
|
|
489
|
+
.then(user => {
|
|
490
|
+
if (user && user.status === 'active') {
|
|
491
|
+
req.user = user;
|
|
492
|
+
req.token = token;
|
|
493
|
+
}
|
|
494
|
+
next();
|
|
495
|
+
})
|
|
496
|
+
.catch(error => {
|
|
497
|
+
// Silently fail for optional auth
|
|
498
|
+
logger.debug('Optional auth failed:', error.message);
|
|
499
|
+
next();
|
|
500
|
+
});
|
|
336
501
|
} catch (error) {
|
|
337
502
|
// Silently fail for optional auth
|
|
338
|
-
|
|
503
|
+
logger.debug('Optional auth failed:', error.message);
|
|
504
|
+
next();
|
|
339
505
|
}
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* POST /fear/api/auth/google
|
|
509
|
+
* @summary Authenticate or register user via Google OAuth
|
|
510
|
+
* @description Handles Google OAuth login/registration. Creates new user if doesn't exist, or logs in existing user
|
|
511
|
+
* @tags authentication, oauth
|
|
512
|
+
*/
|
|
513
|
+
exports.googleAuth = (req, res) => {
|
|
514
|
+
const { googleToken, googleId, email, firstName, lastName, avatar } = req.body;
|
|
340
515
|
|
|
341
|
-
|
|
516
|
+
// Validate required fields
|
|
517
|
+
if (!email || !googleId) {
|
|
518
|
+
return response.error(res, 400, "Google ID and email are required");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Validate email format (assuming validator has email method)
|
|
522
|
+
const emailValidation = validator.input.email ? validator.input.email(email) : { isValid: true };
|
|
523
|
+
if (!emailValidation.isValid) {
|
|
524
|
+
return response.error(res, 400, "Invalid email format");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
logger.info('Google authentication attempt for:', email);
|
|
528
|
+
|
|
529
|
+
// Find user by email or googleId
|
|
530
|
+
User.findOne({
|
|
531
|
+
$or: [
|
|
532
|
+
{ email: email.toLowerCase() },
|
|
533
|
+
{ 'oauth.google.id': googleId }
|
|
534
|
+
],
|
|
535
|
+
status: { $ne: 'deleted' }
|
|
536
|
+
})
|
|
537
|
+
.then(user => {
|
|
538
|
+
// If user exists - login flow
|
|
539
|
+
if (user) {
|
|
540
|
+
// Check account status
|
|
541
|
+
if (user.status === 'suspended') {
|
|
542
|
+
return response.error(res, 403, "Your account has been suspended. Please contact support.");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (user.status === 'inactive') {
|
|
546
|
+
return response.error(res, 403, "Your account is inactive. Please contact support to reactivate.");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Update Google OAuth info if not already set
|
|
550
|
+
if (!user.oauth || !user.oauth.google || !user.oauth.google.id) {
|
|
551
|
+
user.oauth = user.oauth || {};
|
|
552
|
+
user.oauth.google = {
|
|
553
|
+
id: googleId,
|
|
554
|
+
email: email.toLowerCase(),
|
|
555
|
+
connectedAt: new Date()
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Update last login info
|
|
560
|
+
user.lastLoginAt = new Date();
|
|
561
|
+
user.lastLoginIP = req.ip || req.connection.remoteAddress;
|
|
562
|
+
|
|
563
|
+
// Update avatar if provided and user doesn't have one
|
|
564
|
+
if (avatar && !user.avatar) {
|
|
565
|
+
user.avatar = avatar;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return user.save()
|
|
569
|
+
.then(savedUser => {
|
|
570
|
+
logger.info('Google login successful for:', savedUser.email);
|
|
571
|
+
|
|
572
|
+
// Generate token and send response
|
|
573
|
+
const token = TokenService.generateToken(savedUser);
|
|
574
|
+
return response.success(res, savedUser, token, 200, "Google login successful");
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// User doesn't exist - registration flow
|
|
579
|
+
const userData = {
|
|
580
|
+
email: email.toLowerCase(),
|
|
581
|
+
firstName: firstName?.trim() || 'User',
|
|
582
|
+
lastName: lastName?.trim() || '',
|
|
583
|
+
displayName: firstName && lastName ? `${firstName} ${lastName}`.trim() : email.split('@')[0],
|
|
584
|
+
avatar: avatar || undefined,
|
|
585
|
+
role: 'user',
|
|
586
|
+
status: 'active',
|
|
587
|
+
oauth: {
|
|
588
|
+
google: {
|
|
589
|
+
id: googleId,
|
|
590
|
+
email: email.toLowerCase(),
|
|
591
|
+
connectedAt: new Date()
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
isEmailVerified: true, // Google emails are already verified
|
|
595
|
+
lastLoginAt: new Date(),
|
|
596
|
+
lastLoginIP: req.ip || req.connection.remoteAddress
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Create new user (no password required for OAuth users)
|
|
600
|
+
return User.create(userData)
|
|
601
|
+
.then(newUser => {
|
|
602
|
+
logger.info('New user registered via Google:', newUser.email);
|
|
603
|
+
|
|
604
|
+
// Generate token and send response
|
|
605
|
+
const token = TokenService.generateToken(newUser);
|
|
606
|
+
return response.success(res, newUser, token, 201, "Google registration successful");
|
|
607
|
+
});
|
|
608
|
+
})
|
|
609
|
+
.catch(error => {
|
|
610
|
+
logger.error('Google authentication error:', error);
|
|
611
|
+
|
|
612
|
+
// Handle duplicate key error
|
|
613
|
+
if (error.code === 11000) {
|
|
614
|
+
return response.error(res, 409, "User with this email already exists");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Handle validation errors
|
|
618
|
+
if (error.name === 'ValidationError') {
|
|
619
|
+
const messages = Object.values(error.errors).map(err => err.message);
|
|
620
|
+
return response.error(res, 400, messages.join(', '));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return response.error(res, 500, "Google authentication failed", error.message);
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* POST /fear/api/auth/google/link
|
|
629
|
+
* @summary Link Google account to existing authenticated user
|
|
630
|
+
* @description Connects a Google OAuth account to the currently logged-in user
|
|
631
|
+
* @tags authentication, oauth
|
|
632
|
+
*/
|
|
633
|
+
exports.linkGoogleAccount = (req, res) => {
|
|
634
|
+
const { googleId, email } = req.body;
|
|
635
|
+
const userId = req.user._id; // Set by isAuthorized middleware
|
|
636
|
+
|
|
637
|
+
if (!googleId || !email) {
|
|
638
|
+
return response.error(res, 400, "Google ID and email are required");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if Google account is already linked to another user
|
|
642
|
+
User.findOne({
|
|
643
|
+
'oauth.google.id': googleId,
|
|
644
|
+
_id: { $ne: userId }
|
|
645
|
+
})
|
|
646
|
+
.then(existingUser => {
|
|
647
|
+
if (existingUser) {
|
|
648
|
+
return response.error(res, 409, "This Google account is already linked to another user");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Get current user
|
|
652
|
+
return User.findById(userId);
|
|
653
|
+
})
|
|
654
|
+
.then(user => {
|
|
655
|
+
if (!user) {
|
|
656
|
+
return response.error(res, 404, "User not found");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Update user with Google OAuth info
|
|
660
|
+
user.oauth = user.oauth || {};
|
|
661
|
+
user.oauth.google = {
|
|
662
|
+
id: googleId,
|
|
663
|
+
email: email.toLowerCase(),
|
|
664
|
+
connectedAt: new Date()
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Verify email if it matches
|
|
668
|
+
if (!user.isEmailVerified && email.toLowerCase() === user.email) {
|
|
669
|
+
user.isEmailVerified = true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return user.save();
|
|
673
|
+
})
|
|
674
|
+
.then(user => {
|
|
675
|
+
if (!user) return; // Already handled
|
|
676
|
+
|
|
677
|
+
logger.info('Google account linked for user:', user.email);
|
|
678
|
+
|
|
679
|
+
return res.status(200).json({
|
|
680
|
+
success: true,
|
|
681
|
+
message: "Google account linked successfully",
|
|
682
|
+
data: { user: user.toJSON() }
|
|
683
|
+
});
|
|
684
|
+
})
|
|
685
|
+
.catch(error => {
|
|
686
|
+
logger.error('Google account linking error:', error);
|
|
687
|
+
return response.error(res, 500, "Failed to link Google account", error.message);
|
|
688
|
+
});
|
|
342
689
|
};
|
|
343
690
|
|
|
691
|
+
/**
|
|
692
|
+
* DELETE /fear/api/auth/google/unlink
|
|
693
|
+
* @summary Unlink Google account from authenticated user
|
|
694
|
+
* @description Removes Google OAuth connection from the user's account
|
|
695
|
+
* @tags authentication, oauth
|
|
696
|
+
*/
|
|
697
|
+
exports.unlinkGoogleAccount = (req, res) => {
|
|
698
|
+
const userId = req.user._id;
|
|
699
|
+
|
|
700
|
+
User.findById(userId)
|
|
701
|
+
.select('+password')
|
|
702
|
+
.then(user => {
|
|
703
|
+
if (!user) {
|
|
704
|
+
return response.error(res, 404, "User not found");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Check if user has a password set (don't allow unlinking if it's their only auth method)
|
|
708
|
+
if (!user.password && user.oauth?.google) {
|
|
709
|
+
return response.error(
|
|
710
|
+
res,
|
|
711
|
+
400,
|
|
712
|
+
"Cannot unlink Google account. Please set a password first to maintain access to your account."
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Check if Google account is linked
|
|
717
|
+
if (!user.oauth || !user.oauth.google) {
|
|
718
|
+
return response.error(res, 400, "No Google account is linked to this user");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Remove Google OAuth info
|
|
722
|
+
user.oauth.google = undefined;
|
|
723
|
+
|
|
724
|
+
return user.save();
|
|
725
|
+
})
|
|
726
|
+
.then(user => {
|
|
727
|
+
if (!user) return; // Already handled
|
|
728
|
+
|
|
729
|
+
logger.info('Google account unlinked for user:', user.email);
|
|
730
|
+
|
|
731
|
+
return res.status(200).json({
|
|
732
|
+
success: true,
|
|
733
|
+
message: "Google account unlinked successfully",
|
|
734
|
+
data: { user: user.toJSON() }
|
|
735
|
+
});
|
|
736
|
+
})
|
|
737
|
+
.catch(error => {
|
|
738
|
+
logger.error('Google account unlinking error:', error);
|
|
739
|
+
return response.error(res, 500, "Failed to unlink Google account", error.message);
|
|
740
|
+
});
|
|
741
|
+
};
|
|
344
742
|
// Export TokenService and other utilities for use in other modules
|
|
345
743
|
exports.AuthResponse = response;
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
module.exports = {
|
|
747
|
+
login: exports.login,
|
|
748
|
+
register: exports.register,
|
|
749
|
+
logout: exports.logout,
|
|
750
|
+
isAuthorized: exports.isAuthorized,
|
|
751
|
+
googleAuth: exports.googleAuth,
|
|
752
|
+
}
|