@loomcore/api 0.0.1

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 (83) hide show
  1. package/dist/__tests__/common-test.utils.d.ts +35 -0
  2. package/dist/__tests__/common-test.utils.js +181 -0
  3. package/dist/__tests__/test-express-app.d.ts +16 -0
  4. package/dist/__tests__/test-express-app.js +83 -0
  5. package/dist/config/api-common-config.d.ts +3 -0
  6. package/dist/config/api-common-config.js +11 -0
  7. package/dist/config/index.d.ts +1 -0
  8. package/dist/config/index.js +1 -0
  9. package/dist/controllers/api-controller.utils.d.ts +1 -0
  10. package/dist/controllers/api-controller.utils.js +1 -0
  11. package/dist/controllers/api.controller.d.ts +22 -0
  12. package/dist/controllers/api.controller.js +71 -0
  13. package/dist/controllers/auth.controller.d.ts +16 -0
  14. package/dist/controllers/auth.controller.js +73 -0
  15. package/dist/controllers/index.d.ts +1 -0
  16. package/dist/controllers/index.js +1 -0
  17. package/dist/errors/bad-request.error.d.ts +9 -0
  18. package/dist/errors/bad-request.error.js +12 -0
  19. package/dist/errors/database-connection.error.d.ts +9 -0
  20. package/dist/errors/database-connection.error.js +12 -0
  21. package/dist/errors/duplicate-key.error.d.ts +9 -0
  22. package/dist/errors/duplicate-key.error.js +11 -0
  23. package/dist/errors/id-not-found.error.d.ts +9 -0
  24. package/dist/errors/id-not-found.error.js +11 -0
  25. package/dist/errors/index.d.ts +8 -0
  26. package/dist/errors/index.js +8 -0
  27. package/dist/errors/not-found.error.d.ts +9 -0
  28. package/dist/errors/not-found.error.js +12 -0
  29. package/dist/errors/server.error.d.ts +9 -0
  30. package/dist/errors/server.error.js +11 -0
  31. package/dist/errors/unauthenticated.error.d.ts +8 -0
  32. package/dist/errors/unauthenticated.error.js +11 -0
  33. package/dist/errors/unauthorized.error.d.ts +8 -0
  34. package/dist/errors/unauthorized.error.js +11 -0
  35. package/dist/middleware/ensure-user-context.d.ts +2 -0
  36. package/dist/middleware/ensure-user-context.js +7 -0
  37. package/dist/middleware/error-handler.d.ts +2 -0
  38. package/dist/middleware/error-handler.js +30 -0
  39. package/dist/middleware/index.d.ts +3 -0
  40. package/dist/middleware/index.js +3 -0
  41. package/dist/middleware/is-authenticated.d.ts +2 -0
  42. package/dist/middleware/is-authenticated.js +27 -0
  43. package/dist/models/api-common-config.interface.d.ts +22 -0
  44. package/dist/models/api-common-config.interface.js +1 -0
  45. package/dist/models/base-api-config.interface.d.ts +12 -0
  46. package/dist/models/base-api-config.interface.js +1 -0
  47. package/dist/models/index.d.ts +3 -0
  48. package/dist/models/index.js +3 -0
  49. package/dist/models/types/index.d.ts +1 -0
  50. package/dist/models/types/index.js +1 -0
  51. package/dist/services/auth.service.d.ts +54 -0
  52. package/dist/services/auth.service.js +283 -0
  53. package/dist/services/email.service.d.ts +4 -0
  54. package/dist/services/email.service.js +24 -0
  55. package/dist/services/generic-api-service.interface.d.ts +18 -0
  56. package/dist/services/generic-api-service.interface.js +1 -0
  57. package/dist/services/generic-api.service.d.ts +44 -0
  58. package/dist/services/generic-api.service.js +378 -0
  59. package/dist/services/index.d.ts +8 -0
  60. package/dist/services/index.js +8 -0
  61. package/dist/services/jwt.service.d.ts +4 -0
  62. package/dist/services/jwt.service.js +18 -0
  63. package/dist/services/multi-tenant-api.service.d.ts +10 -0
  64. package/dist/services/multi-tenant-api.service.js +31 -0
  65. package/dist/services/password-reset-token.service.d.ts +8 -0
  66. package/dist/services/password-reset-token.service.js +20 -0
  67. package/dist/services/tenant-query-decorator.d.ts +14 -0
  68. package/dist/services/tenant-query-decorator.js +66 -0
  69. package/dist/utils/address.utils.d.ts +6 -0
  70. package/dist/utils/address.utils.js +15 -0
  71. package/dist/utils/api.utils.d.ts +17 -0
  72. package/dist/utils/api.utils.js +60 -0
  73. package/dist/utils/conversion.utils.d.ts +5 -0
  74. package/dist/utils/conversion.utils.js +14 -0
  75. package/dist/utils/db.utils.d.ts +27 -0
  76. package/dist/utils/db.utils.js +273 -0
  77. package/dist/utils/index.d.ts +6 -0
  78. package/dist/utils/index.js +6 -0
  79. package/dist/utils/password.utils.d.ts +7 -0
  80. package/dist/utils/password.utils.js +23 -0
  81. package/dist/utils/string.utils.d.ts +9 -0
  82. package/dist/utils/string.utils.js +30 -0
  83. package/package.json +71 -0
@@ -0,0 +1,12 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export class NotFoundError extends CustomError {
3
+ statusCode = 404;
4
+ constructor(message) {
5
+ const errorMessage = message ? message : 'Not Found';
6
+ super(errorMessage);
7
+ Object.setPrototypeOf(this, NotFoundError.prototype);
8
+ }
9
+ serializeErrors() {
10
+ return [{ message: this.message }];
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export declare class ServerError extends CustomError {
3
+ statusCode: number;
4
+ constructor(message: string);
5
+ serializeErrors(): {
6
+ message: string;
7
+ field?: string | undefined;
8
+ }[];
9
+ }
@@ -0,0 +1,11 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export class ServerError extends CustomError {
3
+ statusCode = 500;
4
+ constructor(message) {
5
+ super(message);
6
+ Object.setPrototypeOf(this, ServerError.prototype);
7
+ }
8
+ serializeErrors() {
9
+ return [{ message: this.message }];
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export declare class UnauthenticatedError extends CustomError {
3
+ statusCode: number;
4
+ constructor();
5
+ serializeErrors(): {
6
+ message: string;
7
+ }[];
8
+ }
@@ -0,0 +1,11 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export class UnauthenticatedError extends CustomError {
3
+ statusCode = 401;
4
+ constructor() {
5
+ super('Unauthenticated');
6
+ Object.setPrototypeOf(this, UnauthenticatedError.prototype);
7
+ }
8
+ serializeErrors() {
9
+ return [{ message: 'Unauthenticated' }];
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export declare class UnauthorizedError extends CustomError {
3
+ statusCode: number;
4
+ constructor();
5
+ serializeErrors(): {
6
+ message: string;
7
+ }[];
8
+ }
@@ -0,0 +1,11 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ export class UnauthorizedError extends CustomError {
3
+ statusCode = 403;
4
+ constructor() {
5
+ super('Unauthorized');
6
+ Object.setPrototypeOf(this, UnauthorizedError.prototype);
7
+ }
8
+ serializeErrors() {
9
+ return [{ message: 'Unauthorized' }];
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare const ensureUserContext: (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,7 @@
1
+ import { EmptyUserContext } from '@loomcore/common/models';
2
+ export const ensureUserContext = (req, res, next) => {
3
+ if (!req.userContext) {
4
+ req.userContext = EmptyUserContext;
5
+ }
6
+ next();
7
+ };
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare const errorHandler: (err: Error, req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,30 @@
1
+ import { CustomError } from '@loomcore/common/errors';
2
+ import { apiUtils } from '../utils/index.js';
3
+ import { config } from '../config/api-common-config.js';
4
+ export const errorHandler = (err, req, res, next) => {
5
+ if (config.debug?.showErrors || config.env !== 'test') {
6
+ console.error('API Error:', {
7
+ error: err.message,
8
+ stack: err.stack,
9
+ path: req.path,
10
+ method: req.method,
11
+ body: req.body,
12
+ query: req.query,
13
+ params: req.params,
14
+ timestamp: new Date().toISOString(),
15
+ errorType: err.constructor.name,
16
+ isCustomError: err instanceof CustomError,
17
+ headers: req.headers
18
+ });
19
+ }
20
+ if (err instanceof CustomError) {
21
+ apiUtils.apiResponse(res, err.statusCode, {
22
+ errors: err.serializeErrors()
23
+ });
24
+ }
25
+ else {
26
+ apiUtils.apiResponse(res, 500, {
27
+ errors: [{ message: 'Server Error' }]
28
+ });
29
+ }
30
+ };
@@ -0,0 +1,3 @@
1
+ export * from './error-handler.js';
2
+ export * from './is-authenticated.js';
3
+ export * from './ensure-user-context.js';
@@ -0,0 +1,3 @@
1
+ export * from './error-handler.js';
2
+ export * from './is-authenticated.js';
3
+ export * from './ensure-user-context.js';
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare const isAuthenticated: (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,27 @@
1
+ import { UserContextSpec } from '@loomcore/common/models';
2
+ import { UnauthenticatedError } from '../errors/index.js';
3
+ import { JwtService } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ export const isAuthenticated = (req, res, next) => {
6
+ let token = null;
7
+ if (req.headers?.authorization) {
8
+ let authHeader = req.headers.authorization;
9
+ const authHeaderArray = authHeader.split('Bearer ');
10
+ if (authHeaderArray?.length > 1) {
11
+ token = authHeaderArray[1];
12
+ }
13
+ }
14
+ if (token) {
15
+ try {
16
+ const rawPayload = JwtService.verify(token, config.clientSecret);
17
+ req.userContext = UserContextSpec.decode(rawPayload);
18
+ next();
19
+ }
20
+ catch (err) {
21
+ throw new UnauthenticatedError();
22
+ }
23
+ }
24
+ else {
25
+ throw new UnauthenticatedError();
26
+ }
27
+ };
@@ -0,0 +1,22 @@
1
+ export interface IApiCommonConfig {
2
+ env: string;
3
+ hostName: string;
4
+ appName: string;
5
+ clientSecret: string;
6
+ debug?: {
7
+ showErrors?: boolean;
8
+ };
9
+ app: {
10
+ multiTenant: boolean;
11
+ };
12
+ auth: {
13
+ jwtExpirationInSeconds: number;
14
+ refreshTokenExpirationInDays: number;
15
+ deviceIdCookieMaxAgeInDays: number;
16
+ passwordResetTokenExpirationInMinutes: number;
17
+ };
18
+ email: {
19
+ sendGridApiKey?: string;
20
+ fromAddress?: string;
21
+ };
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { IApiCommonConfig } from './api-common-config.interface.js';
2
+ export interface IBaseApiConfig {
3
+ mongoDbUrl?: string;
4
+ databaseName?: string;
5
+ externalPort?: number;
6
+ internalPort?: number;
7
+ corsAllowedOrigins: string[];
8
+ saltWorkFactor?: number;
9
+ jobTypes?: string;
10
+ deployedBranch?: string;
11
+ api: IApiCommonConfig;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './api-common-config.interface.js';
2
+ export * from './base-api-config.interface.js';
3
+ export * from './types/index.js';
@@ -0,0 +1,3 @@
1
+ export * from './api-common-config.interface.js';
2
+ export * from './base-api-config.interface.js';
3
+ export * from './types/index.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { Db, UpdateResult } from 'mongodb';
2
+ import { Request, Response } from 'express';
3
+ import { IUserContext, IUser, ITokenResponse, ILoginResponse } from '@loomcore/common/models';
4
+ import { GenericApiService } from './generic-api.service.js';
5
+ export declare class AuthService extends GenericApiService<IUser> {
6
+ private refreshTokensCollection;
7
+ private passwordResetTokenService;
8
+ private emailService;
9
+ constructor(db: Db);
10
+ attemptLogin(req: Request, res: Response, email: string, password: string): Promise<ILoginResponse | null>;
11
+ logUserIn(userContext: IUserContext, deviceId: string): Promise<{
12
+ tokens: {
13
+ accessToken: string;
14
+ refreshToken: string;
15
+ expiresOn: number;
16
+ };
17
+ userContext: IUserContext;
18
+ } | null>;
19
+ getUserById(id: string): Promise<import("mongodb").WithId<import("bson").Document> | null>;
20
+ getUserByEmail(email: string): Promise<IUser>;
21
+ createUser(userContext: IUserContext, user: IUser): Promise<IUser | null>;
22
+ requestTokenUsingRefreshToken(req: Request): Promise<ITokenResponse | null>;
23
+ changeLoggedInUsersPassword(userContext: IUserContext, body: any): Promise<UpdateResult<import("bson").Document>>;
24
+ changePassword(userContext: IUserContext, queryObject: any, password: string): Promise<UpdateResult>;
25
+ createNewTokens(userId: string, deviceId: string, refreshTokenExpiresOn: number): Promise<{
26
+ accessToken: string;
27
+ refreshToken: any;
28
+ expiresOn: number;
29
+ } | null>;
30
+ getActiveRefreshToken(refreshToken: string, deviceId: string): Promise<import("mongodb").WithId<import("bson").Document> | null>;
31
+ createNewRefreshToken(userId: string, deviceId: string, existingExpiresOn?: number | null): Promise<{
32
+ token: string;
33
+ deviceId: string;
34
+ userId: string;
35
+ expiresOn: number;
36
+ created: Date;
37
+ createdBy: string;
38
+ } | null>;
39
+ sendResetPasswordEmail(emailAddress: string): Promise<void>;
40
+ resetPassword(email: string, passwordResetToken: string, password: string): Promise<UpdateResult>;
41
+ deleteRefreshTokensForDevice(deviceId: string): Promise<import("mongodb").DeleteResult>;
42
+ generateJwt(payload: any): string;
43
+ generateRefreshToken(): string;
44
+ generateDeviceId(): string;
45
+ getAndSetDeviceIdCookie(req: Request, res: Response): string;
46
+ getDeviceIdFromCookie(req: Request): any;
47
+ getExpiresOnFromSeconds(expiresInSeconds: number): number;
48
+ getExpiresOnFromMinutes(expiresInMinutes: number): number;
49
+ getExpiresOnFromDays(expiresInDays: number): number;
50
+ prepareEntity(userContext: IUserContext, entity: IUser | Partial<IUser>, isCreate: boolean): Promise<IUser | Partial<IUser>>;
51
+ transformList(users: IUser[]): IUser[];
52
+ transformSingle(user: IUser): IUser;
53
+ private updateLastLoggedIn;
54
+ }
@@ -0,0 +1,283 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import moment from 'moment';
3
+ import crypto from 'crypto';
4
+ import { EmptyUserContext, passwordValidator, UserSpec } from '@loomcore/common/models';
5
+ import { entityUtils } from '@loomcore/common/utils';
6
+ import { BadRequestError, ServerError } from '../errors/index.js';
7
+ import { JwtService, EmailService } from './index.js';
8
+ import { GenericApiService } from './generic-api.service.js';
9
+ import { PasswordResetTokenService } from './password-reset-token.service.js';
10
+ import { passwordUtils } from '../utils/index.js';
11
+ import { config } from '../config/index.js';
12
+ export class AuthService extends GenericApiService {
13
+ refreshTokensCollection;
14
+ passwordResetTokenService;
15
+ emailService;
16
+ constructor(db) {
17
+ super(db, 'users', 'user', UserSpec);
18
+ this.refreshTokensCollection = db.collection('refreshTokens');
19
+ this.passwordResetTokenService = new PasswordResetTokenService(db);
20
+ this.emailService = new EmailService();
21
+ }
22
+ async attemptLogin(req, res, email, password) {
23
+ const lowerCaseEmail = email.toLowerCase();
24
+ const user = await this.getUserByEmail(lowerCaseEmail);
25
+ if (!user) {
26
+ throw new BadRequestError('Invalid Credentials');
27
+ }
28
+ const passwordsMatch = await passwordUtils.comparePasswords(user.password, password);
29
+ if (!passwordsMatch) {
30
+ throw new BadRequestError('Invalid Credentials');
31
+ }
32
+ const userContext = {
33
+ user: user,
34
+ _orgId: user._orgId
35
+ };
36
+ const deviceId = this.getAndSetDeviceIdCookie(req, res);
37
+ const loginResponse = await this.logUserIn(userContext, deviceId);
38
+ return loginResponse;
39
+ }
40
+ async logUserIn(userContext, deviceId) {
41
+ const payload = userContext;
42
+ const accessToken = this.generateJwt(payload);
43
+ const refreshTokenObject = await this.createNewRefreshToken(userContext.user._id.toString(), deviceId);
44
+ const accessTokenExpiresOn = this.getExpiresOnFromSeconds(config.auth.jwtExpirationInSeconds);
45
+ let loginResponse = null;
46
+ if (refreshTokenObject) {
47
+ const tokenResponse = {
48
+ accessToken,
49
+ refreshToken: refreshTokenObject.token,
50
+ expiresOn: accessTokenExpiresOn
51
+ };
52
+ this.updateLastLoggedIn(userContext.user._id.toString())
53
+ .catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
54
+ this.transformSingle(userContext.user);
55
+ loginResponse = { tokens: tokenResponse, userContext };
56
+ }
57
+ return loginResponse;
58
+ }
59
+ getUserById(id) {
60
+ if (!entityUtils.isValidObjectId(id)) {
61
+ throw new BadRequestError('id is not a valid ObjectId');
62
+ }
63
+ return this.collection.findOne({ _id: new ObjectId(id) })
64
+ .then((doc) => {
65
+ return doc;
66
+ });
67
+ }
68
+ getUserByEmail(email) {
69
+ return this.collection.findOne({ email: email })
70
+ .then((user) => {
71
+ return user;
72
+ });
73
+ }
74
+ async createUser(userContext, user) {
75
+ const createdUser = await this.create(userContext, user);
76
+ return createdUser;
77
+ }
78
+ async requestTokenUsingRefreshToken(req) {
79
+ const refreshToken = req.query.refreshToken;
80
+ const deviceId = this.getDeviceIdFromCookie(req);
81
+ let tokens = null;
82
+ if (refreshToken && typeof refreshToken === 'string' && deviceId) {
83
+ let userId = null;
84
+ const activeRefreshToken = await this.getActiveRefreshToken(refreshToken, deviceId);
85
+ if (activeRefreshToken) {
86
+ userId = activeRefreshToken.userId;
87
+ if (userId) {
88
+ tokens = await this.createNewTokens(userId, deviceId, activeRefreshToken.expiresOn);
89
+ }
90
+ }
91
+ }
92
+ return tokens;
93
+ }
94
+ async changeLoggedInUsersPassword(userContext, body) {
95
+ const validationResult = entityUtils.validate(passwordValidator, { password: body.password });
96
+ entityUtils.handleValidationResult(validationResult, 'AuthService.changePassword');
97
+ const queryObject = { _id: new ObjectId(userContext.user._id) };
98
+ const result = await this.changePassword(userContext, queryObject, body.password);
99
+ return result;
100
+ }
101
+ async changePassword(userContext, queryObject, password) {
102
+ const hashedPassword = await passwordUtils.hashPassword(password);
103
+ let updates = { password: hashedPassword, lastPasswordChange: moment().utc().toDate() };
104
+ updates = (await this.onBeforeUpdate(userContext, updates));
105
+ const mongoUpdateResult = await this.collection.updateOne(queryObject, { $set: updates });
106
+ if (mongoUpdateResult?.modifiedCount > 0) {
107
+ await this.onAfterUpdate(userContext, updates);
108
+ }
109
+ return mongoUpdateResult;
110
+ }
111
+ async createNewTokens(userId, deviceId, refreshTokenExpiresOn) {
112
+ let createdRefreshTokenObject = null;
113
+ const newRefreshToken = await this.createNewRefreshToken(userId, deviceId, refreshTokenExpiresOn);
114
+ let user = null;
115
+ if (newRefreshToken) {
116
+ createdRefreshTokenObject = newRefreshToken;
117
+ user = await this.getUserById(userId);
118
+ }
119
+ let tokenResponse = null;
120
+ if (user && createdRefreshTokenObject) {
121
+ const payload = {
122
+ user: user,
123
+ _orgId: user._orgId ? String(user._orgId) : undefined
124
+ };
125
+ const accessToken = this.generateJwt(payload);
126
+ const accessTokenExpiresOn = this.getExpiresOnFromSeconds(config.auth.jwtExpirationInSeconds);
127
+ tokenResponse = {
128
+ accessToken,
129
+ refreshToken: createdRefreshTokenObject.token,
130
+ expiresOn: accessTokenExpiresOn
131
+ };
132
+ }
133
+ return tokenResponse;
134
+ }
135
+ async getActiveRefreshToken(refreshToken, deviceId) {
136
+ const refreshTokenResult = await this.refreshTokensCollection.findOne({ token: refreshToken, deviceId: deviceId });
137
+ let activeRefreshToken = null;
138
+ if (refreshTokenResult) {
139
+ const now = Date.now();
140
+ const notExpired = refreshTokenResult.expiresOn > now;
141
+ if (notExpired) {
142
+ activeRefreshToken = refreshTokenResult;
143
+ }
144
+ }
145
+ return activeRefreshToken;
146
+ }
147
+ async createNewRefreshToken(userId, deviceId, existingExpiresOn = null) {
148
+ const expiresOn = existingExpiresOn ? existingExpiresOn : this.getExpiresOnFromDays(config.auth.refreshTokenExpirationInDays);
149
+ const newRefreshToken = {
150
+ token: this.generateRefreshToken(),
151
+ deviceId,
152
+ userId,
153
+ expiresOn: expiresOn,
154
+ created: moment().utc().toDate(),
155
+ createdBy: userId
156
+ };
157
+ const deleteResult = await this.deleteRefreshTokensForDevice(deviceId);
158
+ const insertResult = await this.refreshTokensCollection.insertOne(newRefreshToken);
159
+ let tokenResult = null;
160
+ if (insertResult.insertedId) {
161
+ tokenResult = newRefreshToken;
162
+ }
163
+ return tokenResult;
164
+ }
165
+ async sendResetPasswordEmail(emailAddress) {
166
+ const expiresOn = this.getExpiresOnFromMinutes(config.auth.passwordResetTokenExpirationInMinutes);
167
+ const passwordResetToken = await this.passwordResetTokenService.createPasswordResetToken(emailAddress, expiresOn);
168
+ if (!passwordResetToken) {
169
+ throw new ServerError(`Failed to create password reset token for email: ${emailAddress}`);
170
+ }
171
+ const httpOrHttps = config.env === 'local' ? 'http' : 'https';
172
+ const urlEncodedEmail = encodeURIComponent(emailAddress);
173
+ const clientUrl = config.hostName;
174
+ const resetPasswordLink = `${httpOrHttps}://${clientUrl}/reset-password/${passwordResetToken.token}/${urlEncodedEmail}`;
175
+ const htmlEmailBody = `<strong><a href="${resetPasswordLink}">Reset Password</a></strong>`;
176
+ await this.emailService.sendHtmlEmail(emailAddress, `Reset Password for ${config.appName}`, htmlEmailBody);
177
+ }
178
+ async resetPassword(email, passwordResetToken, password) {
179
+ const retrievedPasswordResetToken = await this.passwordResetTokenService.getByEmail(email);
180
+ if (!retrievedPasswordResetToken) {
181
+ throw new ServerError(`Unable to retrieve password reset token for email: ${email}`);
182
+ }
183
+ if (retrievedPasswordResetToken.token !== passwordResetToken || retrievedPasswordResetToken.expiresOn < Date.now()) {
184
+ throw new BadRequestError('Invalid password reset token');
185
+ }
186
+ const result = await this.changePassword(EmptyUserContext, { email }, password);
187
+ console.log(`password changed using forgot-password for email: ${email}`);
188
+ await this.passwordResetTokenService.deleteById(EmptyUserContext, retrievedPasswordResetToken._id.toString());
189
+ console.log(`passwordResetToken deleted for email: ${email}`);
190
+ return result;
191
+ }
192
+ deleteRefreshTokensForDevice(deviceId) {
193
+ return this.refreshTokensCollection.deleteMany({ deviceId: deviceId });
194
+ }
195
+ generateJwt(payload) {
196
+ if (payload._orgId !== undefined) {
197
+ payload._orgId = String(payload._orgId);
198
+ }
199
+ const jwtExpiryConfig = config.auth.jwtExpirationInSeconds;
200
+ const jwtExpirationInSeconds = (typeof jwtExpiryConfig === 'string') ? parseInt(jwtExpiryConfig) : jwtExpiryConfig;
201
+ const accessToken = JwtService.sign(payload, config.clientSecret, {
202
+ expiresIn: jwtExpirationInSeconds
203
+ });
204
+ return accessToken;
205
+ }
206
+ ;
207
+ generateRefreshToken() {
208
+ return crypto.randomBytes(40).toString('hex');
209
+ }
210
+ generateDeviceId() {
211
+ return crypto.randomBytes(40).toString('hex');
212
+ }
213
+ getAndSetDeviceIdCookie(req, res) {
214
+ let isNewDeviceId = false;
215
+ let deviceId = '';
216
+ const deviceIdFromCookie = this.getDeviceIdFromCookie(req);
217
+ if (deviceIdFromCookie) {
218
+ deviceId = deviceIdFromCookie;
219
+ }
220
+ else {
221
+ deviceId = this.generateDeviceId();
222
+ isNewDeviceId = true;
223
+ }
224
+ if (isNewDeviceId) {
225
+ const cookieOptions = {
226
+ maxAge: config.auth.deviceIdCookieMaxAgeInDays * 24 * 60 * 60 * 1000,
227
+ httpOnly: true
228
+ };
229
+ if (config.env === 'local' || config.env === 'dev') {
230
+ console.log('setting deviceId cookieOptions sameSite=none and secure=true');
231
+ cookieOptions['sameSite'] = 'none';
232
+ cookieOptions['secure'] = true;
233
+ }
234
+ res.cookie('deviceId', deviceId, cookieOptions);
235
+ }
236
+ return deviceId;
237
+ }
238
+ getDeviceIdFromCookie(req) {
239
+ return req.cookies['deviceId'];
240
+ }
241
+ getExpiresOnFromSeconds(expiresInSeconds) {
242
+ return Date.now() + expiresInSeconds * 1000;
243
+ }
244
+ getExpiresOnFromMinutes(expiresInMinutes) {
245
+ return Date.now() + expiresInMinutes * 60 * 1000;
246
+ }
247
+ getExpiresOnFromDays(expiresInDays) {
248
+ return Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
249
+ }
250
+ async prepareEntity(userContext, entity, isCreate) {
251
+ if (entity.email) {
252
+ entity.email = entity.email.toLowerCase();
253
+ }
254
+ if (entity.password) {
255
+ const hash = await passwordUtils.hashPassword(entity.password);
256
+ entity.password = hash;
257
+ }
258
+ if (isCreate && !entity.roles) {
259
+ entity.roles = ["user"];
260
+ }
261
+ const preparedEntity = await super.prepareEntity(userContext, entity, isCreate);
262
+ return preparedEntity;
263
+ }
264
+ transformList(users) {
265
+ return super.transformList(users);
266
+ }
267
+ transformSingle(user) {
268
+ return super.transformSingle(user);
269
+ }
270
+ async updateLastLoggedIn(userId) {
271
+ try {
272
+ if (!entityUtils.isValidObjectId(userId)) {
273
+ throw new BadRequestError('userId is not a valid ObjectId');
274
+ }
275
+ const queryObject = { _id: new ObjectId(userId) };
276
+ const updates = { lastLoggedIn: moment().utc().toDate() };
277
+ await this.collection.updateOne(queryObject, { $set: updates });
278
+ }
279
+ catch (error) {
280
+ console.log(`Failed to update lastLoggedIn for user ${userId}: ${error}`);
281
+ }
282
+ }
283
+ }
@@ -0,0 +1,4 @@
1
+ export declare class EmailService {
2
+ constructor();
3
+ sendHtmlEmail(emailAddress: string, subject: string, body: string): Promise<void>;
4
+ }
@@ -0,0 +1,24 @@
1
+ import sgMail from '@sendgrid/mail';
2
+ import { ServerError } from '../errors/index.js';
3
+ import { config } from '../config/index.js';
4
+ export class EmailService {
5
+ constructor() {
6
+ sgMail.setApiKey(config.email.sendGridApiKey);
7
+ }
8
+ async sendHtmlEmail(emailAddress, subject, body) {
9
+ const msg = {
10
+ to: emailAddress,
11
+ from: config.email.fromAddress,
12
+ subject: subject,
13
+ html: `${body}`,
14
+ };
15
+ try {
16
+ await sgMail.send(msg);
17
+ console.log(`Email sent to ${emailAddress} with subject ${subject}`);
18
+ }
19
+ catch (error) {
20
+ console.error('Error sending email:', error);
21
+ throw new ServerError('Error sending email');
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ import { DeleteResult, Document, FindOptions } from 'mongodb';
2
+ import { IUserContext, IEntity, IPagedResult, QueryOptions } from '@loomcore/common/models';
3
+ export interface IGenericApiService<T extends IEntity> {
4
+ getAll(userContext: IUserContext): Promise<T[]>;
5
+ get(userContext: IUserContext, queryOptions: QueryOptions): Promise<IPagedResult<T>>;
6
+ getById(userContext: IUserContext, id: string): Promise<T>;
7
+ getCount(userContext: IUserContext): Promise<number>;
8
+ create(userContext: IUserContext, item: T): Promise<T | null>;
9
+ createMany(userContext: IUserContext, items: T[]): Promise<T[]>;
10
+ fullUpdateById(userContext: IUserContext, id: string, item: T): Promise<T>;
11
+ partialUpdateById(userContext: IUserContext, id: string, item: Partial<T>): Promise<T>;
12
+ partialUpdateByIdWithoutBeforeAndAfter(userContext: IUserContext, id: string, item: Partial<T>): Promise<T>;
13
+ update(userContext: IUserContext, queryObject: any, item: Partial<T>): Promise<T[]>;
14
+ deleteById(userContext: IUserContext, id: string): Promise<DeleteResult>;
15
+ deleteMany(userContext: IUserContext, queryObject: any): Promise<DeleteResult>;
16
+ find(userContext: IUserContext, mongoQueryObject: any, options?: FindOptions<Document> | undefined): Promise<T[]>;
17
+ findOne(userContext: IUserContext, mongoQueryObject: any, options?: FindOptions<Document> | undefined): Promise<T>;
18
+ }
@@ -0,0 +1 @@
1
+ export {};