@loomcore/api 0.1.57 → 0.1.59
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/dist/__tests__/common-test.utils.d.ts +2 -1
- package/dist/__tests__/common-test.utils.js +22 -22
- package/dist/__tests__/test-express-app.js +1 -1
- package/dist/databases/migrations/migration-runner.js +2 -0
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +26 -10
- package/dist/databases/mongo-db/utils/build-mongo-url.util.js +4 -4
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +25 -25
- package/dist/databases/postgres/utils/build-postgres-url.util.js +4 -4
- package/dist/middleware/is-authorized.js +1 -1
- package/dist/models/auth-config.interface.d.ts +12 -0
- package/dist/models/auth-config.interface.js +1 -0
- package/dist/models/base-api-config.interface.d.ts +19 -29
- package/dist/services/auth.service.d.ts +1 -0
- package/dist/services/auth.service.js +14 -10
- package/dist/services/email.service.js +13 -8
- package/dist/utils/express.utils.js +1 -1
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import { GenericApiService } from '../services/index.js';
|
|
|
7
7
|
import { IDatabase } from '../databases/models/index.js';
|
|
8
8
|
import { ICategory } from './models/category.model.js';
|
|
9
9
|
import { IProduct } from './models/product.model.js';
|
|
10
|
+
import { DbType } from '../databases/db-type.type.js';
|
|
10
11
|
declare function initialize(database: IDatabase): void;
|
|
11
12
|
declare function getRandomId(): string;
|
|
12
13
|
declare function isMongoDatabase(database: IDatabase): boolean;
|
|
@@ -28,7 +29,7 @@ export declare class CategoryService extends GenericApiService<ICategory> {
|
|
|
28
29
|
export declare class CategoryController extends ApiController<ICategory> {
|
|
29
30
|
constructor(app: Application, database: IDatabase);
|
|
30
31
|
}
|
|
31
|
-
export declare function setupTestConfig(isMultiTenant
|
|
32
|
+
export declare function setupTestConfig(isMultiTenant: boolean | undefined, dbType: DbType): void;
|
|
32
33
|
export declare class ProductService extends GenericApiService<IProduct> {
|
|
33
34
|
private db;
|
|
34
35
|
constructor(database: IDatabase);
|
|
@@ -174,47 +174,47 @@ export class CategoryController extends ApiController {
|
|
|
174
174
|
super('categories', app, categoryService, 'category', CategorySpec);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
export function setupTestConfig(isMultiTenant = true) {
|
|
177
|
+
export function setupTestConfig(isMultiTenant = true, dbType) {
|
|
178
178
|
setBaseApiConfig({
|
|
179
|
-
env: 'test',
|
|
180
|
-
hostName: 'localhost',
|
|
181
|
-
appName: 'test-app',
|
|
182
|
-
clientSecret: 'test-secret',
|
|
183
|
-
database: {
|
|
184
|
-
name: 'test-db',
|
|
185
|
-
},
|
|
186
|
-
externalPort: 4000,
|
|
187
|
-
internalPort: 8083,
|
|
188
|
-
corsAllowedOrigins: ['*'],
|
|
189
|
-
saltWorkFactor: 10,
|
|
190
|
-
jobTypes: '',
|
|
191
|
-
deployedBranch: '',
|
|
192
|
-
debug: {
|
|
193
|
-
showErrors: false
|
|
194
|
-
},
|
|
195
179
|
app: {
|
|
196
|
-
isMultiTenant: isMultiTenant,
|
|
180
|
+
isMultiTenant: isMultiTenant, name: 'test-app', dbType: dbType,
|
|
197
181
|
...(isMultiTenant && {
|
|
198
182
|
metaOrgName: 'Test Meta Organization',
|
|
199
183
|
metaOrgCode: 'TEST_META_ORG'
|
|
200
184
|
})
|
|
201
185
|
},
|
|
202
186
|
auth: {
|
|
187
|
+
adminUser: {
|
|
188
|
+
email: 'admin@test.com',
|
|
189
|
+
password: 'admin-password'
|
|
190
|
+
},
|
|
191
|
+
clientSecret: 'test-secret',
|
|
192
|
+
saltWorkFactor: 10,
|
|
203
193
|
jwtExpirationInSeconds: 3600,
|
|
204
194
|
refreshTokenExpirationInDays: 7,
|
|
205
195
|
deviceIdCookieMaxAgeInDays: 730,
|
|
206
196
|
passwordResetTokenExpirationInMinutes: 20
|
|
207
197
|
},
|
|
198
|
+
database: {
|
|
199
|
+
name: 'test-db',
|
|
200
|
+
host: 'localhost',
|
|
201
|
+
password: 'test-password',
|
|
202
|
+
port: 27017,
|
|
203
|
+
username: 'test-user'
|
|
204
|
+
},
|
|
205
|
+
env: 'test',
|
|
208
206
|
email: {
|
|
209
207
|
emailApiKey: 'WeDontHaveAKeyYet',
|
|
210
208
|
emailApiSecret: 'WeDontHaveASecretYet',
|
|
211
209
|
fromAddress: 'test@test.com',
|
|
212
210
|
systemEmailAddress: 'system@test.com'
|
|
213
211
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
network: {
|
|
213
|
+
hostName: 'localhost',
|
|
214
|
+
internalPort: 8083,
|
|
215
|
+
externalPort: 4000,
|
|
216
|
+
corsAllowedOrigins: ['*']
|
|
217
|
+
},
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
export class ProductService extends GenericApiService {
|
|
@@ -26,7 +26,7 @@ export class TestExpressApp {
|
|
|
26
26
|
return this.initPromise;
|
|
27
27
|
}
|
|
28
28
|
static async _performInit(useMongoDb) {
|
|
29
|
-
setupTestConfig();
|
|
29
|
+
setupTestConfig(true, useMongoDb ? 'mongodb' : 'postgres');
|
|
30
30
|
initializeTypeBox();
|
|
31
31
|
if (!this.database) {
|
|
32
32
|
if (useMongoDb) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Umzug, MongoDBStorage } from 'umzug';
|
|
2
2
|
import { Pool } from 'pg';
|
|
3
3
|
import { MongoClient } from 'mongodb';
|
|
4
|
+
import { setBaseApiConfig } from '../../config/index.js';
|
|
4
5
|
import fs from 'fs';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import { buildMongoUrl } from '../mongo-db/utils/build-mongo-url.util.js';
|
|
@@ -15,6 +16,7 @@ export class MigrationRunner {
|
|
|
15
16
|
primaryTimezone;
|
|
16
17
|
dbConnection;
|
|
17
18
|
constructor(config) {
|
|
19
|
+
setBaseApiConfig(config);
|
|
18
20
|
this.config = config;
|
|
19
21
|
this.dbType = config.app.dbType || 'mongodb';
|
|
20
22
|
this.dbUrl = this.dbType === 'postgres' ? buildPostgresUrl(config) : buildMongoUrl(config);
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext } from '@loomcore/common/models';
|
|
2
|
+
import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext, isSystemUserContextInitialized } from '@loomcore/common/models';
|
|
3
3
|
import { MongoDBDatabase } from '../mongo-db.database.js';
|
|
4
4
|
import { AuthService, OrganizationService } from '../../../services/index.js';
|
|
5
5
|
export const getMongoInitialSchema = (config) => {
|
|
6
6
|
const migrations = [];
|
|
7
7
|
const isMultiTenant = config.app.isMultiTenant === true;
|
|
8
|
+
console.log('📋 Migration Config Diagnostic:');
|
|
9
|
+
console.log(' isMultiTenant:', isMultiTenant);
|
|
10
|
+
console.log(' config.app.metaOrgName:', config.app.metaOrgName ?? '(undefined)');
|
|
11
|
+
console.log(' config.app.metaOrgCode:', config.app.metaOrgCode ?? '(undefined)');
|
|
12
|
+
console.log(' config.auth?.adminUser?.email:', config.auth?.adminUser?.email ?? '(undefined)');
|
|
13
|
+
console.log(' config.auth?.adminUser?.password:', config.auth?.adminUser?.password ? '(set)' : '(undefined)');
|
|
14
|
+
console.log(' Will add meta-org migration:', isMultiTenant && !!config.app.metaOrgName && !!config.app.metaOrgCode);
|
|
15
|
+
console.log(' Will add admin-user migration:', !!config.auth?.adminUser?.email && !!config.auth?.adminUser?.password);
|
|
8
16
|
if (isMultiTenant) {
|
|
9
17
|
migrations.push({
|
|
10
18
|
name: '00000000000001_schema-organizations',
|
|
@@ -146,39 +154,47 @@ export const getMongoInitialSchema = (config) => {
|
|
|
146
154
|
}
|
|
147
155
|
});
|
|
148
156
|
}
|
|
149
|
-
if (config.
|
|
157
|
+
if (config.auth && config.auth.adminUser) {
|
|
150
158
|
migrations.push({
|
|
151
159
|
name: '00000000000009_data-admin-user',
|
|
152
160
|
up: async ({ context: db }) => {
|
|
153
|
-
if (!config.adminUser?.email || !config.adminUser?.password) {
|
|
161
|
+
if (!config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
|
|
154
162
|
throw new Error('Admin user email and password must be provided in config');
|
|
155
163
|
}
|
|
156
164
|
const database = new MongoDBDatabase(db);
|
|
157
165
|
const authService = new AuthService(database);
|
|
166
|
+
if (isMultiTenant && !isSystemUserContextInitialized()) {
|
|
167
|
+
throw new Error('SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
|
|
168
|
+
'This migration only runs if config.app.metaOrgName and config.app.metaOrgCode are provided. ' +
|
|
169
|
+
'Please ensure both values are set in your config.');
|
|
170
|
+
}
|
|
171
|
+
if (!isMultiTenant && !isSystemUserContextInitialized()) {
|
|
172
|
+
initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', undefined);
|
|
173
|
+
}
|
|
158
174
|
const systemUserContext = getSystemUserContext();
|
|
159
175
|
const _id = randomUUID().toString();
|
|
160
176
|
await authService.createUser(systemUserContext, {
|
|
161
177
|
_id: _id,
|
|
162
178
|
_orgId: systemUserContext.organization?._id,
|
|
163
|
-
email: config.adminUser
|
|
164
|
-
password: config.adminUser
|
|
179
|
+
email: config.auth?.adminUser?.email,
|
|
180
|
+
password: config.auth?.adminUser?.password,
|
|
165
181
|
firstName: 'Admin',
|
|
166
182
|
lastName: 'User',
|
|
167
183
|
displayName: 'Admin User',
|
|
168
184
|
});
|
|
169
185
|
},
|
|
170
186
|
down: async ({ context: db }) => {
|
|
171
|
-
if (!config.adminUser?.email)
|
|
187
|
+
if (!config.auth?.adminUser?.email)
|
|
172
188
|
return;
|
|
173
|
-
await db.collection('users').deleteOne({ email: config.adminUser
|
|
189
|
+
await db.collection('users').deleteOne({ email: config.auth?.adminUser?.email });
|
|
174
190
|
}
|
|
175
191
|
});
|
|
176
192
|
}
|
|
177
|
-
if (config.adminUser?.email && isMultiTenant) {
|
|
193
|
+
if (config.auth?.adminUser?.email && isMultiTenant) {
|
|
178
194
|
migrations.push({
|
|
179
195
|
name: '00000000000010_data-admin-authorizations',
|
|
180
196
|
up: async ({ context: db }) => {
|
|
181
|
-
if (!config.adminUser?.email) {
|
|
197
|
+
if (!config.auth?.adminUser?.email) {
|
|
182
198
|
throw new Error('Admin user email not found in config');
|
|
183
199
|
}
|
|
184
200
|
const database = new MongoDBDatabase(db);
|
|
@@ -188,7 +204,7 @@ export const getMongoInitialSchema = (config) => {
|
|
|
188
204
|
if (!metaOrg) {
|
|
189
205
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
190
206
|
}
|
|
191
|
-
const adminUser = await authService.getUserByEmail(config.adminUser
|
|
207
|
+
const adminUser = await authService.getUserByEmail(config.auth?.adminUser?.email);
|
|
192
208
|
if (!adminUser) {
|
|
193
209
|
throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
|
|
194
210
|
}
|
|
@@ -3,11 +3,11 @@ export function buildMongoUrl(config) {
|
|
|
3
3
|
if (!database) {
|
|
4
4
|
throw new Error("Database configuration is required to build the MongoDB URL.");
|
|
5
5
|
}
|
|
6
|
-
const {
|
|
7
|
-
if (!
|
|
6
|
+
const { username, password, host, port, name } = database;
|
|
7
|
+
if (!username || !password || !host || !port || !name) {
|
|
8
8
|
throw new Error("Database configuration must include user, password, host, port, and name to build the MongoDB URL.");
|
|
9
9
|
}
|
|
10
|
-
const
|
|
10
|
+
const encodedUsername = encodeURIComponent(username);
|
|
11
11
|
const encodedPassword = encodeURIComponent(password);
|
|
12
|
-
return `mongodb://${
|
|
12
|
+
return `mongodb://${encodedUsername}:${encodedPassword}@${host}:${port}/${name}`;
|
|
13
13
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext } from '@loomcore/common/models';
|
|
1
|
+
import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext, isSystemUserContextInitialized } from '@loomcore/common/models';
|
|
2
2
|
import { PostgresDatabase } from '../postgres.database.js';
|
|
3
3
|
import { AuthService, OrganizationService } from '../../../services/index.js';
|
|
4
4
|
export const getPostgresInitialSchema = (config) => {
|
|
5
5
|
const migrations = [];
|
|
6
6
|
const isMultiTenant = config.app.isMultiTenant === true;
|
|
7
|
+
console.log('📋 Migration Config Diagnostic:');
|
|
8
|
+
console.log(' isMultiTenant:', isMultiTenant);
|
|
9
|
+
console.log(' config.app.metaOrgName:', config.app.metaOrgName ?? '(undefined)');
|
|
10
|
+
console.log(' config.app.metaOrgCode:', config.app.metaOrgCode ?? '(undefined)');
|
|
11
|
+
console.log(' config.auth?.adminUser?.email:', config.auth?.adminUser?.email ?? '(undefined)');
|
|
12
|
+
console.log(' config.auth?.adminUser?.password:', config.auth?.adminUser?.password ? '(set)' : '(undefined)');
|
|
13
|
+
console.log(' Will add meta-org migration:', isMultiTenant && !!config.app.metaOrgName && !!config.app.metaOrgCode);
|
|
14
|
+
console.log(' Will add admin-user migration:', !!config.auth?.adminUser?.email && !!config.auth?.adminUser?.password);
|
|
7
15
|
if (isMultiTenant) {
|
|
8
16
|
migrations.push({
|
|
9
17
|
name: '00000000000001_schema-organizations',
|
|
@@ -207,37 +215,29 @@ export const getPostgresInitialSchema = (config) => {
|
|
|
207
215
|
}
|
|
208
216
|
});
|
|
209
217
|
}
|
|
210
|
-
if (config.adminUser?.email && config.adminUser?.password) {
|
|
218
|
+
if (config.auth?.adminUser?.email && config.auth?.adminUser?.password) {
|
|
211
219
|
migrations.push({
|
|
212
220
|
name: '00000000000009_data-admin-user',
|
|
213
221
|
up: async ({ context: pool }) => {
|
|
214
|
-
if (!config.adminUser?.email || !config.adminUser?.password) {
|
|
222
|
+
if (!config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
|
|
215
223
|
throw new Error('Admin user email and password must be provided in config');
|
|
216
224
|
}
|
|
217
225
|
const client = await pool.connect();
|
|
218
226
|
try {
|
|
219
227
|
const database = new PostgresDatabase(client);
|
|
220
228
|
const authService = new AuthService(database);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
if (isMultiTenant && !isSystemUserContextInitialized()) {
|
|
230
|
+
throw new Error('SystemUserContext has not been initialized. The meta-org migration (00000000000008_data-meta-org) should have run before this migration. ' +
|
|
231
|
+
'This migration only runs if config.app.metaOrgName and config.app.metaOrgCode are provided. ' +
|
|
232
|
+
'Please ensure both values are set in your config.');
|
|
224
233
|
}
|
|
225
|
-
if (isMultiTenant) {
|
|
226
|
-
|
|
227
|
-
const organizationService = new OrganizationService(database);
|
|
228
|
-
const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
|
|
229
|
-
if (metaOrg) {
|
|
230
|
-
initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', metaOrg);
|
|
231
|
-
systemUserContext = getSystemUserContext();
|
|
232
|
-
}
|
|
233
|
-
if (!systemUserContext?.organization?._id) {
|
|
234
|
-
throw new Error('Cannot create admin user: Multi-tenant mode is enabled but meta-org does not exist. Ensure metaOrgName and metaOrgCode are provided in config so the meta-org migration runs before the admin-user migration.');
|
|
235
|
-
}
|
|
236
|
-
}
|
|
234
|
+
if (!isMultiTenant && !isSystemUserContextInitialized()) {
|
|
235
|
+
initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', undefined);
|
|
237
236
|
}
|
|
237
|
+
const systemUserContext = getSystemUserContext();
|
|
238
238
|
const userData = {
|
|
239
|
-
email: config.adminUser
|
|
240
|
-
password: config.adminUser
|
|
239
|
+
email: config.auth?.adminUser?.email,
|
|
240
|
+
password: config.auth?.adminUser?.password,
|
|
241
241
|
firstName: 'Admin',
|
|
242
242
|
lastName: 'User',
|
|
243
243
|
displayName: 'Admin User',
|
|
@@ -252,17 +252,17 @@ export const getPostgresInitialSchema = (config) => {
|
|
|
252
252
|
}
|
|
253
253
|
},
|
|
254
254
|
down: async ({ context: pool }) => {
|
|
255
|
-
if (!config.adminUser?.email)
|
|
255
|
+
if (!config.auth?.adminUser?.email)
|
|
256
256
|
return;
|
|
257
|
-
const result = await pool.query(`DELETE FROM "users" WHERE "email" = $1`, [config.adminUser
|
|
257
|
+
const result = await pool.query(`DELETE FROM "users" WHERE "email" = $1`, [config.auth?.adminUser?.email]);
|
|
258
258
|
}
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
|
-
if (config.adminUser?.email) {
|
|
261
|
+
if (config.auth?.adminUser?.email) {
|
|
262
262
|
migrations.push({
|
|
263
263
|
name: '00000000000010_data-admin-authorizations',
|
|
264
264
|
up: async ({ context: pool }) => {
|
|
265
|
-
if (!config.adminUser?.email) {
|
|
265
|
+
if (!config.auth?.adminUser?.email) {
|
|
266
266
|
throw new Error('Admin user email not found in config');
|
|
267
267
|
}
|
|
268
268
|
const client = await pool.connect();
|
|
@@ -274,7 +274,7 @@ export const getPostgresInitialSchema = (config) => {
|
|
|
274
274
|
if (isMultiTenant && !metaOrg) {
|
|
275
275
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
276
276
|
}
|
|
277
|
-
const adminUser = await authService.getUserByEmail(config.adminUser
|
|
277
|
+
const adminUser = await authService.getUserByEmail(config.auth?.adminUser?.email);
|
|
278
278
|
if (!adminUser) {
|
|
279
279
|
throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
|
|
280
280
|
}
|
|
@@ -3,11 +3,11 @@ export function buildPostgresUrl(config) {
|
|
|
3
3
|
if (!database) {
|
|
4
4
|
throw new Error("Database configuration is required to build the PostgreSQL URL.");
|
|
5
5
|
}
|
|
6
|
-
const {
|
|
7
|
-
if (!
|
|
6
|
+
const { username, password, host, port, name } = database;
|
|
7
|
+
if (!username || !password || !host || !port || !name) {
|
|
8
8
|
throw new Error("Database configuration must include user, password, host, port, and name to build the PostgreSQL URL.");
|
|
9
9
|
}
|
|
10
|
-
const
|
|
10
|
+
const encodedUsername = encodeURIComponent(username);
|
|
11
11
|
const encodedPassword = encodeURIComponent(password);
|
|
12
|
-
return `postgresql://${
|
|
12
|
+
return `postgresql://${encodedUsername}:${encodedPassword}@${host}:${port}/${name}`;
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@ const isAuthorized = (allowedFeatures) => {
|
|
|
16
16
|
throw new UnauthenticatedError();
|
|
17
17
|
}
|
|
18
18
|
try {
|
|
19
|
-
const rawPayload = JwtService.verify(token, config.clientSecret);
|
|
19
|
+
const rawPayload = JwtService.verify(token, config.auth?.clientSecret || '');
|
|
20
20
|
const userContext = UserContextSpec.decode(rawPayload);
|
|
21
21
|
req.userContext = userContext;
|
|
22
22
|
if (userContext.authorizations.some(authorization => authorization.feature === 'admin')) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IAuthConfig {
|
|
2
|
+
adminUser: {
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
};
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
saltWorkFactor: number;
|
|
8
|
+
deviceIdCookieMaxAgeInDays: number;
|
|
9
|
+
jwtExpirationInSeconds: number;
|
|
10
|
+
passwordResetTokenExpirationInMinutes: number;
|
|
11
|
+
refreshTokenExpirationInDays: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
import { DbType } from "../databases/db-type.type.js";
|
|
2
|
+
import { IAuthConfig } from "./auth-config.interface.js";
|
|
2
3
|
export interface IBaseApiConfig {
|
|
3
|
-
appName: string;
|
|
4
|
-
env: string;
|
|
5
|
-
hostName: string;
|
|
6
|
-
clientSecret: string;
|
|
7
|
-
database: {
|
|
8
|
-
name?: string;
|
|
9
|
-
host?: string;
|
|
10
|
-
port?: number;
|
|
11
|
-
user?: string;
|
|
12
|
-
password?: string;
|
|
13
|
-
};
|
|
14
|
-
externalPort?: number;
|
|
15
|
-
internalPort?: number;
|
|
16
|
-
corsAllowedOrigins: string[];
|
|
17
|
-
saltWorkFactor?: number;
|
|
18
|
-
jobTypes?: string;
|
|
19
|
-
deployedBranch?: string;
|
|
20
|
-
debug?: {
|
|
21
|
-
showErrors?: boolean;
|
|
22
|
-
};
|
|
23
4
|
app: {
|
|
5
|
+
dbType?: DbType;
|
|
24
6
|
isMultiTenant: boolean;
|
|
25
|
-
metaOrgName?: string;
|
|
26
7
|
metaOrgCode?: string;
|
|
27
|
-
|
|
8
|
+
metaOrgName?: string;
|
|
9
|
+
name: string;
|
|
28
10
|
primaryTimezone?: string;
|
|
29
11
|
};
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
auth?: IAuthConfig;
|
|
13
|
+
database?: {
|
|
14
|
+
host: string;
|
|
15
|
+
name: string;
|
|
32
16
|
password: string;
|
|
17
|
+
port: number;
|
|
18
|
+
username: string;
|
|
33
19
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
refreshTokenExpirationInDays: number;
|
|
37
|
-
deviceIdCookieMaxAgeInDays: number;
|
|
38
|
-
passwordResetTokenExpirationInMinutes: number;
|
|
20
|
+
debug?: {
|
|
21
|
+
showErrors?: boolean;
|
|
39
22
|
};
|
|
40
23
|
email?: {
|
|
41
24
|
emailApiKey: string;
|
|
@@ -43,4 +26,11 @@ export interface IBaseApiConfig {
|
|
|
43
26
|
fromAddress: string;
|
|
44
27
|
systemEmailAddress: string;
|
|
45
28
|
};
|
|
29
|
+
env: string;
|
|
30
|
+
network: {
|
|
31
|
+
corsAllowedOrigins: string[];
|
|
32
|
+
externalPort?: number;
|
|
33
|
+
hostName: string;
|
|
34
|
+
internalPort?: number;
|
|
35
|
+
};
|
|
46
36
|
}
|
|
@@ -10,6 +10,7 @@ export declare class AuthService extends MultiTenantApiService<IUser> {
|
|
|
10
10
|
private passwordResetTokenService;
|
|
11
11
|
private emailService;
|
|
12
12
|
private organizationService;
|
|
13
|
+
private authConfig;
|
|
13
14
|
constructor(database: IDatabase);
|
|
14
15
|
attemptLogin(req: Request, res: Response, email: string, password: string): Promise<ILoginResponse | null>;
|
|
15
16
|
logUserIn(userContext: IUserContext, deviceId: string): Promise<{
|
|
@@ -17,12 +17,17 @@ export class AuthService extends MultiTenantApiService {
|
|
|
17
17
|
passwordResetTokenService;
|
|
18
18
|
emailService;
|
|
19
19
|
organizationService;
|
|
20
|
+
authConfig;
|
|
20
21
|
constructor(database) {
|
|
21
22
|
super(database, 'users', 'user', UserSpec);
|
|
22
23
|
this.refreshTokenService = new GenericApiService(database, 'refreshTokens', 'refreshToken', refreshTokenModelSpec);
|
|
23
24
|
this.passwordResetTokenService = new PasswordResetTokenService(database);
|
|
24
25
|
this.emailService = new EmailService();
|
|
25
26
|
this.organizationService = new OrganizationService(database);
|
|
27
|
+
if (!config.auth) {
|
|
28
|
+
throw new ServerError('Auth configuration is not set');
|
|
29
|
+
}
|
|
30
|
+
this.authConfig = config.auth;
|
|
26
31
|
}
|
|
27
32
|
async attemptLogin(req, res, email, password) {
|
|
28
33
|
const lowerCaseEmail = email.toLowerCase();
|
|
@@ -49,7 +54,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
49
54
|
const payload = userContext;
|
|
50
55
|
const accessToken = this.generateJwt(payload);
|
|
51
56
|
const refreshTokenObject = await this.createNewRefreshToken(userContext.user._id, deviceId, userContext.organization?._id);
|
|
52
|
-
const accessTokenExpiresOn = this.getExpiresOnFromSeconds(
|
|
57
|
+
const accessTokenExpiresOn = this.getExpiresOnFromSeconds(this.authConfig.jwtExpirationInSeconds);
|
|
53
58
|
let loginResponse = null;
|
|
54
59
|
if (refreshTokenObject) {
|
|
55
60
|
const tokenResponse = {
|
|
@@ -126,7 +131,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
126
131
|
async createNewTokens(userContext, activeRefreshToken) {
|
|
127
132
|
const payload = userContext;
|
|
128
133
|
const accessToken = this.generateJwt(payload);
|
|
129
|
-
const accessTokenExpiresOn = this.getExpiresOnFromSeconds(
|
|
134
|
+
const accessTokenExpiresOn = this.getExpiresOnFromSeconds(this.authConfig.jwtExpirationInSeconds);
|
|
130
135
|
const tokenResponse = {
|
|
131
136
|
accessToken,
|
|
132
137
|
refreshToken: activeRefreshToken.token,
|
|
@@ -147,7 +152,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
147
152
|
return activeRefreshToken;
|
|
148
153
|
}
|
|
149
154
|
async createNewRefreshToken(userId, deviceId, orgId) {
|
|
150
|
-
const expiresOn = this.getExpiresOnFromDays(
|
|
155
|
+
const expiresOn = this.getExpiresOnFromDays(this.authConfig.refreshTokenExpirationInDays);
|
|
151
156
|
const newRefreshToken = {
|
|
152
157
|
_orgId: orgId,
|
|
153
158
|
token: this.generateRefreshToken(),
|
|
@@ -162,17 +167,16 @@ export class AuthService extends MultiTenantApiService {
|
|
|
162
167
|
return insertResult;
|
|
163
168
|
}
|
|
164
169
|
async sendResetPasswordEmail(emailAddress) {
|
|
165
|
-
const expiresOn = this.getExpiresOnFromMinutes(
|
|
170
|
+
const expiresOn = this.getExpiresOnFromMinutes(this.authConfig.passwordResetTokenExpirationInMinutes);
|
|
166
171
|
const passwordResetToken = await this.passwordResetTokenService.createPasswordResetToken(emailAddress, expiresOn);
|
|
167
172
|
if (!passwordResetToken) {
|
|
168
173
|
throw new ServerError(`Failed to create password reset token for email: ${emailAddress}`);
|
|
169
174
|
}
|
|
170
175
|
const httpOrHttps = config.env === 'local' ? 'http' : 'https';
|
|
171
176
|
const urlEncodedEmail = encodeURIComponent(emailAddress);
|
|
172
|
-
const
|
|
173
|
-
const resetPasswordLink = `${httpOrHttps}://${clientUrl}/reset-password/${passwordResetToken.token}/${urlEncodedEmail}`;
|
|
177
|
+
const resetPasswordLink = `${httpOrHttps}://${config.app.name}/reset-password/${passwordResetToken.token}/${urlEncodedEmail}`;
|
|
174
178
|
const htmlEmailBody = `<strong><a href="${resetPasswordLink}">Reset Password</a></strong>`;
|
|
175
|
-
await this.emailService.sendHtmlEmail(emailAddress, `Reset Password for ${config.
|
|
179
|
+
await this.emailService.sendHtmlEmail(emailAddress, `Reset Password for ${config.app.name}`, htmlEmailBody);
|
|
176
180
|
}
|
|
177
181
|
async resetPassword(email, passwordResetToken, password) {
|
|
178
182
|
const lowerCaseEmail = email.toLowerCase();
|
|
@@ -195,9 +199,9 @@ export class AuthService extends MultiTenantApiService {
|
|
|
195
199
|
return this.refreshTokenService.deleteMany(EmptyUserContext, { filters: { deviceId: { eq: deviceId } } });
|
|
196
200
|
}
|
|
197
201
|
generateJwt(userContext) {
|
|
198
|
-
const jwtExpiryConfig =
|
|
202
|
+
const jwtExpiryConfig = this.authConfig.jwtExpirationInSeconds;
|
|
199
203
|
const jwtExpirationInSeconds = (typeof jwtExpiryConfig === 'string') ? parseInt(jwtExpiryConfig) : jwtExpiryConfig;
|
|
200
|
-
const accessToken = JwtService.sign(userContext,
|
|
204
|
+
const accessToken = JwtService.sign(userContext, this.authConfig.clientSecret, {
|
|
201
205
|
expiresIn: jwtExpirationInSeconds
|
|
202
206
|
});
|
|
203
207
|
return accessToken;
|
|
@@ -222,7 +226,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
222
226
|
}
|
|
223
227
|
if (isNewDeviceId) {
|
|
224
228
|
const cookieOptions = {
|
|
225
|
-
maxAge:
|
|
229
|
+
maxAge: this.authConfig.deviceIdCookieMaxAgeInDays * 24 * 60 * 60 * 1000,
|
|
226
230
|
httpOnly: true
|
|
227
231
|
};
|
|
228
232
|
res.cookie('deviceId', deviceId, cookieOptions);
|
|
@@ -2,23 +2,28 @@ import * as Mailjet from 'node-mailjet';
|
|
|
2
2
|
import { ServerError } from '../errors/index.js';
|
|
3
3
|
import { config } from '../config/index.js';
|
|
4
4
|
export class EmailService {
|
|
5
|
-
mailjet;
|
|
5
|
+
mailjet = null;
|
|
6
6
|
constructor() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
if (config && config.email?.emailApiKey && config.email?.emailApiSecret) {
|
|
8
|
+
this.mailjet = new Mailjet.default({
|
|
9
|
+
apiKey: config.email.emailApiKey,
|
|
10
|
+
apiSecret: config.email.emailApiSecret
|
|
11
|
+
});
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
async sendHtmlEmail(emailAddress, subject, body) {
|
|
13
|
-
if (!config.email?.fromAddress) {
|
|
14
|
-
throw new ServerError('From address is not set in the config');
|
|
15
|
+
if (!config || !config.email?.fromAddress) {
|
|
16
|
+
throw new ServerError('Email configuration is not available. From address is not set in the config.');
|
|
17
|
+
}
|
|
18
|
+
if (!this.mailjet) {
|
|
19
|
+
throw new ServerError('Email service is not configured. Email API credentials are not set in the config.');
|
|
15
20
|
}
|
|
16
21
|
const messageData = {
|
|
17
22
|
Messages: [
|
|
18
23
|
{
|
|
19
24
|
From: {
|
|
20
25
|
Email: config.email?.fromAddress,
|
|
21
|
-
Name: config.
|
|
26
|
+
Name: config.app.name || 'Application'
|
|
22
27
|
},
|
|
23
28
|
To: [
|
|
24
29
|
{
|
|
@@ -20,7 +20,7 @@ function setupExpressApp(database, config, setupRoutes) {
|
|
|
20
20
|
app.use(bodyParser.json());
|
|
21
21
|
app.use(cookieParser());
|
|
22
22
|
app.use(cors({
|
|
23
|
-
origin: config.corsAllowedOrigins,
|
|
23
|
+
origin: config.network.corsAllowedOrigins,
|
|
24
24
|
credentials: true
|
|
25
25
|
}));
|
|
26
26
|
app.use(ensureUserContext);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcore/api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
|
|
6
6
|
"scripts": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"jsonwebtoken": "^9.0.2",
|
|
52
52
|
"node-mailjet": "^6.0.8",
|
|
53
|
-
"qs": "^6.14.
|
|
53
|
+
"qs": "^6.14.1"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@loomcore/common": "^0.0.43",
|