@friggframework/core 2.0.0-next.41 → 2.0.0-next.42
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/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { TokenRepositoryMongo } = require('./token-repository-mongo');
|
|
2
|
+
const { TokenRepositoryPostgres } = require('./token-repository-postgres');
|
|
3
|
+
const config = require('../../database/config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token Repository Factory
|
|
7
|
+
* Creates the appropriate repository adapter based on database type
|
|
8
|
+
*
|
|
9
|
+
* @returns {TokenRepositoryInterface} Configured repository adapter
|
|
10
|
+
*/
|
|
11
|
+
function createTokenRepository() {
|
|
12
|
+
const dbType = config.DB_TYPE;
|
|
13
|
+
|
|
14
|
+
switch (dbType) {
|
|
15
|
+
case 'mongodb':
|
|
16
|
+
return new TokenRepositoryMongo();
|
|
17
|
+
|
|
18
|
+
case 'postgresql':
|
|
19
|
+
return new TokenRepositoryPostgres();
|
|
20
|
+
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
createTokenRepository,
|
|
30
|
+
// Export adapters for direct testing
|
|
31
|
+
TokenRepositoryMongo,
|
|
32
|
+
TokenRepositoryPostgres,
|
|
33
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Repository Interface
|
|
3
|
+
* Abstract base class defining the contract for token persistence adapters
|
|
4
|
+
*
|
|
5
|
+
* This follows the Port in Hexagonal Architecture:
|
|
6
|
+
* - Domain layer depends on this abstraction
|
|
7
|
+
* - Concrete adapters implement this interface
|
|
8
|
+
* - Use cases receive repositories via dependency injection
|
|
9
|
+
*
|
|
10
|
+
* Note: Currently, Token model has identical structure across MongoDB and PostgreSQL,
|
|
11
|
+
* so TokenRepository serves both. This interface exists for consistency and
|
|
12
|
+
* future-proofing if database-specific implementations become needed.
|
|
13
|
+
*
|
|
14
|
+
* @abstract
|
|
15
|
+
*/
|
|
16
|
+
class TokenRepositoryInterface {
|
|
17
|
+
/**
|
|
18
|
+
* Create token with expiration
|
|
19
|
+
*
|
|
20
|
+
* @param {string|number} userId - User ID
|
|
21
|
+
* @param {string} rawToken - Raw unhashed token
|
|
22
|
+
* @param {number} minutes - Minutes until expiration
|
|
23
|
+
* @returns {Promise<Object>} Created token object
|
|
24
|
+
* @abstract
|
|
25
|
+
*/
|
|
26
|
+
async createTokenWithExpire(userId, rawToken, minutes) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Method createTokenWithExpire must be implemented by subclass'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate and get token
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} tokenObj - Token object with id and token
|
|
36
|
+
* @returns {Promise<Object>} Validated token object
|
|
37
|
+
* @abstract
|
|
38
|
+
*/
|
|
39
|
+
async validateAndGetToken(tokenObj) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Method validateAndGetToken must be implemented by subclass'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find token by ID
|
|
47
|
+
*
|
|
48
|
+
* @param {string|number} tokenId - Token ID
|
|
49
|
+
* @returns {Promise<Object|null>} Token object or null
|
|
50
|
+
* @abstract
|
|
51
|
+
*/
|
|
52
|
+
async findTokenById(tokenId) {
|
|
53
|
+
throw new Error('Method findTokenById must be implemented by subclass');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find all tokens for a user
|
|
58
|
+
*
|
|
59
|
+
* @param {string|number} userId - User ID
|
|
60
|
+
* @returns {Promise<Array>} Array of token objects
|
|
61
|
+
* @abstract
|
|
62
|
+
*/
|
|
63
|
+
async findTokensByUserId(userId) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Method findTokensByUserId must be implemented by subclass'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delete token by ID
|
|
71
|
+
*
|
|
72
|
+
* @param {string|number} tokenId - Token ID
|
|
73
|
+
* @returns {Promise<boolean>} True if deleted
|
|
74
|
+
* @abstract
|
|
75
|
+
*/
|
|
76
|
+
async deleteToken(tokenId) {
|
|
77
|
+
throw new Error('Method deleteToken must be implemented by subclass');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete expired tokens
|
|
82
|
+
*
|
|
83
|
+
* @returns {Promise<number>} Number of deleted tokens
|
|
84
|
+
* @abstract
|
|
85
|
+
*/
|
|
86
|
+
async deleteExpiredTokens() {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Method deleteExpiredTokens must be implemented by subclass'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delete all tokens for a user
|
|
94
|
+
*
|
|
95
|
+
* @param {string|number} userId - User ID
|
|
96
|
+
* @returns {Promise<number>} Number of deleted tokens
|
|
97
|
+
* @abstract
|
|
98
|
+
*/
|
|
99
|
+
async deleteTokensByUserId(userId) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'Method deleteTokensByUserId must be implemented by subclass'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create base64 buffer token
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} token - Token object
|
|
109
|
+
* @param {string} rawToken - Raw token
|
|
110
|
+
* @returns {string} Base64 encoded token
|
|
111
|
+
*/
|
|
112
|
+
createBase64BufferToken(token, rawToken) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Method createBase64BufferToken must be implemented by subclass'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get JSON token from base64 buffer token
|
|
120
|
+
*
|
|
121
|
+
* @param {string} base64Token - Base64 encoded token
|
|
122
|
+
* @returns {Object} Decoded token object
|
|
123
|
+
*/
|
|
124
|
+
getJSONTokenFromBase64BufferToken(base64Token) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
'Method getJSONTokenFromBase64BufferToken must be implemented by subclass'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { TokenRepositoryInterface };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const { TokenRepositoryInterface } = require('./token-repository-interface');
|
|
4
|
+
|
|
5
|
+
const BCRYPT_ROUNDS = 10;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MongoDB Token Repository Adapter
|
|
9
|
+
* Handles persistence of authentication tokens with bcrypt hashing
|
|
10
|
+
*
|
|
11
|
+
* MongoDB-specific characteristics:
|
|
12
|
+
* - Uses String IDs (ObjectId)
|
|
13
|
+
* - No ID conversion needed (IDs are already strings)
|
|
14
|
+
* - Bcrypt hashing handled in repository layer
|
|
15
|
+
*/
|
|
16
|
+
class TokenRepositoryMongo extends TokenRepositoryInterface {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.prisma = prisma;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a token with expiration
|
|
24
|
+
* Replaces: Token.createTokenWithExpire(userId, rawToken, minutes)
|
|
25
|
+
*
|
|
26
|
+
* @param {string} userId - The user ID
|
|
27
|
+
* @param {string} rawToken - The raw (unhashed) token string
|
|
28
|
+
* @param {number} minutes - Minutes until expiration
|
|
29
|
+
* @returns {Promise<Object>} The created token record with string IDs
|
|
30
|
+
*/
|
|
31
|
+
async createTokenWithExpire(userId, rawToken, minutes) {
|
|
32
|
+
// Hash the token with bcrypt
|
|
33
|
+
const tokenHash = await bcrypt.hash(rawToken, BCRYPT_ROUNDS);
|
|
34
|
+
|
|
35
|
+
// Calculate expiration time
|
|
36
|
+
const expires = new Date(Date.now() + minutes * 60000);
|
|
37
|
+
|
|
38
|
+
return await this.prisma.token.create({
|
|
39
|
+
data: {
|
|
40
|
+
token: tokenHash,
|
|
41
|
+
expires,
|
|
42
|
+
userId,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate and retrieve token from JSON token object
|
|
49
|
+
* Replaces: Token.validateAndGetTokenFromJSONToken(tokenObj)
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} tokenObj - Object with id and token properties
|
|
52
|
+
* @returns {Promise<Object>} The validated token record with string IDs
|
|
53
|
+
* @throws {Error} If token is invalid, expired, or doesn't exist
|
|
54
|
+
*/
|
|
55
|
+
async validateAndGetToken(tokenObj) {
|
|
56
|
+
const sessionToken = await this.prisma.token.findUnique({
|
|
57
|
+
where: { id: tokenObj.id },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!sessionToken) {
|
|
61
|
+
throw new Error('Invalid Token: Token does not exist');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify token hash matches
|
|
65
|
+
const isValid = await bcrypt.compare(
|
|
66
|
+
tokenObj.token,
|
|
67
|
+
sessionToken.token
|
|
68
|
+
);
|
|
69
|
+
if (!isValid) {
|
|
70
|
+
throw new Error('Invalid Token: Token does not match');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if token is expired
|
|
74
|
+
if (
|
|
75
|
+
sessionToken.expires &&
|
|
76
|
+
new Date(sessionToken.expires) < new Date()
|
|
77
|
+
) {
|
|
78
|
+
throw new Error('Invalid Token: Token is expired');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return sessionToken;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find a token by ID
|
|
86
|
+
* Replaces: Token.findById(tokenId)
|
|
87
|
+
*
|
|
88
|
+
* @param {string} tokenId - The token ID
|
|
89
|
+
* @returns {Promise<Object|null>} The token record with string IDs or null
|
|
90
|
+
*/
|
|
91
|
+
async findTokenById(tokenId) {
|
|
92
|
+
return await this.prisma.token.findUnique({
|
|
93
|
+
where: { id: tokenId },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Find tokens by user ID
|
|
99
|
+
* Replaces: Token.find({ user: userId })
|
|
100
|
+
*
|
|
101
|
+
* @param {string} userId - The user ID
|
|
102
|
+
* @returns {Promise<Array>} Array of token records with string IDs
|
|
103
|
+
*/
|
|
104
|
+
async findTokensByUserId(userId) {
|
|
105
|
+
return await this.prisma.token.findMany({
|
|
106
|
+
where: { userId },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete a token by ID
|
|
112
|
+
* Replaces: Token.deleteOne({ _id: tokenId })
|
|
113
|
+
*
|
|
114
|
+
* @param {string} tokenId - The token ID
|
|
115
|
+
* @returns {Promise<Object>} The deletion result
|
|
116
|
+
*/
|
|
117
|
+
async deleteToken(tokenId) {
|
|
118
|
+
try {
|
|
119
|
+
await this.prisma.token.delete({
|
|
120
|
+
where: { id: tokenId },
|
|
121
|
+
});
|
|
122
|
+
return { acknowledged: true, deletedCount: 1 };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error.code === 'P2025') {
|
|
125
|
+
// Record not found
|
|
126
|
+
return { acknowledged: true, deletedCount: 0 };
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete expired tokens
|
|
134
|
+
* Replaces: Token.deleteMany({ expires: { $lt: new Date() } })
|
|
135
|
+
*
|
|
136
|
+
* @returns {Promise<Object>} The deletion result with count
|
|
137
|
+
*/
|
|
138
|
+
async deleteExpiredTokens() {
|
|
139
|
+
const result = await this.prisma.token.deleteMany({
|
|
140
|
+
where: {
|
|
141
|
+
expires: {
|
|
142
|
+
lt: new Date(),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
acknowledged: true,
|
|
149
|
+
deletedCount: result.count,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Delete all tokens for a user
|
|
155
|
+
* Replaces: Token.deleteMany({ user: userId })
|
|
156
|
+
*
|
|
157
|
+
* @param {string} userId - The user ID
|
|
158
|
+
* @returns {Promise<Object>} The deletion result
|
|
159
|
+
*/
|
|
160
|
+
async deleteTokensByUserId(userId) {
|
|
161
|
+
const result = await this.prisma.token.deleteMany({
|
|
162
|
+
where: { userId },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
acknowledged: true,
|
|
167
|
+
deletedCount: result.count,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create JSON token string from token object and raw token
|
|
173
|
+
* Replaces: Token.createJSONToken(token, rawToken)
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} token - The token record
|
|
176
|
+
* @param {string} rawToken - The raw token string
|
|
177
|
+
* @returns {string} JSON string with id and token
|
|
178
|
+
*/
|
|
179
|
+
createJSONToken(token, rawToken) {
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
id: token.id,
|
|
182
|
+
token: rawToken,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create base64 encoded buffer token
|
|
188
|
+
* Replaces: Token.createBase64BufferToken(token, rawToken)
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} token - The token record
|
|
191
|
+
* @param {string} rawToken - The raw token string
|
|
192
|
+
* @returns {string} Base64 encoded token
|
|
193
|
+
*/
|
|
194
|
+
createBase64BufferToken(token, rawToken) {
|
|
195
|
+
const jsonVal = this.createJSONToken(token, rawToken);
|
|
196
|
+
return Buffer.from(jsonVal).toString('base64');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse JSON token from base64 buffer
|
|
201
|
+
* Replaces: Token.getJSONTokenFromBase64BufferToken(buffer)
|
|
202
|
+
*
|
|
203
|
+
* @param {string} buffer - Base64 encoded token string
|
|
204
|
+
* @returns {Object} Parsed token object with id and token
|
|
205
|
+
*/
|
|
206
|
+
getJSONTokenFromBase64BufferToken(buffer) {
|
|
207
|
+
const tokenStr = Buffer.from(buffer.trim(), 'base64').toString('ascii');
|
|
208
|
+
return JSON.parse(tokenStr);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { TokenRepositoryMongo };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const { TokenRepositoryInterface } = require('./token-repository-interface');
|
|
4
|
+
|
|
5
|
+
const BCRYPT_ROUNDS = 10;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL Token Repository Adapter
|
|
9
|
+
* Handles persistence of authentication tokens with bcrypt hashing
|
|
10
|
+
*
|
|
11
|
+
* PostgreSQL-specific characteristics:
|
|
12
|
+
* - Uses Int IDs with autoincrement
|
|
13
|
+
* - Requires ID conversion: String (app layer) ↔ Int (database)
|
|
14
|
+
* - All returned IDs are converted to strings for application layer consistency
|
|
15
|
+
*/
|
|
16
|
+
class TokenRepositoryPostgres extends TokenRepositoryInterface {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.prisma = prisma;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert string ID to integer for PostgreSQL queries
|
|
24
|
+
* @private
|
|
25
|
+
* @param {string|number|null|undefined} id - ID to convert
|
|
26
|
+
* @returns {number|null|undefined} Integer ID or null/undefined
|
|
27
|
+
* @throws {Error} If ID cannot be converted to integer
|
|
28
|
+
*/
|
|
29
|
+
_convertId(id) {
|
|
30
|
+
if (id === null || id === undefined) return id;
|
|
31
|
+
const parsed = parseInt(id, 10);
|
|
32
|
+
if (isNaN(parsed)) {
|
|
33
|
+
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert token object IDs to strings
|
|
40
|
+
* @private
|
|
41
|
+
* @param {Object|null} token - Token object from database
|
|
42
|
+
* @returns {Object|null} Token with string IDs
|
|
43
|
+
*/
|
|
44
|
+
_convertTokenIds(token) {
|
|
45
|
+
if (!token) return token;
|
|
46
|
+
return {
|
|
47
|
+
...token,
|
|
48
|
+
id: token.id?.toString(),
|
|
49
|
+
userId: token.userId?.toString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a token with expiration
|
|
55
|
+
* Replaces: Token.createTokenWithExpire(userId, rawToken, minutes)
|
|
56
|
+
*
|
|
57
|
+
* @param {string} userId - The user ID (string from application layer)
|
|
58
|
+
* @param {string} rawToken - The raw (unhashed) token string
|
|
59
|
+
* @param {number} minutes - Minutes until expiration
|
|
60
|
+
* @returns {Promise<Object>} The created token record with string IDs
|
|
61
|
+
*/
|
|
62
|
+
async createTokenWithExpire(userId, rawToken, minutes) {
|
|
63
|
+
// Hash the token with bcrypt
|
|
64
|
+
const tokenHash = await bcrypt.hash(rawToken, BCRYPT_ROUNDS);
|
|
65
|
+
|
|
66
|
+
// Calculate expiration time
|
|
67
|
+
const expires = new Date(Date.now() + minutes * 60000);
|
|
68
|
+
|
|
69
|
+
const token = await this.prisma.token.create({
|
|
70
|
+
data: {
|
|
71
|
+
token: tokenHash,
|
|
72
|
+
expires,
|
|
73
|
+
userId: this._convertId(userId),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return this._convertTokenIds(token);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate and retrieve token from JSON token object
|
|
82
|
+
* Replaces: Token.validateAndGetTokenFromJSONToken(tokenObj)
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} tokenObj - Object with id and token properties (id as string from app layer)
|
|
85
|
+
* @returns {Promise<Object>} The validated token record with string IDs
|
|
86
|
+
* @throws {Error} If token is invalid, expired, or doesn't exist
|
|
87
|
+
*/
|
|
88
|
+
async validateAndGetToken(tokenObj) {
|
|
89
|
+
const intId = this._convertId(tokenObj.id);
|
|
90
|
+
const sessionToken = await this.prisma.token.findUnique({
|
|
91
|
+
where: { id: intId },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!sessionToken) {
|
|
95
|
+
throw new Error('Invalid Token: Token does not exist');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Verify token hash matches
|
|
99
|
+
const isValid = await bcrypt.compare(
|
|
100
|
+
tokenObj.token,
|
|
101
|
+
sessionToken.token
|
|
102
|
+
);
|
|
103
|
+
if (!isValid) {
|
|
104
|
+
throw new Error('Invalid Token: Token does not match');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if token is expired
|
|
108
|
+
if (
|
|
109
|
+
sessionToken.expires &&
|
|
110
|
+
new Date(sessionToken.expires) < new Date()
|
|
111
|
+
) {
|
|
112
|
+
throw new Error('Invalid Token: Token is expired');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this._convertTokenIds(sessionToken);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Find a token by ID
|
|
120
|
+
* Replaces: Token.findById(tokenId)
|
|
121
|
+
*
|
|
122
|
+
* @param {string} tokenId - The token ID (string from application layer)
|
|
123
|
+
* @returns {Promise<Object|null>} The token record with string IDs or null
|
|
124
|
+
*/
|
|
125
|
+
async findTokenById(tokenId) {
|
|
126
|
+
const intId = this._convertId(tokenId);
|
|
127
|
+
const token = await this.prisma.token.findUnique({
|
|
128
|
+
where: { id: intId },
|
|
129
|
+
});
|
|
130
|
+
return this._convertTokenIds(token);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Find tokens by user ID
|
|
135
|
+
* Replaces: Token.find({ user: userId })
|
|
136
|
+
*
|
|
137
|
+
* @param {string} userId - The user ID (string from application layer)
|
|
138
|
+
* @returns {Promise<Array>} Array of token records with string IDs
|
|
139
|
+
*/
|
|
140
|
+
async findTokensByUserId(userId) {
|
|
141
|
+
const intUserId = this._convertId(userId);
|
|
142
|
+
const tokens = await this.prisma.token.findMany({
|
|
143
|
+
where: { userId: intUserId },
|
|
144
|
+
});
|
|
145
|
+
return tokens.map((token) => this._convertTokenIds(token));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Delete a token by ID
|
|
150
|
+
* Replaces: Token.deleteOne({ _id: tokenId })
|
|
151
|
+
*
|
|
152
|
+
* @param {string} tokenId - The token ID (string from application layer)
|
|
153
|
+
* @returns {Promise<Object>} The deletion result
|
|
154
|
+
*/
|
|
155
|
+
async deleteToken(tokenId) {
|
|
156
|
+
try {
|
|
157
|
+
const intId = this._convertId(tokenId);
|
|
158
|
+
await this.prisma.token.delete({
|
|
159
|
+
where: { id: intId },
|
|
160
|
+
});
|
|
161
|
+
return { acknowledged: true, deletedCount: 1 };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.code === 'P2025') {
|
|
164
|
+
// Record not found
|
|
165
|
+
return { acknowledged: true, deletedCount: 0 };
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Delete expired tokens
|
|
173
|
+
* Replaces: Token.deleteMany({ expires: { $lt: new Date() } })
|
|
174
|
+
*
|
|
175
|
+
* @returns {Promise<Object>} The deletion result with count
|
|
176
|
+
*/
|
|
177
|
+
async deleteExpiredTokens() {
|
|
178
|
+
const result = await this.prisma.token.deleteMany({
|
|
179
|
+
where: {
|
|
180
|
+
expires: {
|
|
181
|
+
lt: new Date(),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
acknowledged: true,
|
|
188
|
+
deletedCount: result.count,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Delete all tokens for a user
|
|
194
|
+
* Replaces: Token.deleteMany({ user: userId })
|
|
195
|
+
*
|
|
196
|
+
* @param {string} userId - The user ID (string from application layer)
|
|
197
|
+
* @returns {Promise<Object>} The deletion result
|
|
198
|
+
*/
|
|
199
|
+
async deleteTokensByUserId(userId) {
|
|
200
|
+
const intUserId = this._convertId(userId);
|
|
201
|
+
const result = await this.prisma.token.deleteMany({
|
|
202
|
+
where: { userId: intUserId },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
acknowledged: true,
|
|
207
|
+
deletedCount: result.count,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create JSON token string from token object and raw token
|
|
213
|
+
* Replaces: Token.createJSONToken(token, rawToken)
|
|
214
|
+
*
|
|
215
|
+
* Note: Token ID is already a string at this point (from _convertTokenIds),
|
|
216
|
+
* so no conversion needed here.
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} token - The token record (with string IDs)
|
|
219
|
+
* @param {string} rawToken - The raw token string
|
|
220
|
+
* @returns {string} JSON string with id and token
|
|
221
|
+
*/
|
|
222
|
+
createJSONToken(token, rawToken) {
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
id: token.id,
|
|
225
|
+
token: rawToken,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create base64 encoded buffer token
|
|
231
|
+
* Replaces: Token.createBase64BufferToken(token, rawToken)
|
|
232
|
+
*
|
|
233
|
+
* @param {Object} token - The token record (with string IDs)
|
|
234
|
+
* @param {string} rawToken - The raw token string
|
|
235
|
+
* @returns {string} Base64 encoded token
|
|
236
|
+
*/
|
|
237
|
+
createBase64BufferToken(token, rawToken) {
|
|
238
|
+
const jsonVal = this.createJSONToken(token, rawToken);
|
|
239
|
+
return Buffer.from(jsonVal).toString('base64');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Parse JSON token from base64 buffer
|
|
244
|
+
* Replaces: Token.getJSONTokenFromBase64BufferToken(buffer)
|
|
245
|
+
*
|
|
246
|
+
* Note: Parsed token ID will be a string, which is correct for application layer
|
|
247
|
+
*
|
|
248
|
+
* @param {string} buffer - Base64 encoded token string
|
|
249
|
+
* @returns {Object} Parsed token object with id and token (id as string)
|
|
250
|
+
*/
|
|
251
|
+
getJSONTokenFromBase64BufferToken(buffer) {
|
|
252
|
+
const tokenStr = Buffer.from(buffer.trim(), 'base64').toString('ascii');
|
|
253
|
+
return JSON.parse(tokenStr);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = { TokenRepositoryPostgres };
|