@feardread/fear 1.1.6 → 1.1.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.
package/FEAR.js CHANGED
@@ -1,10 +1,13 @@
1
1
  const path = require("path");
2
2
  const fs = require("fs");
3
3
  const express = require("express");
4
+ const session = require('express-session');
4
5
  const compression = require("compression");
5
6
  const cookieParser = require("cookie-parser");
6
7
  const fileUpload = require("express-fileupload");
7
8
  const cors = require("cors");
9
+ const MongoStore = require('connect-mongo');
10
+ const passport = require('passport');
8
11
 
9
12
  module.exports = FEAR = (() => {
10
13
  // Private constants
@@ -14,7 +17,7 @@ module.exports = FEAR = (() => {
14
17
  const AGENT_ROUTE_PATH = '/fear/api/agent';
15
18
 
16
19
  // Constructor function
17
- const FEAR = function() {
20
+ const FEAR = function (config) {
18
21
  this.app = express();
19
22
  this.Router = express.Router;
20
23
  this.server = null;
@@ -29,100 +32,67 @@ module.exports = FEAR = (() => {
29
32
  this.logo = null;
30
33
  this.origins = [];
31
34
  this.corsConfig = null;
35
+ this.stripe = null;
36
+ this.paypal = null;
37
+ this.passport = null;
38
+ this.mailer = null;
39
+
32
40
 
33
- // Initialize
34
41
  this.setupEnvironment();
35
42
  this.setupDependencies();
43
+ if (this.env.ADD_PAYMENTS) this.setupProcessors();
44
+ this.setupMailer();
36
45
  this.setupMiddleware();
37
46
  this.corsConfig = this.getCorsConfig();
38
47
  this.setupRoutes();
39
48
  };
40
49
 
41
- // Consolidated prototype
50
+
42
51
  FEAR.prototype = {
43
52
  constructor: FEAR,
44
-
45
- /**
46
- * Get AI Agent service instance
47
- */
48
- getAiAgent() {
49
- return this.agentService;
53
+ getStripe() {
54
+ return this.stripe;
55
+ },
56
+ getPassport() {
57
+ return this.passport;
50
58
  },
51
-
52
- /**
53
- * Clear global FearRouter
54
- */
55
59
  clearGlobal() {
56
60
  delete global.FearRouter;
57
61
  return this;
58
62
  },
59
-
60
- /**
61
- * Get Express app instance
62
- */
63
63
  getApp() {
64
64
  return this.app;
65
65
  },
66
-
67
- /**
68
- * Get logger instance
69
- */
70
66
  getLogger() {
71
67
  return this.logger;
72
68
  },
73
-
74
- /**
75
- * Get database instance
76
- */
77
69
  getDatabase() {
78
70
  return this.db;
79
71
  },
80
-
81
- /**
82
- * Get environment configuration
83
- */
84
72
  getEnvironment() {
85
73
  return this.env;
86
74
  },
87
-
88
- /**
89
- * Get cloud service instance
90
- */
91
75
  getCloud() {
92
76
  return this.cloud;
93
77
  },
94
-
95
- /**
96
- * Get Express Router
97
- */
98
78
  getRouter() {
99
79
  return this.Router;
100
80
  },
101
-
102
- /**
103
- * Get validator instance
104
- */
105
81
  getValidator() {
106
82
  return this.validator;
107
83
  },
108
-
109
- /**
110
- * Get handler instance
111
- */
112
84
  getHandler() {
113
85
  return this.handler;
114
86
  },
115
-
116
- /**
117
- * Get CORS configuration
118
- */
87
+ getPaypal() {
88
+ return this.paypal;
89
+ },
90
+ getMailer() {
91
+ return this.mailer;
92
+ },
119
93
  getCorsConfigValue() {
120
94
  return this.corsConfig;
121
95
  },
122
-
123
- /**
124
- * Setup environment variables from .env file
125
- */
126
96
  setupEnvironment() {
127
97
  const envResult = require("dotenv").config({ path: ".env" });
128
98
 
@@ -138,6 +108,7 @@ module.exports = FEAR = (() => {
138
108
  */
139
109
  setupDependencies() {
140
110
  this.logger = require("./libs/logger");
111
+ this.passport = require('./libs/passport');
141
112
  this.morgan = require("./libs/logger/morgan");
142
113
  this.cloud = require("./libs/cloud");
143
114
  this.db = require("./libs/db");
@@ -147,26 +118,66 @@ module.exports = FEAR = (() => {
147
118
  this.origins = this.getAllowedOrigins();
148
119
  },
149
120
 
121
+ setupProcessors() {
122
+ const StripeHandler = require("./libs/stripe");
123
+ const PayPal = require('./libs/paypal');
124
+
125
+ if ( !this.env.PAYPAL_CLIENT_ID|| !this.env.STRIPE_API_KEY ) {
126
+ this.logger.warn('Missing environment values in .env file')
127
+ }
128
+
129
+ this.stripe = new StripeHandler(this);
130
+ this.paypal = new PayPal(this);
131
+ },
132
+
150
133
  /**
151
134
  * Setup Express middleware
152
135
  */
153
136
  setupMiddleware() {
154
137
  this.app.set("PORT", this.env.NODE_PORT || DEFAULT_PORT);
155
-
156
- this.app.use(this.morgan);
157
138
  this.app.use(express.json({ limit: DEFAULT_JSON_LIMIT }));
139
+ this.app.use(express.urlencoded({ extended: true }));
140
+ this.app.use(session({
141
+ secret: process.env.SESSION_SECRET,
142
+ resave: false,
143
+ saveUninitialized: false,
144
+ //store: MongoStore.create({
145
+ //mongoUrl: process.env.DB_LINK,
146
+ //touchAfter: 24 * 3600 // lazy session update (24 hours)
147
+ //}),
148
+ cookie: {
149
+ maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week
150
+ httpOnly: true,
151
+ secure: process.env.NODE_ENV === 'production' // HTTPS only in production
152
+ }
153
+ }));
154
+ this.app.use(this.morgan);
158
155
  this.app.use(compression());
159
156
  this.app.use(fileUpload());
160
157
  this.app.use(cookieParser());
158
+ this.app.use(passport.initialize());
159
+ this.app.use(passport.session());
161
160
 
162
- // Request logging middleware
161
+ require('./libs/passport');
163
162
  this.app.use((req, res, next) => {
164
163
  this.logger.info(`FEAR API Query :: ${req.url}`);
165
- res.locals.user = req.user;
164
+ res.locals.user = req.user || null;
166
165
  next();
167
166
  });
168
167
  },
169
168
 
169
+ setupMailer() {
170
+ const mailService = (this.env.NODE_ENV === 'production') ? 'mailgun' : 'google';
171
+ const mailinfo = require('./libs/emailer/info');
172
+ const smtp = require('./libs/emailer/smtp')
173
+
174
+ mailinfo.service = mailService;
175
+ this.mailinfo = mailinfo;
176
+
177
+ if ( !this.mailer ) {
178
+ this.mailer = new smtp(this);
179
+ }
180
+ },
170
181
  /**
171
182
  * Parse allowed origins from environment
172
183
  */
@@ -287,6 +298,8 @@ module.exports = FEAR = (() => {
287
298
  router.getHandler = () => this.handler;
288
299
  router.getValidator = () => this.validator;
289
300
  router.getAiAgent = () => this.agentService;
301
+ router.getStripe = () => this.stripe;
302
+ router.getPaypal = () => this.paypal;
290
303
  //router.getAgentWebInterface = () => this.agentWebInterface;
291
304
 
292
305
  return router;
@@ -314,14 +327,14 @@ module.exports = FEAR = (() => {
314
327
  this.logger.error(`Failed to start server on port ${serverPort}:`, err);
315
328
  return reject(err);
316
329
  }
317
-
330
+
318
331
  this.logger.info(`FEAR server started on port ${serverPort}`);
319
-
332
+
320
333
  // Log agent interface status
321
334
  if (this.agentWebInterface) {
322
335
  this.logger.info(`Agent Web Interface available at ${AGENT_ROUTE_PATH}`);
323
336
  }
324
-
337
+
325
338
  resolve(this.server);
326
339
  });
327
340
  });
@@ -347,7 +360,7 @@ module.exports = FEAR = (() => {
347
360
  }
348
361
 
349
362
  // Shutdown agent web interface
350
- const agentShutdown = this.agentWebInterface &&
363
+ const agentShutdown = this.agentWebInterface &&
351
364
  typeof this.agentWebInterface.shutdown === 'function' ?
352
365
  this.agentWebInterface.shutdown() :
353
366
  Promise.resolve();
@@ -397,8 +410,8 @@ module.exports = FEAR = (() => {
397
410
  getDatabase: () => this.db,
398
411
  getEnvironment: () => this.env,
399
412
  getCloud: () => this.cloud,
400
- getAiAgent: () => this.agentService,
401
- getAgentWebInterface: () => this.agentWebInterface
413
+ getStripe: () => this.stripe,
414
+ initProcessors: this.setupProcessors
402
415
  };
403
416
  return this;
404
417
  }
package/FEARServer.js CHANGED
@@ -8,7 +8,7 @@ const FearServer = (function () {
8
8
  // Private constants
9
9
  const DEFAULT_PATHS = {
10
10
  root: path.resolve(),
11
- app: '/backend/dashboard/build',
11
+ app: '/backend/dashboard/build',
12
12
  build: 'backend/dashboard/build'
13
13
  };
14
14
 
@@ -184,11 +184,11 @@ const FearServer = (function () {
184
184
  /**
185
185
  * Initialize FEAR application
186
186
  */
187
- initialize(paths = DEFAULT_PATHS) {
187
+ initialize(paths = DEFAULT_PATHS, ADD_PAYMENTS = false) {
188
188
  try {
189
189
  // Import FEAR after dotenv is configured
190
190
  const FearFactory = require("./FEAR");
191
- this.fear = new FearFactory();
191
+ this.fear = new FearFactory({ADD_PAYMENTS});
192
192
  this.Router = this.fear.Router;
193
193
 
194
194
  this.setupStaticFiles(paths.root, paths.app, paths.build, paths.basePath);
@@ -212,7 +212,6 @@ const FearServer = (function () {
212
212
  if (this.fear.logo) {
213
213
  logger.warn(this.fear.logo);
214
214
  }
215
-
216
215
  // Initialize database connection
217
216
  return this.initializeDatabase()
218
217
  .then(() => {
@@ -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
 
@@ -361,7 +360,7 @@ exports.isAuthorized = (req, res, next) => {
361
360
  let token;
362
361
 
363
362
  // Check for token in cookies first, then Authorization header
364
- if (req.cookies?.jwt) {
363
+ if (req && req.cookies?.jwt) {
365
364
  token = req.cookies.jwt;
366
365
  } else if (req.headers.authorization?.startsWith('Bearer ')) {
367
366
  token = req.headers.authorization.split(' ')[1];
@@ -505,12 +504,248 @@ exports.optionalAuth = (req, res, next) => {
505
504
  next();
506
505
  }
507
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;
515
+
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
+ }
508
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
+ });
689
+ };
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
+ };
509
742
  // Export TokenService and other utilities for use in other modules
510
743
  exports.AuthResponse = response;
511
744
 
512
745
  module.exports = {
513
746
  login: exports.login,
514
747
  register: exports.register,
515
- logout: exports.logout
748
+ logout: exports.logout,
749
+ isAuthorized: exports.isAuthorized,
750
+ googleAuth: exports.googleAuth,
516
751
  }