w3home-utils 1.0.0
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 +193 -0
- package/package.json +42 -0
- package/w3home-utils/activityLogger.js +129 -0
- package/w3home-utils/authUtils.js +118 -0
- package/w3home-utils/authorization/backofficeAuthorization.js +154 -0
- package/w3home-utils/authorization/buyerAuthorization.js +97 -0
- package/w3home-utils/authorization/index.js +87 -0
- package/w3home-utils/authorization/roles.js +75 -0
- package/w3home-utils/authorization/rolesLoader.js +138 -0
- package/w3home-utils/index.js +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# w3home-utils
|
|
2
|
+
|
|
3
|
+
W3Home Utilities - Authorization, Activity Logging, and Authentication utilities for HomePay services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install w3home-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const {
|
|
15
|
+
// Authentication
|
|
16
|
+
getIndetifiers,
|
|
17
|
+
getUser,
|
|
18
|
+
getUserType,
|
|
19
|
+
decodeIdToken,
|
|
20
|
+
|
|
21
|
+
// Authorization
|
|
22
|
+
authorize,
|
|
23
|
+
withAuthorization,
|
|
24
|
+
authorizeBuyer,
|
|
25
|
+
authorizeBackofficeProject,
|
|
26
|
+
ROLES,
|
|
27
|
+
UserType,
|
|
28
|
+
|
|
29
|
+
// Activity Logging
|
|
30
|
+
logActivity,
|
|
31
|
+
logPostActivity,
|
|
32
|
+
withActivityLogging,
|
|
33
|
+
|
|
34
|
+
// Common
|
|
35
|
+
corsHeaders
|
|
36
|
+
} = require('w3home-utils');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
### Get User Identifiers from Request Headers
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
const { getIndetifiers } = require('w3home-utils');
|
|
45
|
+
|
|
46
|
+
const handler = async (event) => {
|
|
47
|
+
const { userId } = await getIndetifiers(event.headers);
|
|
48
|
+
if (!userId) {
|
|
49
|
+
return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };
|
|
50
|
+
}
|
|
51
|
+
// ... continue with userId
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Get User Details
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const { getUser, getUserType } = require('w3home-utils');
|
|
59
|
+
|
|
60
|
+
const handler = async (event) => {
|
|
61
|
+
const { userId } = await getIndetifiers(event.headers);
|
|
62
|
+
const user = await getUser(userId);
|
|
63
|
+
const userType = getUserType(user); // 'BUYER' | 'BACKOFFICE_USER' | 'BACKOFFICE_ADMIN'
|
|
64
|
+
// ...
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Authorization
|
|
69
|
+
|
|
70
|
+
### Using withAuthorization Wrapper
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
const { withAuthorization, getUser } = require('w3home-utils');
|
|
74
|
+
|
|
75
|
+
const myHandler = async (event, context) => {
|
|
76
|
+
// event.authContext contains authorization result
|
|
77
|
+
const { authorized, role, permissions } = event.authContext;
|
|
78
|
+
// ...
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports.handler = withAuthorization(myHandler, {
|
|
82
|
+
resource: 'projects',
|
|
83
|
+
getResourceId: (event) => event.pathParameters?.projectId,
|
|
84
|
+
getUser: (userId) => getUser(userId)
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Manual Authorization Check
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
const { authorize, getUserType } = require('w3home-utils');
|
|
92
|
+
|
|
93
|
+
const handler = async (event) => {
|
|
94
|
+
const { userId } = await getIndetifiers(event.headers);
|
|
95
|
+
const user = await getUser(userId);
|
|
96
|
+
|
|
97
|
+
const authResult = await authorize({
|
|
98
|
+
userId,
|
|
99
|
+
userType: getUserType(user),
|
|
100
|
+
resource: 'projects',
|
|
101
|
+
action: 'READ',
|
|
102
|
+
resourceId: event.pathParameters?.projectId,
|
|
103
|
+
user
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!authResult.authorized) {
|
|
107
|
+
return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) };
|
|
108
|
+
}
|
|
109
|
+
// ...
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Activity Logging
|
|
114
|
+
|
|
115
|
+
### Using withActivityLogging Wrapper
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const { withActivityLogging, getIndetifiers } = require('w3home-utils');
|
|
119
|
+
|
|
120
|
+
const myHandler = async (event, context) => {
|
|
121
|
+
// Your handler logic
|
|
122
|
+
return { statusCode: 200, body: JSON.stringify({ success: true }) };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
module.exports.handler = withActivityLogging(myHandler, {
|
|
126
|
+
resource: 'payments',
|
|
127
|
+
extractUserId: async (event) => {
|
|
128
|
+
const { userId } = await getIndetifiers(event.headers);
|
|
129
|
+
return userId;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Manual Activity Logging
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const { logPostActivity } = require('w3home-utils');
|
|
138
|
+
|
|
139
|
+
const handler = async (event, context) => {
|
|
140
|
+
const { userId } = await getIndetifiers(event.headers);
|
|
141
|
+
|
|
142
|
+
// ... perform action ...
|
|
143
|
+
|
|
144
|
+
logPostActivity({
|
|
145
|
+
event,
|
|
146
|
+
userId,
|
|
147
|
+
action: 'CREATE',
|
|
148
|
+
resource: 'payments',
|
|
149
|
+
statusCode: 200,
|
|
150
|
+
context
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Roles & Permissions
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
const { ROLES, UserType, hasPermission } = require('w3home-utils');
|
|
159
|
+
|
|
160
|
+
// Check if user has permission
|
|
161
|
+
const canRead = await hasPermission(userId, 'projects', 'READ');
|
|
162
|
+
|
|
163
|
+
// Get role definitions
|
|
164
|
+
console.log(ROLES.BACKOFFICE_ADMIN);
|
|
165
|
+
// { id: 'BACKOFFICE_ADMIN', permissions: [...] }
|
|
166
|
+
|
|
167
|
+
// User types
|
|
168
|
+
console.log(UserType.BUYER); // 'BUYER'
|
|
169
|
+
console.log(UserType.BACKOFFICE_USER); // 'BACKOFFICE_USER'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Environment Variables
|
|
173
|
+
|
|
174
|
+
| Variable | Default | Description |
|
|
175
|
+
|----------|---------|-------------|
|
|
176
|
+
| `USERS_TABLE` | `w3HomeUsers` | DynamoDB table for users |
|
|
177
|
+
| `ROLES_TABLE` | `w3home-roles` | DynamoDB table for roles |
|
|
178
|
+
| `CONFIG_TABLE` | `w3home-config` | DynamoDB table for config |
|
|
179
|
+
| `STAGE` | `dev` | Environment stage |
|
|
180
|
+
|
|
181
|
+
## Peer Dependencies
|
|
182
|
+
|
|
183
|
+
This package requires `aws-sdk` as a peer dependency. In Lambda, this is already available. For local development:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm install aws-sdk --save-dev
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
UNLICENSED - HomePay Internal Use Only
|
|
192
|
+
|
|
193
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "w3home-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "W3Home Utilities - Authorization, Activity Logging, Auth Utilities",
|
|
5
|
+
"main": "w3home-utils/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"w3home-utils/**/*.js"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"No tests specified\" && exit 0",
|
|
11
|
+
"prepublishOnly": "npm test",
|
|
12
|
+
"seed:dev": "node seed-tables.js dev",
|
|
13
|
+
"seed:prd": "node seed-tables.js prd"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"jsonwebtoken": "^9.0.2"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"aws-sdk": "^2.1000.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"aws-sdk": "^2.1692.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"authorization",
|
|
26
|
+
"authentication",
|
|
27
|
+
"activity-logging",
|
|
28
|
+
"w3home",
|
|
29
|
+
"homepay",
|
|
30
|
+
"lambda",
|
|
31
|
+
"aws"
|
|
32
|
+
],
|
|
33
|
+
"author": "HomePay",
|
|
34
|
+
"license": "UNLICENSED",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/homepay/w3home-utils.git"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Logger Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const LOG_PREFIX = 'ACTIVITY_LOG';
|
|
6
|
+
|
|
7
|
+
function extractIdentifiers(event, userId) {
|
|
8
|
+
const identifiers = { userId, projectId: null, apartmentId: null, customerId: null, paymentId: null };
|
|
9
|
+
|
|
10
|
+
if (event.pathParameters) {
|
|
11
|
+
identifiers.projectId = event.pathParameters.projectId || null;
|
|
12
|
+
identifiers.apartmentId = event.pathParameters.apartmentId || null;
|
|
13
|
+
identifiers.customerId = event.pathParameters.customerId || null;
|
|
14
|
+
identifiers.paymentId = event.pathParameters.paymentId || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!identifiers.projectId || !identifiers.apartmentId) {
|
|
18
|
+
try {
|
|
19
|
+
const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
|
|
20
|
+
if (body) {
|
|
21
|
+
identifiers.projectId = identifiers.projectId || body.projectId || null;
|
|
22
|
+
identifiers.apartmentId = identifiers.apartmentId || body.apartmentId || null;
|
|
23
|
+
identifiers.customerId = identifiers.customerId || body.customerId || null;
|
|
24
|
+
identifiers.paymentId = identifiers.paymentId || body.paymentId || null;
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return identifiers;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractRequestContext(event) {
|
|
33
|
+
const httpContext = event.requestContext?.http || event.requestContext || {};
|
|
34
|
+
return {
|
|
35
|
+
method: httpContext.method || event.httpMethod || 'UNKNOWN',
|
|
36
|
+
path: event.rawPath || event.path || httpContext.path || 'UNKNOWN',
|
|
37
|
+
sourceIp: httpContext.sourceIp || event.requestContext?.identity?.sourceIp || null,
|
|
38
|
+
userAgent: httpContext.userAgent || event.headers?.['user-agent'] || event.headers?.['User-Agent'] || null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function logActivity({ event, userId, action, resource, statusCode, metadata = {}, context }) {
|
|
43
|
+
const identifiers = extractIdentifiers(event, userId);
|
|
44
|
+
const requestContext = extractRequestContext(event);
|
|
45
|
+
|
|
46
|
+
const activityLog = {
|
|
47
|
+
logType: LOG_PREFIX,
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
userId: identifiers.userId,
|
|
50
|
+
projectId: identifiers.projectId,
|
|
51
|
+
apartmentId: identifiers.apartmentId,
|
|
52
|
+
action,
|
|
53
|
+
resource,
|
|
54
|
+
statusCode,
|
|
55
|
+
method: requestContext.method,
|
|
56
|
+
path: requestContext.path,
|
|
57
|
+
sourceIp: requestContext.sourceIp,
|
|
58
|
+
userAgent: requestContext.userAgent,
|
|
59
|
+
requestId: context?.awsRequestId || null,
|
|
60
|
+
functionName: context?.functionName || null,
|
|
61
|
+
stage: process.env.STAGE || 'dev',
|
|
62
|
+
body: event.body,
|
|
63
|
+
metadata
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
console.log(JSON.stringify(activityLog));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function logPostActivity(params) {
|
|
70
|
+
logActivity(params);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logReadActivity(params) {
|
|
74
|
+
logActivity({ ...params, action: params.action || 'READ' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withActivityLogging(handler, config = {}) {
|
|
78
|
+
return async (event, context) => {
|
|
79
|
+
const resource = config.resource || 'unknown';
|
|
80
|
+
let userId = null;
|
|
81
|
+
let action = 'CREATE';
|
|
82
|
+
let statusCode = null;
|
|
83
|
+
let metadata = {};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (config.extractUserId) userId = await config.extractUserId(event);
|
|
87
|
+
|
|
88
|
+
if (config.extractAction) {
|
|
89
|
+
action = await config.extractAction(event);
|
|
90
|
+
} else {
|
|
91
|
+
const method = event.httpMethod || event.requestContext?.http?.method || 'POST';
|
|
92
|
+
const mapping = { 'GET': 'READ', 'POST': 'CREATE', 'PUT': 'UPDATE', 'PATCH': 'UPDATE', 'DELETE': 'DELETE' };
|
|
93
|
+
action = mapping[method] || 'CREATE';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await handler(event, context);
|
|
97
|
+
statusCode = result.statusCode || 200;
|
|
98
|
+
|
|
99
|
+
if (config.extractMetadata) metadata = config.extractMetadata(result, event);
|
|
100
|
+
logActivity({ event, userId, action, resource, statusCode, metadata, context });
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
statusCode = 500;
|
|
105
|
+
metadata = { error: error.message };
|
|
106
|
+
logActivity({ event, userId, action, resource, statusCode, metadata, context });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createResourceLogger(resource) {
|
|
113
|
+
return {
|
|
114
|
+
log: (params) => logActivity({ ...params, resource }),
|
|
115
|
+
logPost: (params) => logPostActivity({ ...params, resource }),
|
|
116
|
+
logRead: (params) => logReadActivity({ ...params, resource })
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
logActivity,
|
|
122
|
+
logPostActivity,
|
|
123
|
+
logReadActivity,
|
|
124
|
+
withActivityLogging,
|
|
125
|
+
createResourceLogger,
|
|
126
|
+
extractIdentifiers,
|
|
127
|
+
LOG_PREFIX
|
|
128
|
+
};
|
|
129
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const jwt = require('jsonwebtoken');
|
|
6
|
+
const AWS = require('aws-sdk');
|
|
7
|
+
|
|
8
|
+
const USERS_TABLE = process.env.USERS_TABLE || 'w3HomeUsers';
|
|
9
|
+
const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
10
|
+
|
|
11
|
+
const USER_CACHE_TTL = 5 * 60 * 1000;
|
|
12
|
+
const userCache = new Map();
|
|
13
|
+
|
|
14
|
+
function decodeIdToken(idToken) {
|
|
15
|
+
try {
|
|
16
|
+
const decoded = jwt.decode(idToken);
|
|
17
|
+
return decoded?.sub || null;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error decoding token:', error.message);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getIndetifiers(headers) {
|
|
25
|
+
let authorizationToken = '';
|
|
26
|
+
|
|
27
|
+
if (headers['Authorization']) {
|
|
28
|
+
const parts = headers['Authorization'].split(' ');
|
|
29
|
+
authorizationToken = parts.length > 1 ? parts[1] : parts[0];
|
|
30
|
+
}
|
|
31
|
+
if (!authorizationToken && headers['Token']) {
|
|
32
|
+
authorizationToken = headers['Token'];
|
|
33
|
+
}
|
|
34
|
+
if (!authorizationToken && headers['authorization']) {
|
|
35
|
+
const parts = headers['authorization'].split(' ');
|
|
36
|
+
authorizationToken = parts.length > 1 ? parts[1] : parts[0];
|
|
37
|
+
}
|
|
38
|
+
if (!authorizationToken && headers['token']) {
|
|
39
|
+
authorizationToken = headers['token'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (authorizationToken) {
|
|
43
|
+
const sub = decodeIdToken(authorizationToken);
|
|
44
|
+
if (sub) return { userId: sub };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { userId: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getUser(userId, options = {}, noCredentials = false) {
|
|
51
|
+
if (!userId) return null;
|
|
52
|
+
|
|
53
|
+
const useCache = !options.noCache && !noCredentials;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
|
|
56
|
+
if (useCache) {
|
|
57
|
+
const cached = userCache.get(userId);
|
|
58
|
+
if (cached && cached.expiry > now) return cached.user;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await dynamodb.get({
|
|
63
|
+
TableName: USERS_TABLE,
|
|
64
|
+
Key: { id: userId }
|
|
65
|
+
}).promise();
|
|
66
|
+
|
|
67
|
+
if (!result.Item) return null;
|
|
68
|
+
|
|
69
|
+
if (useCache) {
|
|
70
|
+
userCache.set(userId, { user: result.Item, expiry: now + USER_CACHE_TTL });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result.Item;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Error fetching user:', error.message);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUserType(user) {
|
|
81
|
+
if (!user) return null;
|
|
82
|
+
if (user.userType) return user.userType;
|
|
83
|
+
|
|
84
|
+
const role = user.role;
|
|
85
|
+
if (role === 'ADMIN' || role === 'BACKOFFICE_ADMIN') return 'BACKOFFICE_ADMIN';
|
|
86
|
+
if (role === 'USER' || role === 'BACKOFFICE_USER') return 'BACKOFFICE_USER';
|
|
87
|
+
if (user.customerUserId || (!user.institutionId && !user.institutionIds)) return 'BUYER';
|
|
88
|
+
|
|
89
|
+
return 'BACKOFFICE_USER';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearUserCache(userId) {
|
|
93
|
+
userId ? userCache.delete(userId) : userCache.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getUserInstitutionId(user) {
|
|
97
|
+
if (!user) return null;
|
|
98
|
+
return user.primaryInstitutionId || user.institutionId || (user.institutionIds?.[0]) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getUserInstitutionIds(user) {
|
|
102
|
+
if (!user) return [];
|
|
103
|
+
if (user.institutionIds && Array.isArray(user.institutionIds)) return user.institutionIds;
|
|
104
|
+
if (user.institutionId) return [user.institutionId];
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
decodeIdToken,
|
|
110
|
+
getIndetifiers,
|
|
111
|
+
getUser,
|
|
112
|
+
getUserType,
|
|
113
|
+
clearUserCache,
|
|
114
|
+
getUserInstitutionId,
|
|
115
|
+
getUserInstitutionIds,
|
|
116
|
+
USER_CACHE_TTL
|
|
117
|
+
};
|
|
118
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backoffice Authorization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const AWS = require('aws-sdk');
|
|
6
|
+
|
|
7
|
+
const PROJECT_TABLE = process.env.PROJECT_TABLE || process.env.PROJECTS_TABLE || 'w3HomeProjects';
|
|
8
|
+
const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
9
|
+
|
|
10
|
+
async function getProjectById(projectId) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await dynamodb.get({
|
|
13
|
+
TableName: PROJECT_TABLE,
|
|
14
|
+
Key: { id: projectId }
|
|
15
|
+
}).promise();
|
|
16
|
+
return result.Item || null;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error fetching project:', error);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getUserOrganizations(user) {
|
|
24
|
+
if (user.institutionIds && Array.isArray(user.institutionIds) && user.institutionIds.length > 0) {
|
|
25
|
+
return user.institutionIds;
|
|
26
|
+
}
|
|
27
|
+
if (user.institutionId) return [user.institutionId];
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getProjectOrganizations(project) {
|
|
32
|
+
return [
|
|
33
|
+
project.financialInstitution,
|
|
34
|
+
project.insuranceInstitution,
|
|
35
|
+
project.constructorInstitution,
|
|
36
|
+
project.constructorInstitution2
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function authorizeBackofficeProject(user, projectId) {
|
|
41
|
+
const project = await getProjectById(projectId);
|
|
42
|
+
|
|
43
|
+
if (!project) {
|
|
44
|
+
return { authorized: false, error: 'PROJECT_NOT_FOUND', message: 'Project does not exist' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const userOrgs = getUserOrganizations(user);
|
|
48
|
+
if (userOrgs.length === 0) {
|
|
49
|
+
return { authorized: false, error: 'NO_ORGANIZATION', message: 'User is not associated with any organization' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const projectOrgs = getProjectOrganizations(project);
|
|
53
|
+
const matchingOrg = userOrgs.find(orgId => projectOrgs.includes(orgId));
|
|
54
|
+
|
|
55
|
+
if (!matchingOrg) {
|
|
56
|
+
return { authorized: false, error: 'ORG_NOT_IN_PROJECT', message: 'User organization does not participate in this project' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isProjectOwner = project.creatorInstitutionId === matchingOrg || project.userId === user.id;
|
|
60
|
+
const userRole = user.role || 'USER';
|
|
61
|
+
const isAdmin = userRole === 'ADMIN' || userRole === 'BACKOFFICE_ADMIN';
|
|
62
|
+
|
|
63
|
+
const basePermissions = isProjectOwner ? ['READ', 'CREATE', 'UPDATE', 'DELETE'] : ['READ', 'CREATE', 'UPDATE'];
|
|
64
|
+
const permissions = isAdmin ? [...basePermissions, 'MANAGE_USERS'] : basePermissions;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
authorized: true,
|
|
68
|
+
accessLevel: isProjectOwner ? 'OWNER' : 'PARTICIPANT',
|
|
69
|
+
organizationId: matchingOrg,
|
|
70
|
+
role: userRole,
|
|
71
|
+
isAdmin,
|
|
72
|
+
permissions,
|
|
73
|
+
projectId
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getAccessibleProjects(user) {
|
|
78
|
+
const userOrgs = getUserOrganizations(user);
|
|
79
|
+
if (userOrgs.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const allProjects = [];
|
|
82
|
+
const seenIds = new Set();
|
|
83
|
+
|
|
84
|
+
const indexes = [
|
|
85
|
+
{ indexName: 'constructorInstitution-index', attributeName: 'constructorInstitution' },
|
|
86
|
+
{ indexName: 'financialInstitution-index', attributeName: 'financialInstitution' },
|
|
87
|
+
{ indexName: 'insuranceInstitution-index', attributeName: 'insuranceInstitution' }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const orgId of userOrgs) {
|
|
91
|
+
for (const { indexName, attributeName } of indexes) {
|
|
92
|
+
try {
|
|
93
|
+
const result = await dynamodb.query({
|
|
94
|
+
TableName: PROJECT_TABLE,
|
|
95
|
+
IndexName: indexName,
|
|
96
|
+
KeyConditionExpression: `#inst = :orgId`,
|
|
97
|
+
ExpressionAttributeNames: { '#inst': attributeName },
|
|
98
|
+
ExpressionAttributeValues: { ':orgId': orgId }
|
|
99
|
+
}).promise();
|
|
100
|
+
|
|
101
|
+
for (const project of (result.Items || [])) {
|
|
102
|
+
if (!seenIds.has(project.id)) {
|
|
103
|
+
allProjects.push(project);
|
|
104
|
+
seenIds.add(project.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return allProjects;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function canManageUsers(user) {
|
|
115
|
+
const role = user.role || 'USER';
|
|
116
|
+
return role === 'ADMIN' || role === 'BACKOFFICE_ADMIN';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function authorizeUserManagement(actor, targetUserId, action) {
|
|
120
|
+
if (!canManageUsers(actor)) {
|
|
121
|
+
return { authorized: false, error: 'NOT_ADMIN', message: 'Only administrators can manage users' };
|
|
122
|
+
}
|
|
123
|
+
if (action === 'DELETE' && actor.id === targetUserId) {
|
|
124
|
+
return { authorized: false, error: 'CANNOT_DELETE_SELF', message: 'Cannot delete your own account' };
|
|
125
|
+
}
|
|
126
|
+
if (action === 'CHANGE_ROLE' && actor.id === targetUserId) {
|
|
127
|
+
return { authorized: false, error: 'CANNOT_CHANGE_OWN_ROLE', message: 'Cannot change your own role' };
|
|
128
|
+
}
|
|
129
|
+
return { authorized: true, permissions: ['CREATE_USER', 'UPDATE_USER', 'DELETE_USER', 'CHANGE_ROLE'] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function validateUserAccessToProject(apartment, user) {
|
|
133
|
+
if (!apartment.projectId) return false;
|
|
134
|
+
const result = await authorizeBackofficeProject(user, apartment.projectId);
|
|
135
|
+
return result.authorized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function authorizeBackofficeAction(user, projectId, resource, action) {
|
|
139
|
+
const projectAuth = await authorizeBackofficeProject(user, projectId);
|
|
140
|
+
if (!projectAuth.authorized) return projectAuth;
|
|
141
|
+
|
|
142
|
+
if (!projectAuth.permissions.includes(action.toUpperCase())) {
|
|
143
|
+
return { authorized: false, error: 'ACTION_NOT_ALLOWED', message: `No permission to ${action}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (resource === 'user' && !canManageUsers(user)) {
|
|
147
|
+
return { authorized: false, error: 'NOT_ADMIN', message: 'Only administrators can manage users' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { authorized: true, ...projectAuth, resource, action };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { authorizeBackofficeProject, getAccessibleProjects, canManageUsers, authorizeUserManagement, validateUserAccessToProject, authorizeBackofficeAction, getUserOrganizations, getProjectOrganizations };
|
|
154
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buyer Authorization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const AWS = require('aws-sdk');
|
|
6
|
+
|
|
7
|
+
const CUSTOMERS_TABLE = process.env.CUSTOMERS_TABLE || 'w3HomeCustomers';
|
|
8
|
+
const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
9
|
+
|
|
10
|
+
async function getCustomersByUserId(userId) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await dynamodb.query({
|
|
13
|
+
TableName: CUSTOMERS_TABLE,
|
|
14
|
+
IndexName: 'customerUserId-index',
|
|
15
|
+
KeyConditionExpression: 'customerUserId = :userId',
|
|
16
|
+
ExpressionAttributeValues: { ':userId': userId }
|
|
17
|
+
}).promise();
|
|
18
|
+
return result.Items || [];
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error fetching customers:', error);
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getCustomerByUserIdAndApartment(userId, apartmentId) {
|
|
26
|
+
const customers = await getCustomersByUserId(userId);
|
|
27
|
+
return customers.find(c => c.apartmentId === apartmentId) || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function validateBuyerAccess(userId, apartmentId) {
|
|
31
|
+
const customer = await getCustomerByUserIdAndApartment(userId, apartmentId);
|
|
32
|
+
if (!customer) return false;
|
|
33
|
+
if (customer.status && customer.status !== 'ACTIVE') return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function authorizeBuyer(userId, apartmentId) {
|
|
38
|
+
const customers = await getCustomersByUserId(userId);
|
|
39
|
+
|
|
40
|
+
if (!customers || customers.length === 0) {
|
|
41
|
+
return { authorized: false, error: 'NO_CUSTOMER_RECORD', message: 'User is not registered as a customer' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const linkedCustomer = customers.find(c => c.apartmentId === apartmentId && (!c.status || c.status === 'ACTIVE'));
|
|
45
|
+
|
|
46
|
+
if (!linkedCustomer) {
|
|
47
|
+
return { authorized: false, error: 'NOT_LINKED_TO_APARTMENT', message: 'User is not linked to this apartment' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
authorized: true,
|
|
52
|
+
customerId: linkedCustomer.id,
|
|
53
|
+
apartmentId: linkedCustomer.apartmentId,
|
|
54
|
+
projectId: linkedCustomer.projectId,
|
|
55
|
+
permissions: ['READ_APARTMENT', 'READ_PAYMENTS', 'READ_VOUCHERS', 'READ_DOCUMENTS', 'UPDATE_SELF']
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getBuyerAccessibleApartments(userId) {
|
|
60
|
+
const customers = await getCustomersByUserId(userId);
|
|
61
|
+
return customers
|
|
62
|
+
.filter(c => !c.status || c.status === 'ACTIVE')
|
|
63
|
+
.map(c => ({ apartmentId: c.apartmentId, projectId: c.projectId, customerId: c.id }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function authorizeBuyerAction(userId, apartmentId, resource, action, context = {}) {
|
|
67
|
+
const accessResult = await authorizeBuyer(userId, apartmentId);
|
|
68
|
+
if (!accessResult.authorized) return accessResult;
|
|
69
|
+
|
|
70
|
+
const buyerPermissions = {
|
|
71
|
+
apartment: ['READ'],
|
|
72
|
+
payment: ['READ'],
|
|
73
|
+
voucher: ['READ'],
|
|
74
|
+
document: ['READ'],
|
|
75
|
+
customer: ['READ', 'UPDATE']
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const resourcePerms = buyerPermissions[resource.toLowerCase()];
|
|
79
|
+
if (!resourcePerms) {
|
|
80
|
+
return { authorized: false, error: 'RESOURCE_NOT_ALLOWED', message: `Buyers cannot access resource: ${resource}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!resourcePerms.includes(action.toUpperCase())) {
|
|
84
|
+
return { authorized: false, error: 'ACTION_NOT_ALLOWED', message: `Buyers cannot perform ${action} on ${resource}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (resource.toLowerCase() === 'customer' && action.toUpperCase() === 'UPDATE') {
|
|
88
|
+
if (context.customerId && context.customerId !== accessResult.customerId) {
|
|
89
|
+
return { authorized: false, error: 'NOT_OWNER', message: 'Buyers can only update their own profile' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { authorized: true, ...accessResult, resource, action };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { authorizeBuyer, getBuyerAccessibleApartments, validateBuyerAccess, authorizeBuyerAction, getCustomersByUserId, getCustomerByUserIdAndApartment };
|
|
97
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { authorizeBuyer, getBuyerAccessibleApartments, validateBuyerAccess, authorizeBuyerAction } = require('./buyerAuthorization');
|
|
6
|
+
const { authorizeBackofficeProject, getAccessibleProjects, canManageUsers, authorizeUserManagement, validateUserAccessToProject, authorizeBackofficeAction } = require('./backofficeAuthorization');
|
|
7
|
+
const { getRoles, getRole, hasPermission, invalidateCache, invalidateRolesCache, getCacheStats } = require('./rolesLoader');
|
|
8
|
+
const { UserType, BackofficeRole, Actions, Resources, ROLES, getRoleById, roleHasPermission, getRolePermissions } = require('./roles');
|
|
9
|
+
|
|
10
|
+
async function authorize(params) {
|
|
11
|
+
const { userId, userType, resource, action, resourceId, context = {}, user } = params;
|
|
12
|
+
|
|
13
|
+
if (!userId && !user) return { authorized: false, error: 'USER_NOT_FOUND' };
|
|
14
|
+
|
|
15
|
+
switch (userType) {
|
|
16
|
+
case 'BUYER':
|
|
17
|
+
return await authorizeBuyerAction(userId, resourceId, resource, action, context);
|
|
18
|
+
case 'BACKOFFICE_ADMIN':
|
|
19
|
+
case 'ADMIN':
|
|
20
|
+
return { authorized: true, accessLevel: 'FULL', role: 'ADMIN', isAdmin: true, permissions: ['READ', 'CREATE', 'UPDATE', 'DELETE', 'MANAGE_USERS'] };
|
|
21
|
+
case 'BACKOFFICE_USER':
|
|
22
|
+
case 'USER':
|
|
23
|
+
if (!user) return { authorized: false, error: 'USER_OBJECT_REQUIRED' };
|
|
24
|
+
return await authorizeBackofficeAction(user, resourceId, resource, action);
|
|
25
|
+
default:
|
|
26
|
+
return { authorized: false, error: 'UNKNOWN_USER_TYPE' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapHttpMethodToAction(method) {
|
|
31
|
+
return { 'GET': 'READ', 'POST': 'CREATE', 'PUT': 'UPDATE', 'PATCH': 'UPDATE', 'DELETE': 'DELETE' }[method] || 'READ';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withAuthorization(handler, config) {
|
|
35
|
+
return async (event, context) => {
|
|
36
|
+
const { userType, resource, getResourceId, getUser } = config;
|
|
37
|
+
const userId = event.requestContext?.authorizer?.claims?.sub;
|
|
38
|
+
|
|
39
|
+
if (!userId) return { statusCode: 401, body: JSON.stringify({ error: 'UNAUTHORIZED' }), headers: { 'Access-Control-Allow-Origin': '*' } };
|
|
40
|
+
|
|
41
|
+
const resourceId = getResourceId ? getResourceId(event) : null;
|
|
42
|
+
let user = null;
|
|
43
|
+
if (getUser) {
|
|
44
|
+
user = await getUser(userId);
|
|
45
|
+
if (!user) return { statusCode: 401, body: JSON.stringify({ error: 'UNAUTHORIZED' }), headers: { 'Access-Control-Allow-Origin': '*' } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const action = mapHttpMethodToAction(event.httpMethod || event.requestContext?.http?.method);
|
|
49
|
+
const authResult = await authorize({ userId, userType: userType || user?.userType, resource, action, resourceId, user });
|
|
50
|
+
|
|
51
|
+
if (!authResult.authorized) return { statusCode: 403, body: JSON.stringify({ error: 'FORBIDDEN', message: authResult.message }), headers: { 'Access-Control-Allow-Origin': '*' } };
|
|
52
|
+
|
|
53
|
+
event.authContext = authResult;
|
|
54
|
+
return handler(event, context);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
authorize,
|
|
60
|
+
withAuthorization,
|
|
61
|
+
mapHttpMethodToAction,
|
|
62
|
+
authorizeBuyer,
|
|
63
|
+
getBuyerAccessibleApartments,
|
|
64
|
+
validateBuyerAccess,
|
|
65
|
+
authorizeBuyerAction,
|
|
66
|
+
authorizeBackofficeProject,
|
|
67
|
+
getAccessibleProjects,
|
|
68
|
+
canManageUsers,
|
|
69
|
+
authorizeUserManagement,
|
|
70
|
+
validateUserAccessToProject,
|
|
71
|
+
authorizeBackofficeAction,
|
|
72
|
+
ROLES,
|
|
73
|
+
UserType,
|
|
74
|
+
BackofficeRole,
|
|
75
|
+
Actions,
|
|
76
|
+
Resources,
|
|
77
|
+
getRoleById,
|
|
78
|
+
roleHasPermission,
|
|
79
|
+
getRolePermissions,
|
|
80
|
+
getRoles,
|
|
81
|
+
getRole,
|
|
82
|
+
hasPermission,
|
|
83
|
+
invalidateCache,
|
|
84
|
+
invalidateRolesCache,
|
|
85
|
+
getCacheStats
|
|
86
|
+
};
|
|
87
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roles & Permissions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const UserType = { BACKOFFICE_ADMIN: 'BACKOFFICE_ADMIN', BACKOFFICE_USER: 'BACKOFFICE_USER', BUYER: 'BUYER' };
|
|
6
|
+
const BackofficeRole = { ADMIN: 'ADMIN', USER: 'USER' };
|
|
7
|
+
const Actions = { READ: 'READ', CREATE: 'CREATE', UPDATE: 'UPDATE', DELETE: 'DELETE', MANAGE: 'MANAGE' };
|
|
8
|
+
const Resources = { PROJECT: 'project', APARTMENT: 'apartment', PAYMENT: 'payment', VOUCHER: 'voucher', CUSTOMER: 'customer', DOCUMENT: 'document', USER: 'user', INSTITUTION: 'institution' };
|
|
9
|
+
|
|
10
|
+
const ROLES = {
|
|
11
|
+
BACKOFFICE_ADMIN: {
|
|
12
|
+
id: 'BACKOFFICE_ADMIN',
|
|
13
|
+
name: 'System Administrator',
|
|
14
|
+
userType: UserType.BACKOFFICE_ADMIN,
|
|
15
|
+
permissions: [
|
|
16
|
+
{ resource: Resources.PROJECT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
17
|
+
{ resource: Resources.APARTMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
18
|
+
{ resource: Resources.PAYMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
19
|
+
{ resource: Resources.CUSTOMER, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
20
|
+
{ resource: Resources.DOCUMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
21
|
+
{ resource: Resources.USER, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE, Actions.MANAGE] }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
BACKOFFICE_USER: {
|
|
25
|
+
id: 'BACKOFFICE_USER',
|
|
26
|
+
name: 'Organization User',
|
|
27
|
+
userType: UserType.BACKOFFICE_USER,
|
|
28
|
+
permissions: [
|
|
29
|
+
{ resource: Resources.PROJECT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE] },
|
|
30
|
+
{ resource: Resources.APARTMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE] },
|
|
31
|
+
{ resource: Resources.PAYMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] },
|
|
32
|
+
{ resource: Resources.CUSTOMER, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE] },
|
|
33
|
+
{ resource: Resources.DOCUMENT, actions: [Actions.READ, Actions.CREATE, Actions.UPDATE, Actions.DELETE] }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
BUYER: {
|
|
37
|
+
id: 'BUYER',
|
|
38
|
+
name: 'Apartment Buyer',
|
|
39
|
+
userType: UserType.BUYER,
|
|
40
|
+
permissions: [
|
|
41
|
+
{ resource: Resources.APARTMENT, actions: [Actions.READ] },
|
|
42
|
+
{ resource: Resources.PAYMENT, actions: [Actions.READ] },
|
|
43
|
+
{ resource: Resources.DOCUMENT, actions: [Actions.READ] },
|
|
44
|
+
{ resource: Resources.CUSTOMER, actions: [Actions.READ, Actions.UPDATE] }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ROLE_ALIASES = { 'ADMIN': 'BACKOFFICE_ADMIN', 'USER': 'BACKOFFICE_USER' };
|
|
50
|
+
|
|
51
|
+
function getRoleById(roleId) {
|
|
52
|
+
return ROLES[ROLE_ALIASES[roleId] || roleId] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function roleHasPermission(roleId, resource, action) {
|
|
56
|
+
const role = getRoleById(roleId);
|
|
57
|
+
if (!role) return false;
|
|
58
|
+
const perm = role.permissions.find(p => p.resource === resource);
|
|
59
|
+
return perm ? perm.actions.includes(action) : false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getRolePermissions(roleId) {
|
|
63
|
+
const role = getRoleById(roleId);
|
|
64
|
+
if (!role) return [];
|
|
65
|
+
const permissions = [];
|
|
66
|
+
for (const perm of role.permissions) {
|
|
67
|
+
for (const action of perm.actions) {
|
|
68
|
+
permissions.push(`${perm.resource}:${action.toLowerCase()}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return permissions;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { UserType, BackofficeRole, Actions, Resources, ROLES, ROLE_ALIASES, getRoleById, roleHasPermission, getRolePermissions };
|
|
75
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roles Loader with Caching
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const AWS = require('aws-sdk');
|
|
6
|
+
const { ROLES, getRoleById, roleHasPermission } = require('./roles');
|
|
7
|
+
|
|
8
|
+
const ROLES_TABLE = process.env.ROLES_TABLE || 'w3home-roles';
|
|
9
|
+
const CONFIG_TABLE = process.env.CONFIG_TABLE || 'w3home-config';
|
|
10
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
11
|
+
const VERSION_CHECK_INTERVAL = 30 * 1000;
|
|
12
|
+
|
|
13
|
+
const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
14
|
+
|
|
15
|
+
let rolesCache = null;
|
|
16
|
+
let rolesCacheExpiry = 0;
|
|
17
|
+
let permissionsMap = null;
|
|
18
|
+
let cachedVersion = 0;
|
|
19
|
+
let lastVersionCheck = 0;
|
|
20
|
+
|
|
21
|
+
async function loadRolesFromDB() {
|
|
22
|
+
try {
|
|
23
|
+
const result = await dynamodb.scan({
|
|
24
|
+
TableName: ROLES_TABLE,
|
|
25
|
+
FilterExpression: 'SK = :sk',
|
|
26
|
+
ExpressionAttributeValues: { ':sk': 'METADATA' }
|
|
27
|
+
}).promise();
|
|
28
|
+
|
|
29
|
+
const roles = {};
|
|
30
|
+
for (const item of (result.Items || [])) {
|
|
31
|
+
roles[item.id] = item;
|
|
32
|
+
}
|
|
33
|
+
return Object.keys(roles).length > 0 ? roles : ROLES;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error loading roles:', error.message);
|
|
36
|
+
return ROLES;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildPermissionsMap(roles) {
|
|
41
|
+
const map = {};
|
|
42
|
+
for (const [roleId, role] of Object.entries(roles)) {
|
|
43
|
+
if (!role.permissions) continue;
|
|
44
|
+
for (const permission of role.permissions) {
|
|
45
|
+
if (typeof permission === 'string') {
|
|
46
|
+
if (permission.endsWith(':*')) {
|
|
47
|
+
const resource = permission.replace(':*', '');
|
|
48
|
+
['read', 'create', 'update', 'delete'].forEach(action => { map[`${roleId}:${resource}:${action}`] = true; });
|
|
49
|
+
} else {
|
|
50
|
+
map[`${roleId}:${permission}`] = true;
|
|
51
|
+
}
|
|
52
|
+
} else if (permission.resource && permission.actions) {
|
|
53
|
+
for (const action of permission.actions) {
|
|
54
|
+
map[`${roleId}:${permission.resource}:${action.toLowerCase()}`] = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getCurrentVersion() {
|
|
63
|
+
try {
|
|
64
|
+
const result = await dynamodb.get({
|
|
65
|
+
TableName: CONFIG_TABLE,
|
|
66
|
+
Key: { PK: 'CONFIG', SK: 'ROLES_VERSION' }
|
|
67
|
+
}).promise();
|
|
68
|
+
return result.Item?.version || 0;
|
|
69
|
+
} catch { return cachedVersion; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getRoles() {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
if (rolesCache && now - lastVersionCheck > VERSION_CHECK_INTERVAL) {
|
|
76
|
+
try {
|
|
77
|
+
const currentVersion = await getCurrentVersion();
|
|
78
|
+
lastVersionCheck = now;
|
|
79
|
+
if (currentVersion !== cachedVersion && currentVersion > 0) {
|
|
80
|
+
rolesCache = null;
|
|
81
|
+
permissionsMap = null;
|
|
82
|
+
cachedVersion = currentVersion;
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (rolesCache && now < rolesCacheExpiry) return rolesCache;
|
|
88
|
+
|
|
89
|
+
rolesCache = await loadRolesFromDB();
|
|
90
|
+
permissionsMap = buildPermissionsMap(rolesCache);
|
|
91
|
+
rolesCacheExpiry = now + CACHE_TTL;
|
|
92
|
+
|
|
93
|
+
return rolesCache;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getRole(roleId) {
|
|
97
|
+
const roles = await getRoles();
|
|
98
|
+
return roles[roleId] || getRoleById(roleId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function hasPermission(roleId, resource, action) {
|
|
102
|
+
if (!permissionsMap) await getRoles();
|
|
103
|
+
const key = `${roleId}:${resource}:${action.toLowerCase()}`;
|
|
104
|
+
return permissionsMap[key] === true || roleHasPermission(roleId, resource, action);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function invalidateCache() {
|
|
108
|
+
rolesCache = null;
|
|
109
|
+
permissionsMap = null;
|
|
110
|
+
rolesCacheExpiry = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function invalidateRolesCache() {
|
|
114
|
+
try {
|
|
115
|
+
await dynamodb.update({
|
|
116
|
+
TableName: CONFIG_TABLE,
|
|
117
|
+
Key: { PK: 'CONFIG', SK: 'ROLES_VERSION' },
|
|
118
|
+
UpdateExpression: 'ADD version :inc',
|
|
119
|
+
ExpressionAttributeValues: { ':inc': 1 }
|
|
120
|
+
}).promise();
|
|
121
|
+
} catch {}
|
|
122
|
+
invalidateCache();
|
|
123
|
+
cachedVersion = 0;
|
|
124
|
+
lastVersionCheck = 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getCacheStats() {
|
|
128
|
+
return {
|
|
129
|
+
hasCachedRoles: !!rolesCache,
|
|
130
|
+
cachedRolesCount: rolesCache ? Object.keys(rolesCache).length : 0,
|
|
131
|
+
cacheExpiry: rolesCacheExpiry,
|
|
132
|
+
cachedVersion,
|
|
133
|
+
hasPermissionsMap: !!permissionsMap
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { getRoles, getRole, hasPermission, invalidateCache, invalidateRolesCache, getCacheStats, CACHE_TTL, VERSION_CHECK_INTERVAL };
|
|
138
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* w3home-utils - Common Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
decodeIdToken,
|
|
7
|
+
getIndetifiers,
|
|
8
|
+
getUser,
|
|
9
|
+
getUserType,
|
|
10
|
+
clearUserCache,
|
|
11
|
+
getUserInstitutionId,
|
|
12
|
+
getUserInstitutionIds
|
|
13
|
+
} = require('./authUtils');
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
authorize,
|
|
17
|
+
withAuthorization,
|
|
18
|
+
mapHttpMethodToAction,
|
|
19
|
+
authorizeBuyer,
|
|
20
|
+
getBuyerAccessibleApartments,
|
|
21
|
+
validateBuyerAccess,
|
|
22
|
+
authorizeBuyerAction,
|
|
23
|
+
authorizeBackofficeProject,
|
|
24
|
+
getAccessibleProjects,
|
|
25
|
+
canManageUsers,
|
|
26
|
+
authorizeUserManagement,
|
|
27
|
+
validateUserAccessToProject,
|
|
28
|
+
authorizeBackofficeAction,
|
|
29
|
+
ROLES,
|
|
30
|
+
UserType,
|
|
31
|
+
BackofficeRole,
|
|
32
|
+
Actions,
|
|
33
|
+
Resources,
|
|
34
|
+
getRoleById,
|
|
35
|
+
roleHasPermission,
|
|
36
|
+
getRolePermissions,
|
|
37
|
+
getRoles,
|
|
38
|
+
getRole,
|
|
39
|
+
hasPermission,
|
|
40
|
+
invalidateCache,
|
|
41
|
+
invalidateRolesCache,
|
|
42
|
+
getCacheStats
|
|
43
|
+
} = require('./authorization');
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
logActivity,
|
|
47
|
+
logPostActivity,
|
|
48
|
+
logReadActivity,
|
|
49
|
+
withActivityLogging,
|
|
50
|
+
createResourceLogger,
|
|
51
|
+
LOG_PREFIX
|
|
52
|
+
} = require('./activityLogger');
|
|
53
|
+
|
|
54
|
+
const corsHeaders = {
|
|
55
|
+
'Access-Control-Allow-Origin': '*',
|
|
56
|
+
'Access-Control-Allow-Credentials': true,
|
|
57
|
+
'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token',
|
|
58
|
+
'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
// Auth
|
|
63
|
+
decodeIdToken,
|
|
64
|
+
getIndetifiers,
|
|
65
|
+
getUser,
|
|
66
|
+
getUserType,
|
|
67
|
+
clearUserCache,
|
|
68
|
+
getUserInstitutionId,
|
|
69
|
+
getUserInstitutionIds,
|
|
70
|
+
|
|
71
|
+
// Authorization
|
|
72
|
+
authorize,
|
|
73
|
+
withAuthorization,
|
|
74
|
+
mapHttpMethodToAction,
|
|
75
|
+
authorizeBuyer,
|
|
76
|
+
getBuyerAccessibleApartments,
|
|
77
|
+
validateBuyerAccess,
|
|
78
|
+
authorizeBuyerAction,
|
|
79
|
+
authorizeBackofficeProject,
|
|
80
|
+
getAccessibleProjects,
|
|
81
|
+
canManageUsers,
|
|
82
|
+
authorizeUserManagement,
|
|
83
|
+
validateUserAccessToProject,
|
|
84
|
+
authorizeBackofficeAction,
|
|
85
|
+
ROLES,
|
|
86
|
+
UserType,
|
|
87
|
+
BackofficeRole,
|
|
88
|
+
Actions,
|
|
89
|
+
Resources,
|
|
90
|
+
getRoleById,
|
|
91
|
+
roleHasPermission,
|
|
92
|
+
getRolePermissions,
|
|
93
|
+
getRoles,
|
|
94
|
+
getRole,
|
|
95
|
+
hasPermission,
|
|
96
|
+
invalidateCache,
|
|
97
|
+
invalidateRolesCache,
|
|
98
|
+
getCacheStats,
|
|
99
|
+
|
|
100
|
+
// Activity Logging
|
|
101
|
+
logActivity,
|
|
102
|
+
logPostActivity,
|
|
103
|
+
logReadActivity,
|
|
104
|
+
withActivityLogging,
|
|
105
|
+
createResourceLogger,
|
|
106
|
+
LOG_PREFIX,
|
|
107
|
+
|
|
108
|
+
// Common
|
|
109
|
+
corsHeaders
|
|
110
|
+
};
|
|
111
|
+
|