@friggframework/core 2.0.0-next.41 → 2.0.0-next.43

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.
Files changed (197) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +27 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +122 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +318 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  134. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  135. package/prisma-postgresql/schema.prisma +300 -0
  136. package/syncs/manager.js +468 -443
  137. package/syncs/repositories/sync-repository-factory.js +38 -0
  138. package/syncs/repositories/sync-repository-interface.js +109 -0
  139. package/syncs/repositories/sync-repository-mongo.js +239 -0
  140. package/syncs/repositories/sync-repository-postgres.js +319 -0
  141. package/syncs/sync.js +0 -1
  142. package/token/repositories/token-repository-factory.js +33 -0
  143. package/token/repositories/token-repository-interface.js +131 -0
  144. package/token/repositories/token-repository-mongo.js +212 -0
  145. package/token/repositories/token-repository-postgres.js +257 -0
  146. package/token/repositories/token-repository.js +219 -0
  147. package/types/integrations/index.d.ts +2 -6
  148. package/types/module-plugin/index.d.ts +5 -57
  149. package/types/syncs/index.d.ts +0 -2
  150. package/user/repositories/user-repository-factory.js +46 -0
  151. package/user/repositories/user-repository-interface.js +198 -0
  152. package/user/repositories/user-repository-mongo.js +250 -0
  153. package/user/repositories/user-repository-postgres.js +311 -0
  154. package/user/tests/doubles/test-user-repository.js +72 -0
  155. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  156. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  157. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  158. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  159. package/user/tests/use-cases/login-user.test.js +140 -0
  160. package/user/use-cases/create-individual-user.js +61 -0
  161. package/user/use-cases/create-organization-user.js +47 -0
  162. package/user/use-cases/create-token-for-user-id.js +30 -0
  163. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  164. package/user/use-cases/login-user.js +122 -0
  165. package/user/user.js +77 -0
  166. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  167. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  168. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  169. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  170. package/websocket/repositories/websocket-connection-repository.js +160 -0
  171. package/database/models/State.js +0 -9
  172. package/database/models/Token.js +0 -70
  173. package/database/mongo.js +0 -171
  174. package/encrypt/Cryptor.test.js +0 -32
  175. package/encrypt/encrypt.js +0 -104
  176. package/encrypt/encrypt.test.js +0 -1069
  177. package/handlers/routers/middleware/loadUser.js +0 -15
  178. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  179. package/integrations/create-frigg-backend.js +0 -31
  180. package/integrations/integration-factory.js +0 -251
  181. package/integrations/integration-mapping.js +0 -43
  182. package/integrations/integration-model.js +0 -46
  183. package/integrations/integration-user.js +0 -144
  184. package/integrations/test/integration-base.test.js +0 -144
  185. package/module-plugin/auther.js +0 -393
  186. package/module-plugin/credential.js +0 -22
  187. package/module-plugin/entity-manager.js +0 -70
  188. package/module-plugin/manager.js +0 -169
  189. package/module-plugin/module-factory.js +0 -61
  190. package/module-plugin/test/auther.test.js +0 -97
  191. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  192. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  193. /package/{module-plugin → modules}/requester/basic.js +0 -0
  194. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.js +0 -0
  196. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  197. /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 };