propro-utils 1.4.72 → 1.4.74

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/README.md CHANGED
@@ -1,88 +1,89 @@
1
1
  # propro-utils
2
2
 
3
- `propro-utils` is a comprehensive Node.js middleware designed for handling both server-side and client-side authentication, notifcations and other utility functions. It's ideal for applications requiring robust authentication strategies and is highly customizable to fit various authentication needs.
3
+ `propro-utils` is a comprehensive Node.js middleware designed for handling authentication, authorization, and various utility functions for web applications. It provides a robust solution for both server-side and client-side authentication, profile management, and application settings.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Dual Authentication Modes**: Supports both server-side and client-side authentication.
7
+ - **Flexible Authentication**: Supports both server-side and client-side authentication.
8
+ - **Profile Management**: Includes routes for updating user profiles, passwords, emails, two-factor authentication, and avatars.
9
+ - **App Settings**: Provides a route for managing application-specific settings.
10
+ - **Token Refresh**: Handles token refreshing for maintaining user sessions.
11
+ - **Logout Functionality**: Implements secure user logout.
8
12
  - **Configurable**: Offers a wide range of options for customizing the authentication process.
9
- - **Lazy Loading**: Efficiently initializes authentication modules on-demand.
10
- - **Environment Validation**: Ensures critical environment variables are set.
11
- - **Error Management**: Provides robust error handling during the authentication process.
13
+ - **Error Handling**: Provides robust error management during the authentication process.
12
14
 
13
15
  ## Installation
14
16
 
15
- Install the middleware using npm or Yarn:
17
+ Install the middleware using `yarn`:
16
18
 
17
- ````bash
18
- npm install propro-utils
19
- # or
20
- yarn add propro-utils
19
+ ```bash
20
+ yarn add propro-utils
21
+ ```
21
22
 
22
23
  ## Usage
23
24
 
24
- After installing the middleware, you can import it in your project:
25
+ After installing the middleware, you can import and use it in your Express application:
25
26
 
26
27
  ```javascript
27
- const proproAuthMiddleware = require("propro-utils");
28
- ````
29
-
30
- Then, you can use the middleware in your server and client side code:
28
+ const express = require('express');
29
+ const AuthMiddleware = require('propro-utils');
30
+ const userSchema = require('./models/user');
31
31
 
32
- ```javascript
33
- const express = require("express");
34
32
  const app = express();
35
- const proproAuthMiddleware = require("propro-utils");
36
-
37
- // Initialize middleware with default parameters
38
- app.use(proproAuthMiddleware());
39
-
40
- // Or, initialize with custom settings
41
- // For example, to use only server authentication:
42
- app.use(proproAuthMiddleware(true, false));
43
-
44
- // And to use only client authentication:
45
- app.use(proproAuthMiddleware(false, true));
46
-
47
- // Add options
48
- app.use(
49
- proproAuthMiddleware({
50
- useServerAuth: true,
51
- serverOptions: {
52
- validateUser: async (userId) => {
53
- /* User validation logic */
54
- },
55
- secret = "RESTFULAPIs",
56
- authUrl = process.env.AUTH_URL,
57
- clientId = process.env.CLIENT_ID,
58
- clientSecret = process.env.CLIENT_SECRET,
59
- clientUrl = process.env.CLIENT_URL,
60
- redirectUri = process.env.REDIRECT_URI,
61
- onAuthFailRedirect: "/login",
62
- additionalChecks: async (req) => {
63
- /* Additional request checks */
64
- },
65
- },
66
- useClientAuth: false,
67
- clientOptions: {
68
- // Client-side authentication options
69
- },
70
- })
33
+
34
+ const authMiddleware = new AuthMiddleware(
35
+ {
36
+ authUrl: process.env.AUTH_URL,
37
+ clientId: process.env.CLIENT_ID,
38
+ clientSecret: process.env.CLIENT_SECRET,
39
+ clientUrl: process.env.CLIENT_URL,
40
+ redirectUri: process.env.REDIRECT_URI,
41
+ appName: process.env.APP_NAME,
42
+ appUrl: process.env.APP_URL,
43
+ },
44
+ userSchema
71
45
  );
46
+
47
+ app.use(authMiddleware.middleware());
48
+
49
+ // Your other routes and middleware
72
50
  ```
73
51
 
74
52
  ### Configuration Options
75
53
 
76
- - useServerAuth (boolean): Enable or disable server-side authentication.
77
- - serverOptions (object): Configuration options for server-side authentication.
78
- - useClientAuth (boolean): Enable or disable client-side authentication.
79
- - clientOptions (object): Configuration options for client-side authentication.
54
+ The `AuthMiddleware` constructor accepts an options object with the following properties:
80
55
 
81
- ## Contributing
56
+ - `secret`: The secret key used for authentication (default: 'RESTFULAPIs')
57
+ - `authUrl`: The authentication URL
58
+ - `clientId`: The client ID
59
+ - `clientSecret`: The client secret
60
+ - `clientUrl`: The client URL
61
+ - `redirectUri`: The redirect URI
62
+ - `appName`: The application name
63
+ - `appUrl`: The URL of the client application
82
64
 
83
- If you have suggestions for how propro-utils could be improved, or want to report a bug, open an issue! We'd love all and any contributions.
65
+ ## API Routes
66
+
67
+ The middleware sets up the following routes:
68
+
69
+ - `GET /api/auth`: Initiates the authentication process
70
+ - `GET /api/callback`: Handles the callback from the authentication server
71
+ - `POST /api/refreshToken`: Refreshes the authentication token
72
+ - `POST /api/logout`: Logs out the user
73
+ - `PATCH /api/profile`: Updates the user profile
74
+ - `PATCH /api/profile/password`: Updates the user's password
75
+ - `PATCH /api/profile/email`: Updates the user's email
76
+ - `PATCH /api/profile/2fa`: Manages two-factor authentication
77
+ - `PATCH /api/profile/avatar`: Updates the user's avatar
78
+ - `PATCH /api/app/settings`: Manages application settings
79
+
80
+ ## Error Handling
81
+
82
+ The middleware includes comprehensive error handling for various scenarios, including missing tokens, failed requests, and server errors.
83
+
84
+ ## Contributing
84
85
 
85
- For more, check out the [Contributing Guide](./CONTRIBUTING.md).
86
+ Contributions are welcome! Please feel free to submit a Pull Request.
86
87
 
87
88
  ## License
88
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "propro-utils",
3
- "version": "1.4.72",
3
+ "version": "1.4.74",
4
4
  "description": "Auth middleware for propro-auth",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,24 +1,45 @@
1
- import axios from 'axios';
1
+ // client/ClientAuth.js
2
2
 
3
- const setupClientMiddleware = () => {
4
- axios.interceptors.request.use(function (config) {
5
- const token = localStorage.getItem('token');
6
- if (token) {
7
- config.headers.Authorization = `Bearer ${token}`;
8
- }
9
- return config;
10
- }, function (error) {
11
- return Promise.reject(error);
12
- });
3
+ const axios = require('axios');
13
4
 
14
- axios.interceptors.response.use(function (response) {
15
- return response;
16
- }, function (error) {
17
- if (error.response.status === 401) {
18
- localStorage.removeItem('token');
19
- }
20
- return Promise.reject(error);
21
- });
22
- };
5
+ class ClientAuth {
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ }
23
9
 
24
- export default setupClientMiddleware;
10
+ middleware() {
11
+ return (req, res, next) => {
12
+ this.setupInterceptors();
13
+ next();
14
+ };
15
+ }
16
+
17
+ setupInterceptors() {
18
+ axios.interceptors.request.use(
19
+ function (config) {
20
+ const token = localStorage.getItem('token');
21
+ if (token) {
22
+ config.headers.Authorization = `Bearer ${token}`;
23
+ }
24
+ return config;
25
+ },
26
+ function (error) {
27
+ return Promise.reject(error);
28
+ }
29
+ );
30
+
31
+ axios.interceptors.response.use(
32
+ function (response) {
33
+ return response;
34
+ },
35
+ function (error) {
36
+ if (error.response && error.response.status === 401) {
37
+ localStorage.removeItem('token');
38
+ }
39
+ return Promise.reject(error);
40
+ }
41
+ );
42
+ }
43
+ }
44
+
45
+ module.exports = ClientAuth;
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  const {
2
2
  validateEnvironmentVariables,
3
3
  } = require('./server/middleware/validateEnv');
4
+ const ServerAuth = require('./server/index');
5
+ const ClientAuth = require('./client/ClientAuth');
6
+
4
7
  let _serverAuth, _clientAuth;
5
8
 
6
9
  /**
@@ -43,31 +46,66 @@ let _serverAuth, _clientAuth;
43
46
  * }, UserSchema));
44
47
  * ```
45
48
  */
46
- module.exports = function proproAuthMiddleware(options = {}, userSchema) {
47
- validateEnvironmentVariables([
48
- 'AUTH_URL',
49
- 'CLIENT_ID',
50
- 'CLIENT_SECRET',
51
- 'CLIENT_URL',
52
- 'REDIRECT_URI',
53
- 'APP_URL',
54
- ]);
55
- return (req, res, next) => {
56
- try {
57
- // Lazy loading and initializing server and client authentication modules with options
58
- if (options.useServerAuth) {
59
- _serverAuth =
60
- _serverAuth || require('./server')(options.serverOptions, userSchema);
61
- _serverAuth(req, res, next);
62
- } else if (options.useClientAuth) {
63
- _clientAuth = _clientAuth || require('./client')(options.clientOptions);
64
- _clientAuth(req, res, next);
65
- } else {
66
- next();
67
- }
68
- } catch (error) {
69
- console.error('Error in authentication middleware:', error);
70
- next(error);
49
+ class ProProAuthMiddleware {
50
+ constructor(options = {}, userSchema) {
51
+ this.options = options;
52
+ this.userSchema = userSchema;
53
+ this.serverAuth = null;
54
+ this.clientAuth = null;
55
+
56
+ this.validateConfig();
57
+ }
58
+
59
+ validateConfig() {
60
+ validateEnvironmentVariables([
61
+ 'AUTH_URL',
62
+ 'CLIENT_ID',
63
+ 'CLIENT_SECRET',
64
+ 'CLIENT_URL',
65
+ 'REDIRECT_URI',
66
+ 'APP_URL',
67
+ ]);
68
+
69
+ if (!this.options.useServerAuth && !this.options.useClientAuth) {
70
+ console.warn(
71
+ 'Neither server-side nor client-side authentication is enabled.'
72
+ );
73
+ }
74
+ }
75
+
76
+ initializeServerAuth() {
77
+ if (!this.serverAuth) {
78
+ this.serverAuth = new ServerAuth(
79
+ this.options.serverOptions,
80
+ this.userSchema
81
+ );
82
+ }
83
+ return this.serverAuth.middleware();
84
+ }
85
+
86
+ initializeClientAuth() {
87
+ if (!this.clientAuth) {
88
+ this.clientAuth = new ClientAuth(this.options.clientOptions);
71
89
  }
72
- };
73
- };
90
+ return this.clientAuth.middleware();
91
+ }
92
+
93
+ middleware() {
94
+ return (req, res, next) => {
95
+ try {
96
+ if (this.options.useServerAuth) {
97
+ return this.initializeServerAuth()(req, res, next);
98
+ } else if (this.options.useClientAuth) {
99
+ return this.initializeClientAuth()(req, res, next);
100
+ } else {
101
+ next();
102
+ }
103
+ } catch (error) {
104
+ console.error('Error in authentication middleware:', error);
105
+ next(error);
106
+ }
107
+ };
108
+ }
109
+ }
110
+
111
+ module.exports = ProProAuthMiddleware;
@@ -3,9 +3,9 @@ const {
3
3
  exchangeToken,
4
4
  formatRedirectUrl,
5
5
  } = require('./middleware/verifyToken');
6
- const { setAuthCookies } = require('./middleware/setAuthCookies');
6
+ const { setAuthCookies } = require('./middleware/cookieUtils');
7
7
  const { checkIfUserExists } = require('../../middlewares/account_info');
8
- const { post } = require('axios');
8
+ const axios = require('axios');
9
9
 
10
10
  /**
11
11
  * Middleware for handling authentication and authorization.
@@ -22,145 +22,238 @@ const { post } = require('axios');
22
22
  * @param {Schema} [userSchema] - The user schema to perform the operations on.
23
23
  * @returns {Function} - Express middleware function.
24
24
  */
25
- function proproAuthMiddleware(options = {}, userSchema) {
26
- const {
27
- secret = 'RESTFULAPIs',
28
- authUrl = process.env.AUTH_URL,
29
- clientId = process.env.CLIENT_ID,
30
- clientSecret = process.env.CLIENT_SECRET,
31
- clientUrl = process.env.CLIENT_URL,
32
- redirectUri = process.env.REDIRECT_URI,
33
- appName = process.env.APP_NAME,
34
- appUrl = process.env.APP_URL,
35
- } = options;
36
-
37
- let refreshToken;
38
-
39
- return async (req, res, next) => {
25
+ class AuthMiddleware {
26
+ constructor(options = {}, userSchema) {
27
+ this.options = {
28
+ secret: options.secret || 'RESTFULAPIs',
29
+ authUrl: options.authUrl || process.env.AUTH_URL,
30
+ clientId: options.clientId || process.env.CLIENT_ID,
31
+ clientSecret: options.clientSecret || process.env.CLIENT_SECRET,
32
+ clientUrl: options.clientUrl || process.env.CLIENT_URL,
33
+ redirectUri: options.redirectUri || process.env.REDIRECT_URI,
34
+ appName: options.appName || process.env.APP_NAME,
35
+ appUrl: options.appUrl || process.env.APP_URL,
36
+ };
37
+ this.userSchema = userSchema;
38
+ this.router = Router();
39
+ this.initializeRoutes();
40
+ }
41
+
42
+ initializeRoutes() {
43
+ this.router.get('/api/auth', this.handleAuth);
44
+ this.router.get('/api/callback', this.handleCallback);
45
+ this.router.post('/api/refreshToken', this.handleRefreshToken);
46
+ this.router.post('/api/logout', this.handleLogout);
47
+ this.router.patch('/api/profile', this.handleProfileUpdate);
48
+ this.router.patch('/api/profile/password', this.handlePasswordUpdate);
49
+ this.router.patch('/api/profile/email', this.handleEmailUpdate);
50
+ this.router.patch('api/profile/2fa', this.handleTwoFactorAuth);
51
+ this.router.patch('/api/profile/avatar', this.handleAvatarUpdate);
52
+ this.router.patch('api/app/settings', this.handleAppSettings);
53
+ }
54
+
55
+ handleAuth = (req, res) => {
56
+ const redirectUrl = this.constructRedirectUrl();
57
+ res.status(200).json({ redirectUrl });
58
+ };
59
+
60
+ handleCallback = async (req, res) => {
61
+ const { code } = req.query;
62
+ if (!code) {
63
+ return res.status(400).send('No code received');
64
+ }
65
+
40
66
  try {
41
- if (
42
- !['/api/auth', '/api/callback', '/api/refreshToken'].includes(req.path)
43
- ) {
44
- return next();
45
- }
67
+ const { tokens, account, redirectUrl } = await exchangeToken(
68
+ this.options.authUrl,
69
+ code,
70
+ this.options.clientId,
71
+ this.options.clientSecret,
72
+ this.options.redirectUri
73
+ );
46
74
 
47
- if (req.path === '/api/auth') {
48
- const redirectUrl = constructRedirectUrl(
49
- clientUrl,
50
- appName,
51
- clientId,
52
- redirectUri
53
- );
54
- return res.status(200).json({ redirectUrl });
55
- }
75
+ const user = await checkIfUserExists(this.userSchema, account.accountId);
76
+ setAuthCookies(res, tokens, account, user, redirectUrl);
56
77
 
57
- if (req.path === '/api/refreshToken') {
58
- if (req.cookies) {
59
- refreshToken = req.cookies['x-refresh-token'];
60
- }
61
- if (!refreshToken) {
62
- const redirectUrl = constructRedirectUrl(
63
- clientUrl,
64
- appName,
65
- clientId,
66
- redirectUri
67
- );
68
- return res
69
- .status(401)
70
- .json({ redirectUrl, error: 'No refresh token provided' });
71
- }
72
-
73
- const formatedAuthUrl = formatRedirectUrl(authUrl);
74
-
75
- let response;
76
-
77
- try {
78
- response = await post(
79
- `${formatedAuthUrl}/api/v1/auth/refreshTokens`,
80
- {
81
- refreshToken,
82
- },
83
- {
84
- params: {
85
- actionType: 'refresh',
86
- },
87
- }
88
- );
89
- } catch (error) {
90
- console.error('an error occur: ', error.message);
91
- }
92
-
93
- let { account, access, refresh } = response.data;
94
-
95
- if (!account || !access || !refresh) {
96
- return res
97
- .status(401)
98
- .json({ error: 'Invalid or expired refresh token' });
99
- }
100
-
101
- const user = await checkIfUserExists(userSchema, account.accountId);
102
-
103
- setAuthCookies(res, { access, refresh }, account, user, appUrl);
78
+ res.redirect(formatRedirectUrl(redirectUrl));
79
+ } catch (error) {
80
+ console.error('Error in callback:', error);
81
+ res.status(500).send('Internal Server Error');
82
+ }
83
+ };
84
+
85
+ handleRefreshToken = async (req, res) => {
86
+ const refreshToken = req.cookies['x-refresh-token'];
87
+ if (!refreshToken) {
88
+ return res.status(401).json({
89
+ redirectUrl: this.constructRedirectUrl(),
90
+ error: 'No refresh token provided',
91
+ });
92
+ }
93
+
94
+ try {
95
+ const response = await this.refreshTokens(refreshToken);
96
+ const { account, access, refresh } = response.data;
104
97
 
98
+ if (!account || !access || !refresh) {
105
99
  return res
106
- .status(200)
107
- .json({ message: 'Token refreshed successfully' });
100
+ .status(401)
101
+ .json({ error: 'Invalid or expired refresh token' });
108
102
  }
109
103
 
110
- if (req.path === '/api/callback') {
111
- const code = req.query.code;
112
- if (!code) {
113
- return res.status(400).send('No code received');
114
- }
104
+ const user = await checkIfUserExists(this.userSchema, account.accountId);
105
+ setAuthCookies(
106
+ res,
107
+ { access, refresh },
108
+ account,
109
+ user,
110
+ this.options.appUrl
111
+ );
115
112
 
116
- const { tokens, account, redirectUrl } = await exchangeToken(
117
- authUrl,
118
- code,
119
- clientId,
120
- clientSecret,
121
- redirectUri
122
- );
113
+ res.status(200).json({ message: 'Token refreshed successfully' });
114
+ } catch (error) {
115
+ console.error('Error refreshing token:', error);
116
+ res.status(401).json({ error: 'Failed to refresh token' });
117
+ }
118
+ };
123
119
 
124
- const user = await checkIfUserExists(userSchema, account.accountId);
120
+ handleLogout = async (req, res) => {
121
+ const refreshToken = req.cookies['x-refresh-token'];
122
+ if (!refreshToken) {
123
+ return res.status(401).json({ error: 'No refresh token provided' });
124
+ }
125
125
 
126
- setAuthCookies(res, tokens, account, user, redirectUrl);
126
+ try {
127
+ await this.logoutUser(refreshToken);
128
+ clearAuthCookies(res);
129
+ res.status(200).json({ redirectUrl: this.constructRedirectUrl() });
130
+ } catch (error) {
131
+ console.error('Error logging out:', error);
132
+ res.status(500).json({ error: 'Failed to logout' });
133
+ }
134
+ };
127
135
 
128
- const setCookies = res.getHeader('Set-Cookie');
129
- console.log('Set-Cookies: ', setCookies);
136
+ refreshTokens = async refreshToken => {
137
+ const formattedAuthUrl = formatRedirectUrl(this.options.authUrl);
138
+ return axios.post(
139
+ `${formattedAuthUrl}/api/v1/auth/refreshTokens`,
140
+ { refreshToken },
141
+ { params: { actionType: 'refresh' } }
142
+ );
143
+ };
130
144
 
131
- console.log('Redirect URL: ', redirectUrl);
145
+ logoutUser = async refreshToken => {
146
+ const formattedAuthUrl = formatRedirectUrl(this.options.authUrl);
147
+ return axios.post(
148
+ `${formattedAuthUrl}/api/v1/auth/logout`,
149
+ { refreshToken },
150
+ { params: { actionType: 'refresh' } }
151
+ );
152
+ };
132
153
 
133
- const urlToRedirect = formatRedirectUrl(redirectUrl);
154
+ constructRedirectUrl() {
155
+ const urlToRedirect = formatRedirectUrl(this.options.clientUrl);
156
+ return `${urlToRedirect}/signin?response_type=code&appName=${
157
+ this.options.appName
158
+ }&client_id=${this.options.clientId}&redirect_uri=${encodeURIComponent(
159
+ this.options.redirectUri
160
+ )}`;
161
+ }
134
162
 
135
- return res.redirect(urlToRedirect);
163
+ handleProfileUpdate = async (req, res) => {
164
+ try {
165
+ const response = await this.proxyToAuthServer(req, '/api/v1/profile');
166
+ res.status(response.status).json(response.data);
167
+ } catch (error) {
168
+ this.handleProxyError(error, res);
169
+ }
170
+ };
136
171
 
137
- // return res.status(200).json({
138
- // message: 'Token received successfully',
139
- // redirectUrl: urlToRedirect,
140
- // });
141
- }
172
+ handlePasswordUpdate = async (req, res) => {
173
+ try {
174
+ const response = await this.proxyToAuthServer(
175
+ req,
176
+ '/api/v1/profile/password'
177
+ );
178
+ res.status(response.status).json(response.data);
142
179
  } catch (error) {
143
- console.log('Unauthorized: ', error.message);
144
- res.status(401).send('Unauthorized: Invalid or expired token');
180
+ this.handleProxyError(error, res);
145
181
  }
146
182
  };
147
- }
148
183
 
149
- /**
150
- * Constructs the redirect URL for a client application.
151
- *
152
- * @param {string} clientUrl - The URL of the client application.
153
- * @param {string} appName - The name of the application.
154
- * @param {string} clientId - The client ID.
155
- * @param {string} redirectUri - The redirect URI.
156
- * @return {string} The constructed redirect URL.
157
- */
158
- function constructRedirectUrl(clientUrl, appName, clientId, redirectUri) {
159
- const urlToRedirect = formatRedirectUrl(clientUrl);
184
+ handleEmailUpdate = async (req, res) => {
185
+ try {
186
+ const response = await this.proxyToAuthServer(
187
+ req,
188
+ '/api/v1/profile/email'
189
+ );
190
+ res.status(response.status).json(response.data);
191
+ } catch (error) {
192
+ this.handleProxyError(error, res);
193
+ }
194
+ };
195
+
196
+ handleTwoFactorAuth = async (req, res) => {
197
+ try {
198
+ const response = await this.proxyToAuthServer(req, '/api/v1/profile/2fa');
199
+ res.status(response.status).json(response.data);
200
+ } catch (error) {
201
+ this.handleProxyError(error, res);
202
+ }
203
+ };
204
+
205
+ handleAvatarUpdate = async (req, res) => {
206
+ try {
207
+ const response = await this.proxyToAuthServer(
208
+ req,
209
+ '/api/v1/profile/avatar'
210
+ );
211
+ res.status(response.status).json(response.data);
212
+ } catch (error) {
213
+ this.handleProxyError(error, res);
214
+ }
215
+ };
216
+
217
+ proxyToAuthServer = async (req, path) => {
218
+ const accessToken = req.cookies['x-access-token'];
219
+ if (!accessToken) {
220
+ throw new Error('No access token provided');
221
+ }
222
+
223
+ const formattedAuthUrl = formatRedirectUrl(this.options.authUrl);
224
+ return axios({
225
+ method: req.method,
226
+ url: `${formattedAuthUrl}${path}`,
227
+ data: req.body,
228
+ headers: {
229
+ Authorization: `Bearer ${accessToken}`,
230
+ 'Content-Type': 'application/json',
231
+ },
232
+ });
233
+ };
234
+
235
+ handleProxyError = (error, res) => {
236
+ console.error('Error proxying request to auth server:', error);
237
+ if (error.response) {
238
+ res.status(error.response.status).json(error.response.data);
239
+ } else {
240
+ res.status(500).json({ error: 'Internal Server Error' });
241
+ }
242
+ };
243
+
244
+ handleAppSettings = async (req, res) => {
245
+ try {
246
+ const { settings } = req.body;
247
+ res.status(200).json({ message: 'App settings updated successfully' });
248
+ } catch (error) {
249
+ console.error('Error updating app settings:', error);
250
+ res.status(500).json({ error: 'Failed to update app settings' });
251
+ }
252
+ };
160
253
 
161
- return `${urlToRedirect}/signin?response_type=code&appName=${appName}&client_id=${clientId}&redirect_uri=${encodeURIComponent(
162
- redirectUri
163
- )}`;
254
+ middleware() {
255
+ return this.router;
256
+ }
164
257
  }
165
258
 
166
- module.exports = proproAuthMiddleware;
259
+ module.exports = AuthMiddleware;
@@ -1,88 +1,294 @@
1
- const proproAuthMiddleware = require('./index');
1
+ const request = require('supertest');
2
+ const express = require('express');
2
3
  const axios = require('axios');
3
- const { createMockExpressContext } = require('../../utils/testUtils');
4
+ const AuthMiddleware = require('./AuthMiddleware');
5
+ const {
6
+ setAuthCookies,
7
+ clearAuthCookies,
8
+ } = require('./middleware/cookieUtils');
9
+ const {
10
+ exchangeToken,
11
+ formatRedirectUrl,
12
+ } = require('./middleware/verifyToken');
13
+ const { checkIfUserExists } = require('../../middlewares/account_info');
4
14
 
5
15
  jest.mock('axios');
6
- jest.mock('dotenv', () => ({
7
- config: jest.fn(),
8
- }));
9
-
10
- process.env.AUTH_URL = 'https://auth.innate.io/api/auth';
11
- process.env.CLIENT_ID = 'testClientId';
12
- process.env.CLIENT_SECRET = 'testClientSecret';
13
- process.env.CLIENT_URL = 'https://example.com';
14
- process.env.REDIRECT_URI = 'https://example.com/callback';
15
- process.env.APP_NAME = 'TestApp';
16
-
17
- describe('proproAuthMiddleware /api/auth', () => {
18
- it('should return a redirect URL on /api/auth path', async () => {
19
- const { req, res, next } = createMockExpressContext('/api/auth');
20
-
21
- const middleware = proproAuthMiddleware({}, {});
22
- await middleware(req, res, next);
23
-
24
- expect(res.status).toHaveBeenCalledWith(200);
25
- expect(res.json).toHaveBeenCalledWith(
26
- expect.objectContaining({
27
- redirectUrl: expect.any(String),
28
- })
16
+ jest.mock('./middleware/cookieUtils');
17
+ jest.mock('./middleware/verifyToken');
18
+ jest.mock('../../middlewares/account_info');
19
+
20
+ describe('AuthMiddleware', () => {
21
+ let app;
22
+ let authMiddleware;
23
+ let mockUserSchema;
24
+
25
+ beforeEach(() => {
26
+ app = express();
27
+ mockUserSchema = {};
28
+ authMiddleware = new AuthMiddleware(
29
+ {
30
+ authUrl: 'http://auth.example.com',
31
+ clientId: 'test-client-id',
32
+ clientSecret: 'test-client-secret',
33
+ clientUrl: 'http://client.example.com',
34
+ redirectUri: 'http://client.example.com/callback',
35
+ appName: 'TestApp',
36
+ appUrl: 'http://app.example.com',
37
+ },
38
+ mockUserSchema
29
39
  );
40
+ app.use(express.json());
41
+ app.use(authMiddleware.middleware());
42
+ });
43
+
44
+ afterEach(() => {
45
+ jest.clearAllMocks();
46
+ });
47
+
48
+ describe('handleAuth', () => {
49
+ it('should return a redirect URL', async () => {
50
+ const response = await request(app).get('/api/auth');
51
+ expect(response.status).toBe(200);
52
+ expect(response.body).toHaveProperty('redirectUrl');
53
+ expect(response.body.redirectUrl).toContain(
54
+ 'http://client.example.com/signin'
55
+ );
56
+ });
57
+ });
58
+
59
+ describe('handleCallback', () => {
60
+ it('should handle successful token exchange', async () => {
61
+ const mockTokens = {
62
+ access: { token: 'access-token' },
63
+ refresh: { token: 'refresh-token' },
64
+ };
65
+ const mockAccount = { accountId: '123' };
66
+ const mockUser = { id: '456' };
67
+
68
+ exchangeToken.mockResolvedValue({
69
+ tokens: mockTokens,
70
+ account: mockAccount,
71
+ redirectUrl: 'http://app.example.com',
72
+ });
73
+ checkIfUserExists.mockResolvedValue(mockUser);
74
+
75
+ const response = await request(app).get('/api/callback?code=test-code');
76
+
77
+ expect(response.status).toBe(302); // Expecting a redirect
78
+ expect(response.header.location).toBe('http://app.example.com');
79
+ expect(setAuthCookies).toHaveBeenCalledWith(
80
+ expect.anything(),
81
+ mockTokens,
82
+ mockAccount,
83
+ mockUser,
84
+ 'http://app.example.com'
85
+ );
86
+ });
87
+
88
+ it('should handle missing code', async () => {
89
+ const response = await request(app).get('/api/callback');
90
+ expect(response.status).toBe(400);
91
+ expect(response.text).toBe('No code received');
92
+ });
93
+
94
+ it('should handle token exchange error', async () => {
95
+ exchangeToken.mockRejectedValue(new Error('Exchange failed'));
96
+
97
+ const response = await request(app).get('/api/callback?code=test-code');
98
+
99
+ expect(response.status).toBe(500);
100
+ expect(response.text).toBe('Internal Server Error');
101
+ });
102
+ });
103
+
104
+ describe('handleRefreshToken', () => {
105
+ it('should handle successful token refresh', async () => {
106
+ const mockRefreshToken = 'refresh-token';
107
+ const mockResponse = {
108
+ data: {
109
+ account: { accountId: '123' },
110
+ access: { token: 'new-access-token' },
111
+ refresh: { token: 'new-refresh-token' },
112
+ },
113
+ };
114
+
115
+ axios.post.mockResolvedValue(mockResponse);
116
+ checkIfUserExists.mockResolvedValue({ id: '456' });
117
+
118
+ const response = await request(app)
119
+ .post('/api/refreshToken')
120
+ .set('Cookie', [`x-refresh-token=${mockRefreshToken}`]);
121
+
122
+ expect(response.status).toBe(200);
123
+ expect(response.body).toEqual({
124
+ message: 'Token refreshed successfully',
125
+ });
126
+ expect(setAuthCookies).toHaveBeenCalled();
127
+ });
128
+
129
+ it('should handle missing refresh token', async () => {
130
+ const response = await request(app).post('/api/refreshToken');
131
+
132
+ expect(response.status).toBe(401);
133
+ expect(response.body).toHaveProperty(
134
+ 'error',
135
+ 'No refresh token provided'
136
+ );
137
+ });
138
+
139
+ it('should handle invalid refresh token', async () => {
140
+ axios.post.mockResolvedValue({ data: {} });
141
+
142
+ const response = await request(app)
143
+ .post('/api/refreshToken')
144
+ .set('Cookie', ['x-refresh-token=invalid-token']);
145
+
146
+ expect(response.status).toBe(401);
147
+ expect(response.body).toEqual({
148
+ error: 'Invalid or expired refresh token',
149
+ });
150
+ });
30
151
  });
31
- });
32
152
 
33
- // describe('proproAuthMiddleware /api/refreshToken', () => {
34
- // it('should refresh tokens and set cookies', async () => {
35
- // axios.post.mockResolvedValue({
36
- // data: {
37
- // account: { id: 'accountId' },
38
- // access: { token: 'accessToken', expires: new Date(Date.now() + 10000) },
39
- // refresh: {
40
- // token: 'refreshToken',
41
- // expires: new Date(Date.now() + 20000),
42
- // },
43
- // },
44
- // });
45
-
46
- // const { req, res, next } = createMockExpressContext('/api/refreshToken', {
47
- // cookies: { 'x-refresh-token': 'oldRefreshToken' },
48
- // });
49
-
50
- // const middleware = proproAuthMiddleware({}, {});
51
- // await middleware(req, res, next);
52
-
53
- // expect(res.cookie).toHaveBeenCalledWith(
54
- // 'x-refresh-token',
55
- // expect.any(String),
56
- // expect.objectContaining({
57
- // httpOnly: true,
58
- // secure: expect.any(Boolean),
59
- // })
60
- // );
61
- // expect(res.cookie).toHaveBeenCalledWith(
62
- // 'x-access-token',
63
- // expect.any(String),
64
- // expect.objectContaining({
65
- // httpOnly: true,
66
- // secure: expect.any(Boolean),
67
- // })
68
- // );
69
- // expect(res.status).toHaveBeenCalledWith(200);
70
- // expect(res.json).toHaveBeenCalledWith(
71
- // expect.objectContaining({
72
- // message: 'Token refreshed successfully',
73
- // })
74
- // );
75
- // });
76
- // });
77
-
78
- // describe('proproAuthMiddleware /api/callback', () => {
79
- // it('should handle the callback and set cookies', async () => {
80
- // const { req, res, next } = createMockExpressContext('/api/callback', {
81
- // query: { code: 'authorizationCode' },
82
- // });
83
-
84
- // const userSchemaMock = {};
85
- // const middleware = proproAuthMiddleware({}, userSchemaMock);
86
- // await middleware(req, res, next);
87
- // });
88
- // });
153
+ describe('handleLogout', () => {
154
+ it('should handle successful logout', async () => {
155
+ const mockRefreshToken = 'refresh-token';
156
+ axios.post.mockResolvedValue({});
157
+
158
+ const response = await request(app)
159
+ .post('/api/logout')
160
+ .set('Cookie', [`x-refresh-token=${mockRefreshToken}`]);
161
+
162
+ expect(response.status).toBe(200);
163
+ expect(response.body).toHaveProperty('redirectUrl');
164
+ expect(clearAuthCookies).toHaveBeenCalled();
165
+ });
166
+
167
+ it('should handle missing refresh token', async () => {
168
+ const response = await request(app).post('/api/logout');
169
+
170
+ expect(response.status).toBe(401);
171
+ expect(response.body).toEqual({ error: 'No refresh token provided' });
172
+ });
173
+
174
+ it('should handle logout error', async () => {
175
+ axios.post.mockRejectedValue(new Error('Logout failed'));
176
+
177
+ const response = await request(app)
178
+ .post('/api/logout')
179
+ .set('Cookie', ['x-refresh-token=test-token']);
180
+
181
+ expect(response.status).toBe(500);
182
+ expect(response.body).toEqual({ error: 'Failed to logout' });
183
+ });
184
+ });
185
+
186
+ describe('Profile Update Handlers', () => {
187
+ const testCases = [
188
+ {
189
+ method: 'handleProfileUpdate',
190
+ path: '/api/profile',
191
+ authPath: '/api/v1/profile',
192
+ },
193
+ {
194
+ method: 'handlePasswordUpdate',
195
+ path: '/api/profile/password',
196
+ authPath: '/api/v1/profile/password',
197
+ },
198
+ {
199
+ method: 'handleEmailUpdate',
200
+ path: '/api/profile/email',
201
+ authPath: '/api/v1/profile/email',
202
+ },
203
+ {
204
+ method: 'handleTwoFactorAuth',
205
+ path: '/api/profile/2fa',
206
+ authPath: '/api/v1/profile/2fa',
207
+ },
208
+ {
209
+ method: 'handleAvatarUpdate',
210
+ path: '/api/profile/avatar',
211
+ authPath: '/api/v1/profile/avatar',
212
+ },
213
+ ];
214
+
215
+ testCases.forEach(({ method, path, authPath }) => {
216
+ describe(method, () => {
217
+ it('should successfully proxy the request', async () => {
218
+ const mockResponse = {
219
+ status: 200,
220
+ data: { message: 'Update successful' },
221
+ };
222
+ axios.mockResolvedValue(mockResponse);
223
+
224
+ const response = await request(app)
225
+ .patch(path)
226
+ .set('Cookie', ['x-access-token=test-token'])
227
+ .send({ someData: 'testData' });
228
+
229
+ expect(response.status).toBe(200);
230
+ expect(response.body).toEqual({ message: 'Update successful' });
231
+ expect(axios).toHaveBeenCalledWith(
232
+ expect.objectContaining({
233
+ method: 'PATCH',
234
+ url: `http://auth.example.com${authPath}`,
235
+ data: { someData: 'testData' },
236
+ headers: expect.objectContaining({
237
+ Authorization: 'Bearer test-token',
238
+ 'Content-Type': 'application/json',
239
+ }),
240
+ })
241
+ );
242
+ });
243
+
244
+ it('should handle missing access token', async () => {
245
+ const response = await request(app).patch(path);
246
+
247
+ expect(response.status).toBe(500);
248
+ expect(response.body).toEqual({ error: 'Internal Server Error' });
249
+ });
250
+
251
+ it('should handle proxy error', async () => {
252
+ axios.mockRejectedValue({
253
+ response: { status: 400, data: { error: 'Bad Request' } },
254
+ });
255
+
256
+ const response = await request(app)
257
+ .patch(path)
258
+ .set('Cookie', ['x-access-token=test-token']);
259
+
260
+ expect(response.status).toBe(400);
261
+ expect(response.body).toEqual({ error: 'Bad Request' });
262
+ });
263
+ });
264
+ });
265
+ });
266
+
267
+ describe('handleAppSettings', () => {
268
+ it('should successfully update app settings', async () => {
269
+ const response = await request(app)
270
+ .patch('/api/app/settings')
271
+ .send({ settings: { theme: 'dark' } });
272
+
273
+ expect(response.status).toBe(200);
274
+ expect(response.body).toEqual({
275
+ message: 'App settings updated successfully',
276
+ });
277
+ });
278
+
279
+ it('should handle update error', async () => {
280
+ authMiddleware.handleAppSettings = jest
281
+ .fn()
282
+ .mockImplementation((req, res) => {
283
+ throw new Error('Update failed');
284
+ });
285
+
286
+ const response = await request(app)
287
+ .patch('/api/app/settings')
288
+ .send({ settings: { theme: 'light' } });
289
+
290
+ expect(response.status).toBe(500);
291
+ expect(response.body).toEqual({ error: 'Failed to update app settings' });
292
+ });
293
+ });
294
+ });
@@ -15,13 +15,17 @@ const setAuthCookies = (res, tokens, account, user, appUrl) => {
15
15
  const accessMaxAge =
16
16
  new Date(tokens.access.expires).getTime() - currentDateTime.getTime();
17
17
 
18
- const domain = appUrl ? new URL(appUrl).hostname : undefined;
18
+ let domain = appUrl ? new URL(appUrl).hostname : undefined;
19
+
20
+ if (domain.includes('mapmap.app')) {
21
+ domain = '.mapmap.app';
22
+ }
19
23
 
20
24
  console.log('domain: ', domain);
21
25
 
22
26
  const commonAttributes = {
23
27
  // secure: process.env.NODE_ENV === 'production',
24
- secure: false,
28
+ secure: true,
25
29
  sameSite: 'None',
26
30
  // path: '/',
27
31
  domain: domain,
@@ -59,6 +63,27 @@ const setAuthCookies = (res, tokens, account, user, appUrl) => {
59
63
  );
60
64
  };
61
65
 
66
+ const clearAuthCookies = res => {
67
+ const commonAttributes = {
68
+ secure: true,
69
+ sameSite: 'None',
70
+ domain: process.env.APP_URL
71
+ ? new URL(process.env.APP_URL).hostname
72
+ : undefined,
73
+ };
74
+
75
+ [
76
+ 'x-refresh-token',
77
+ 'x-access-token',
78
+ 'user',
79
+ 'account',
80
+ 'has_account_token',
81
+ ].forEach(cookieName => {
82
+ res.clearCookie(cookieName, commonAttributes);
83
+ });
84
+ };
85
+
62
86
  module.exports = {
63
87
  setAuthCookies,
88
+ clearAuthCookies,
64
89
  };
@@ -1,4 +1,4 @@
1
- const { setAuthCookies } = require('./setAuthCookies');
1
+ const { setAuthCookies } = require('./cookieUtils');
2
2
 
3
3
  describe('setAuthCookies', () => {
4
4
  let res;