propro-utils 1.3.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/.github/workflows/.deploy +31 -0
- package/.idea/modules.xml +8 -0
- package/.idea/propro-auth-middleware.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +89 -0
- package/dist/NotificationService.js +64 -0
- package/dist/baseStrategy.js +44 -0
- package/dist/emailStrategy.js +79 -0
- package/dist/index.js +16 -0
- package/dist/mailjetStrategy.js +71 -0
- package/dist/template.data.js +298 -0
- package/dist/testemail.js +12 -0
- package/dist/twilloStrategy.js +69 -0
- package/notify/NotifLocal.js +81 -0
- package/notify/NotifLocal.jsx +89 -0
- package/notify/NotificationService.js +31 -0
- package/notify/README.md +110 -0
- package/notify/baseStrategy.js +13 -0
- package/notify/emailStrategy.js +57 -0
- package/notify/index.js +11 -0
- package/notify/mailjetStrategy.js +27 -0
- package/notify/template.data.js +292 -0
- package/notify/testemail.js +15 -0
- package/notify/twilloStrategy.js +30 -0
- package/package.json +69 -0
- package/src/.env.example +5 -0
- package/src/client/index.js +24 -0
- package/src/index.js +68 -0
- package/src/server/index.js +108 -0
- package/src/server/middleware/index.js +3 -0
- package/src/server/middleware/oauth.test.js +203 -0
- package/src/server/middleware/refreshToken.js +82 -0
- package/src/server/middleware/refreshToken.test.js +121 -0
- package/src/server/middleware/validateEnv.js +12 -0
- package/src/server/middleware/verifyToken.js +119 -0
- package/src/server/middleware/verifyToken.test.js +171 -0
- package/src/server/server.test.js +37 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const jwt = require("jsonwebtoken");
|
|
3
|
+
const tokenCache = new Map();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verifies a JSON Web Token (JWT) using the provided secret.
|
|
7
|
+
* @param {string} token - The JWT to be verified.
|
|
8
|
+
* @param {string} secret - The secret used to sign the JWT.
|
|
9
|
+
* @returns {object|null} - The decoded payload of the JWT if it is valid, or null if it is not valid.
|
|
10
|
+
*/
|
|
11
|
+
function verifyJWT(token, secret) {
|
|
12
|
+
try {
|
|
13
|
+
return jwt.verify(token, secret);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Exchanges an authorization code for an access token.
|
|
21
|
+
* @param {string} authUrl - The URL of the authorization server.
|
|
22
|
+
* @param {string} code - The authorization code.
|
|
23
|
+
* @param {string} clientId - The client ID.
|
|
24
|
+
* @param {string} clientSecret - The client secret.
|
|
25
|
+
* @param {string} redirectUri - The redirect URI.
|
|
26
|
+
* @returns {Promise<Object>} - The response data containing the access token.
|
|
27
|
+
*/
|
|
28
|
+
async function exchangeToken(
|
|
29
|
+
authUrl,
|
|
30
|
+
code,
|
|
31
|
+
clientId,
|
|
32
|
+
clientSecret,
|
|
33
|
+
redirectUri
|
|
34
|
+
) {
|
|
35
|
+
const response = await axios.post(
|
|
36
|
+
authUrl + "/api/v1/auth/authorize",
|
|
37
|
+
{
|
|
38
|
+
grantType: "authorization_code",
|
|
39
|
+
code: code,
|
|
40
|
+
redirectUri: redirectUri,
|
|
41
|
+
clientId: clientId,
|
|
42
|
+
clientSecret: clientSecret,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Middleware function to verify user account and permissions
|
|
54
|
+
* @param {Array} requiredPermissions - Array of required permissions for the user
|
|
55
|
+
* @returns {Function} - Express middleware function
|
|
56
|
+
*/
|
|
57
|
+
const VerifyAccount = (requiredPermissions) => {
|
|
58
|
+
return async (req, res, next) => {
|
|
59
|
+
const accessToken = req.headers.authorization?.split(" ")[1];
|
|
60
|
+
if (!accessToken) {
|
|
61
|
+
return res.status(401).json({ error: "Access token is required" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if token is in cache
|
|
65
|
+
if (tokenCache.has(accessToken)) {
|
|
66
|
+
req.user = tokenCache.get(accessToken);
|
|
67
|
+
return next();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
|
|
72
|
+
if (!isValid(decoded, requiredPermissions)) {
|
|
73
|
+
return res.status(403).json({ error: "Invalid permissions" });
|
|
74
|
+
}
|
|
75
|
+
tokenCache.set(accessToken, decoded);
|
|
76
|
+
req.user = decoded;
|
|
77
|
+
return next();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
try {
|
|
80
|
+
const userResponse = await axios.get(
|
|
81
|
+
`${process.env.AUTH_URL}/api/user`,
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
Authorization: `Bearer ${accessToken}`,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!isValid(userResponse.data, requiredPermissions)) {
|
|
90
|
+
return res.status(403).json({ error: "Invalid permissions" });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
tokenCache.set(accessToken, userResponse.data);
|
|
94
|
+
req.user = userResponse.data;
|
|
95
|
+
return next();
|
|
96
|
+
} catch (networkError) {
|
|
97
|
+
return res.status(500).json({ error: "Error validating token" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Checks if the decoded token has all the required permissions.
|
|
105
|
+
* @param {object} decodedToken - The decoded token object.
|
|
106
|
+
* @param {string[]} requiredPermissions - An array of required permissions.
|
|
107
|
+
* @returns {boolean} - Returns true if the decoded token has all the required permissions, false otherwise.
|
|
108
|
+
*/
|
|
109
|
+
function isValid(decodedToken, requiredPermissions) {
|
|
110
|
+
return requiredPermissions.every((permission) =>
|
|
111
|
+
decodedToken.permissions.includes(permission)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
VerifyAccount,
|
|
117
|
+
exchangeToken,
|
|
118
|
+
verifyJWT,
|
|
119
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
describe("isValid", () => {
|
|
2
|
+
it("should return true when all required permissions are present", () => {
|
|
3
|
+
const decodedToken = { permissions: ["user", "manageUsers"] };
|
|
4
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
5
|
+
|
|
6
|
+
const result = isValid(decodedToken, requiredPermissions);
|
|
7
|
+
|
|
8
|
+
expect(result).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return false when some required permissions are missing", () => {
|
|
12
|
+
const decodedToken = { permissions: ["user"] };
|
|
13
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
14
|
+
|
|
15
|
+
const result = isValid(decodedToken, requiredPermissions);
|
|
16
|
+
|
|
17
|
+
expect(result).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should return false when all required permissions are missing", () => {
|
|
21
|
+
const decodedToken = { permissions: [] };
|
|
22
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
23
|
+
|
|
24
|
+
const result = isValid(decodedToken, requiredPermissions);
|
|
25
|
+
|
|
26
|
+
expect(result).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return true when no permissions are required", () => {
|
|
30
|
+
const decodedToken = { permissions: ["user", "manageUsers"] };
|
|
31
|
+
const requiredPermissions = [];
|
|
32
|
+
|
|
33
|
+
const result = isValid(decodedToken, requiredPermissions);
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("VerifyAccount", () => {
|
|
40
|
+
// Verify that a valid access token is present in the request header and proceed with the verification process.
|
|
41
|
+
it("should return a 401 error response when access token is not present in the request header", async () => {
|
|
42
|
+
const req = { headers: {} };
|
|
43
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
44
|
+
const next = jest.fn();
|
|
45
|
+
|
|
46
|
+
await VerifyAccount([])(req, res, next);
|
|
47
|
+
|
|
48
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
49
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
50
|
+
error: "Access token is required",
|
|
51
|
+
});
|
|
52
|
+
expect(next).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Verify that the account has all the required permissions and attach account info to the request object.
|
|
56
|
+
it("should return a 403 error response when the account does not have all the required permissions", async () => {
|
|
57
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
58
|
+
const account = { id: "123", permissions: ["user"] };
|
|
59
|
+
const req = { headers: { authorization: "Bearer token" }, account };
|
|
60
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
61
|
+
const next = jest.fn();
|
|
62
|
+
|
|
63
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
64
|
+
|
|
65
|
+
expect(res.status).toHaveBeenCalledWith(403);
|
|
66
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
67
|
+
error: "Insufficient permissions",
|
|
68
|
+
});
|
|
69
|
+
expect(next).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Verify that the function calls the next middleware function after successful verification.
|
|
73
|
+
it("should call the next middleware function after successful verification", async () => {
|
|
74
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
75
|
+
const account = { id: "123", permissions: ["user", "manageUsers"] };
|
|
76
|
+
const req = { headers: { authorization: "Bearer token" }, account };
|
|
77
|
+
const res = { status: jest.fn(), json: jest.fn() };
|
|
78
|
+
const next = jest.fn();
|
|
79
|
+
|
|
80
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
81
|
+
|
|
82
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
83
|
+
expect(res.json).not.toHaveBeenCalled();
|
|
84
|
+
expect(next).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Verify that the function returns a 500 error response when an unexpected error occurs during verification.
|
|
88
|
+
it("should return a 500 error response when an unexpected error occurs during verification", async () => {
|
|
89
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
90
|
+
const req = { headers: { authorization: "Bearer token" } };
|
|
91
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
92
|
+
const next = jest.fn();
|
|
93
|
+
|
|
94
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
95
|
+
|
|
96
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
97
|
+
expect(res.json).toHaveBeenCalledWith({ error: "Internal Server Error" });
|
|
98
|
+
expect(next).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Verify that the function handles errors returned by the external API and returns a 401 error response when the access token is invalid or expired.
|
|
102
|
+
it("should return a 401 error response when the access token is invalid or expired", async () => {
|
|
103
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
104
|
+
const req = { headers: { authorization: "Bearer token" } };
|
|
105
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
106
|
+
const next = jest.fn();
|
|
107
|
+
|
|
108
|
+
axios.get.mockRejectedValueOnce({ response: { status: 401 } });
|
|
109
|
+
|
|
110
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
111
|
+
|
|
112
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
113
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
114
|
+
error: "Invalid or expired access token",
|
|
115
|
+
});
|
|
116
|
+
expect(next).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Verify that the function handles errors returned by the external API and returns a 500 error response when an unexpected error occurs during API call.
|
|
120
|
+
it("should return a 500 error response when an unexpected error occurs during API call", async () => {
|
|
121
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
122
|
+
const req = { headers: { authorization: "Bearer token" } };
|
|
123
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
124
|
+
const next = jest.fn();
|
|
125
|
+
|
|
126
|
+
axios.get.mockRejectedValueOnce(new Error("API error"));
|
|
127
|
+
|
|
128
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
129
|
+
|
|
130
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
131
|
+
expect(res.json).toHaveBeenCalledWith({ error: "Internal Server Error" });
|
|
132
|
+
expect(next).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Verify that the function retrieves the user from cache when the token is already cached.
|
|
136
|
+
it("should retrieve the user from cache when the token is already cached", async () => {
|
|
137
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
138
|
+
const account = { id: "123", permissions: ["user", "manageUsers"] };
|
|
139
|
+
const accessToken = "token";
|
|
140
|
+
const decoded = { ...account, accessToken };
|
|
141
|
+
const req = { headers: { authorization: `Bearer ${accessToken}` } };
|
|
142
|
+
const res = { status: jest.fn(), json: jest.fn() };
|
|
143
|
+
const next = jest.fn();
|
|
144
|
+
|
|
145
|
+
tokenCache.set(accessToken, decoded);
|
|
146
|
+
|
|
147
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
148
|
+
|
|
149
|
+
expect(req.user).toEqual(decoded);
|
|
150
|
+
expect(next).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Verify that the function retrieves the user from the external API when the token is not cached.
|
|
154
|
+
it("should retrieve the user from the external API when the token is not cached", async () => {
|
|
155
|
+
const requiredPermissions = ["user", "manageUsers"];
|
|
156
|
+
const account = { id: "123", permissions: ["user", "manageUsers"] };
|
|
157
|
+
const accessToken = "token";
|
|
158
|
+
const decoded = { ...account, accessToken };
|
|
159
|
+
const req = { headers: { authorization: `Bearer ${accessToken}` } };
|
|
160
|
+
const res = { status: jest.fn(), json: jest.fn() };
|
|
161
|
+
const next = jest.fn();
|
|
162
|
+
|
|
163
|
+
axios.get.mockResolvedValueOnce({ data: decoded });
|
|
164
|
+
|
|
165
|
+
await VerifyAccount(requiredPermissions)(req, res, next);
|
|
166
|
+
|
|
167
|
+
expect(req.user).toEqual(decoded);
|
|
168
|
+
expect(tokenCache.get(accessToken)).toEqual(decoded);
|
|
169
|
+
expect(next).toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const request = require("supertest");
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const app = express();
|
|
4
|
+
const axios = require("axios");
|
|
5
|
+
jest.mock("axios");
|
|
6
|
+
|
|
7
|
+
require("./index.js")(app);
|
|
8
|
+
|
|
9
|
+
describe("GET /auth", () => {
|
|
10
|
+
it("should redirect to the auth client", async () => {
|
|
11
|
+
const res = await request(app).get("/auth");
|
|
12
|
+
expect(res.statusCode).toEqual(302);
|
|
13
|
+
expect(res.headers.location).toContain(process.env.CLIENT_URL);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("GET /callback", () => {
|
|
18
|
+
it("should exchange code for tokens", async () => {
|
|
19
|
+
const mockCode = "mockCode";
|
|
20
|
+
const mockTokenResponse = {
|
|
21
|
+
data: {
|
|
22
|
+
access_token: "mockAccessToken",
|
|
23
|
+
refresh_token: "mockRefreshToken",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
axios.post.mockResolvedValue(mockTokenResponse);
|
|
27
|
+
|
|
28
|
+
const res = await request(app).get(`/callback?code=${mockCode}`);
|
|
29
|
+
expect(res.statusCode).toEqual(302);
|
|
30
|
+
expect(res.headers.location).toContain(mockTokenResponse.data.access_token);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should respond with 400 if no code is provided", async () => {
|
|
34
|
+
const res = await request(app).get("/callback");
|
|
35
|
+
expect(res.statusCode).toEqual(400);
|
|
36
|
+
});
|
|
37
|
+
});
|