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 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
+