@loomcore/api 0.1.57 → 0.1.58
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 +9 -9
- package/dist/databases/mongo-db/utils/build-mongo-url.util.js +4 -4
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +9 -9
- 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);
|
|
@@ -146,11 +146,11 @@ export const getMongoInitialSchema = (config) => {
|
|
|
146
146
|
}
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
|
-
if (config.
|
|
149
|
+
if (config.auth && config.auth.adminUser) {
|
|
150
150
|
migrations.push({
|
|
151
151
|
name: '00000000000009_data-admin-user',
|
|
152
152
|
up: async ({ context: db }) => {
|
|
153
|
-
if (
|
|
153
|
+
if (config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
|
|
154
154
|
throw new Error('Admin user email and password must be provided in config');
|
|
155
155
|
}
|
|
156
156
|
const database = new MongoDBDatabase(db);
|
|
@@ -160,25 +160,25 @@ export const getMongoInitialSchema = (config) => {
|
|
|
160
160
|
await authService.createUser(systemUserContext, {
|
|
161
161
|
_id: _id,
|
|
162
162
|
_orgId: systemUserContext.organization?._id,
|
|
163
|
-
email: config.adminUser
|
|
164
|
-
password: config.adminUser
|
|
163
|
+
email: config.auth?.adminUser?.email,
|
|
164
|
+
password: config.auth?.adminUser?.password,
|
|
165
165
|
firstName: 'Admin',
|
|
166
166
|
lastName: 'User',
|
|
167
167
|
displayName: 'Admin User',
|
|
168
168
|
});
|
|
169
169
|
},
|
|
170
170
|
down: async ({ context: db }) => {
|
|
171
|
-
if (!config.adminUser?.email)
|
|
171
|
+
if (!config.auth?.adminUser?.email)
|
|
172
172
|
return;
|
|
173
|
-
await db.collection('users').deleteOne({ email: config.adminUser
|
|
173
|
+
await db.collection('users').deleteOne({ email: config.auth?.adminUser?.email });
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
|
-
if (config.adminUser?.email && isMultiTenant) {
|
|
177
|
+
if (config.auth?.adminUser?.email && isMultiTenant) {
|
|
178
178
|
migrations.push({
|
|
179
179
|
name: '00000000000010_data-admin-authorizations',
|
|
180
180
|
up: async ({ context: db }) => {
|
|
181
|
-
if (!config.adminUser?.email) {
|
|
181
|
+
if (!config.auth?.adminUser?.email) {
|
|
182
182
|
throw new Error('Admin user email not found in config');
|
|
183
183
|
}
|
|
184
184
|
const database = new MongoDBDatabase(db);
|
|
@@ -188,7 +188,7 @@ export const getMongoInitialSchema = (config) => {
|
|
|
188
188
|
if (!metaOrg) {
|
|
189
189
|
throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
|
|
190
190
|
}
|
|
191
|
-
const adminUser = await authService.getUserByEmail(config.adminUser
|
|
191
|
+
const adminUser = await authService.getUserByEmail(config.auth?.adminUser?.email);
|
|
192
192
|
if (!adminUser) {
|
|
193
193
|
throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
|
|
194
194
|
}
|
|
@@ -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
|
}
|
|
@@ -207,11 +207,11 @@ export const getPostgresInitialSchema = (config) => {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
-
if (config.adminUser?.email && config.adminUser?.password) {
|
|
210
|
+
if (config.auth?.adminUser?.email && config.auth?.adminUser?.password) {
|
|
211
211
|
migrations.push({
|
|
212
212
|
name: '00000000000009_data-admin-user',
|
|
213
213
|
up: async ({ context: pool }) => {
|
|
214
|
-
if (!config.adminUser?.email || !config.adminUser?.password) {
|
|
214
|
+
if (!config.auth?.adminUser?.email || !config.auth?.adminUser?.password) {
|
|
215
215
|
throw new Error('Admin user email and password must be provided in config');
|
|
216
216
|
}
|
|
217
217
|
const client = await pool.connect();
|
|
@@ -236,8 +236,8 @@ export const getPostgresInitialSchema = (config) => {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
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.network.hostName}/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.58",
|
|
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",
|