@loomcore/api 0.1.19 → 0.1.21

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.
@@ -31,11 +31,20 @@ export class AuthController {
31
31
  const body = req.body;
32
32
  const validationErrors = this.authService.validate(body);
33
33
  entityUtils.handleValidationResult(validationErrors, 'AuthController.registerUser');
34
+ if (!userContext) {
35
+ throw new BadRequestError('Missing required fields: userContext is required.');
36
+ }
34
37
  const user = await this.authService.createUser(userContext, body);
35
38
  apiUtils.apiResponse(res, 201, { data: user || undefined }, UserSpec, PublicUserSchema);
36
39
  }
37
40
  async requestTokenUsingRefreshToken(req, res, next) {
38
- let tokens = await this.authService.requestTokenUsingRefreshToken(req);
41
+ const userContext = req.userContext;
42
+ const refreshToken = req.query.refreshToken;
43
+ if (!userContext || !refreshToken || typeof refreshToken !== 'string') {
44
+ throw new BadRequestError('Missing required fields: userContext and refreshToken are required.');
45
+ }
46
+ const deviceId = this.authService.getDeviceIdFromCookie(req);
47
+ const tokens = await this.authService.requestTokenUsingRefreshToken(userContext, refreshToken, deviceId);
39
48
  if (tokens) {
40
49
  apiUtils.apiResponse(res, 200, { data: tokens }, TokenResponseSpec);
41
50
  }
@@ -4,9 +4,10 @@ export declare class CreateAdminUserMigration implements IMigration {
4
4
  private readonly client;
5
5
  private readonly adminEmail;
6
6
  private readonly adminPassword;
7
- constructor(client: Client, adminEmail: string, adminPassword: string);
7
+ private readonly _orgId?;
8
+ constructor(client: Client, adminEmail: string, adminPassword: string, _orgId?: string | undefined);
8
9
  index: number;
9
- execute(_orgId?: string): Promise<{
10
+ execute(): Promise<{
10
11
  success: boolean;
11
12
  error: Error;
12
13
  } | {
@@ -6,20 +6,22 @@ export class CreateAdminUserMigration {
6
6
  client;
7
7
  adminEmail;
8
8
  adminPassword;
9
- constructor(client, adminEmail, adminPassword) {
9
+ _orgId;
10
+ constructor(client, adminEmail, adminPassword, _orgId) {
10
11
  this.client = client;
11
12
  this.adminEmail = adminEmail;
12
13
  this.adminPassword = adminPassword;
14
+ this._orgId = _orgId;
13
15
  }
14
16
  index = 6;
15
- async execute(_orgId) {
17
+ async execute() {
16
18
  const _id = randomUUID().toString();
17
19
  try {
18
20
  const database = new PostgresDatabase(this.client);
19
21
  const authService = new AuthService(database);
20
22
  const adminUser = await authService.createUser(getSystemUserContext(), {
21
23
  _id: _id,
22
- _orgId: _orgId,
24
+ _orgId: this._orgId,
23
25
  email: this.adminEmail,
24
26
  password: this.adminPassword,
25
27
  firstName: 'Admin',
@@ -3,6 +3,7 @@ import { CreateMigrationTableMigration } from "./001-create-migrations-table.mig
3
3
  import { CreateUsersTableMigration } from "./003-create-users-table.migration.js";
4
4
  import { doesTableExist } from "../utils/does-table-exist.util.js";
5
5
  import { CreateAdminUserMigration } from "./006-create-admin-user.migration.js";
6
+ import { getSystemUserContext } from "@loomcore/common/models";
6
7
  export async function setupDatabaseForAuth(client, adminUsername, adminPassword) {
7
8
  let runMigrations = [];
8
9
  if (await doesTableExist(client, 'migrations')) {
@@ -22,8 +23,10 @@ export async function setupDatabaseForAuth(client, adminUsername, adminPassword)
22
23
  migrationsToRun.push(new CreateUsersTableMigration(client));
23
24
  if (!runMigrations.includes(4))
24
25
  migrationsToRun.push(new CreateRefreshTokenTableMigration(client));
25
- if (!runMigrations.includes(6))
26
- migrationsToRun.push(new CreateAdminUserMigration(client, adminUsername, adminPassword));
26
+ if (!runMigrations.includes(6)) {
27
+ const systemUserContext = getSystemUserContext();
28
+ migrationsToRun.push(new CreateAdminUserMigration(client, adminUsername, adminPassword, systemUserContext._orgId));
29
+ }
27
30
  for (const migration of migrationsToRun) {
28
31
  await migration.execute();
29
32
  }
@@ -1,13 +1,14 @@
1
1
  import { Request, Response } from 'express';
2
2
  import { IUserContext, IUser, ITokenResponse, ILoginResponse } from '@loomcore/common/models';
3
- import { GenericApiService } from './generic-api-service/generic-api.service.js';
3
+ import { MultiTenantApiService } from './multi-tenant-api.service.js';
4
4
  import { UpdateResult } from '../databases/models/update-result.js';
5
5
  import { IRefreshToken } from '../models/refresh-token.model.js';
6
6
  import { IDatabase } from '../databases/models/index.js';
7
- export declare class AuthService extends GenericApiService<IUser> {
7
+ export declare class AuthService extends MultiTenantApiService<IUser> {
8
8
  private refreshTokenService;
9
9
  private passwordResetTokenService;
10
10
  private emailService;
11
+ private organizationService;
11
12
  constructor(database: IDatabase);
12
13
  attemptLogin(req: Request, res: Response, email: string, password: string): Promise<ILoginResponse | null>;
13
14
  logUserIn(userContext: IUserContext, deviceId: string): Promise<{
@@ -18,23 +19,22 @@ export declare class AuthService extends GenericApiService<IUser> {
18
19
  };
19
20
  userContext: IUserContext;
20
21
  } | null>;
21
- getUserById(id: string): Promise<IUser | null>;
22
22
  getUserByEmail(email: string): Promise<IUser | null>;
23
23
  createUser(userContext: IUserContext, user: Partial<IUser>): Promise<IUser | null>;
24
- requestTokenUsingRefreshToken(req: Request): Promise<ITokenResponse | null>;
24
+ requestTokenUsingRefreshToken(userContext: IUserContext, refreshToken: string, deviceId: string): Promise<ITokenResponse | null>;
25
25
  changeLoggedInUsersPassword(userContext: IUserContext, body: any): Promise<UpdateResult>;
26
26
  changePassword(userContext: IUserContext, queryObject: any, password: string): Promise<UpdateResult>;
27
- createNewTokens(userId: string, deviceId: string, refreshTokenExpiresOn: number): Promise<{
27
+ createNewTokens(userContext: IUserContext, activeRefreshToken: IRefreshToken): Promise<{
28
28
  accessToken: string;
29
- refreshToken: any;
29
+ refreshToken: string;
30
30
  expiresOn: number;
31
- } | null>;
31
+ }>;
32
32
  getActiveRefreshToken(userContext: IUserContext, refreshToken: string, deviceId: string): Promise<IRefreshToken | null>;
33
33
  createNewRefreshToken(userId: string, deviceId: string, existingExpiresOn?: number | null, orgId?: string): Promise<IRefreshToken | null>;
34
34
  sendResetPasswordEmail(emailAddress: string): Promise<void>;
35
35
  resetPassword(email: string, passwordResetToken: string, password: string): Promise<UpdateResult>;
36
36
  deleteRefreshTokensForDevice(deviceId: string): Promise<import("../databases/models/delete-result.js").DeleteResult>;
37
- generateJwt(payload: any): string;
37
+ generateJwt(userContext: IUserContext): string;
38
38
  generateRefreshToken(): string;
39
39
  generateDeviceId(): string;
40
40
  getAndSetDeviceIdCookie(req: Request, res: Response): string;
@@ -5,19 +5,23 @@ import { entityUtils } from '@loomcore/common/utils';
5
5
  import { BadRequestError, ServerError } from '../errors/index.js';
6
6
  import { JwtService, EmailService } from './index.js';
7
7
  import { GenericApiService } from './generic-api-service/generic-api.service.js';
8
+ import { MultiTenantApiService } from './multi-tenant-api.service.js';
8
9
  import { PasswordResetTokenService } from './password-reset-token.service.js';
10
+ import { OrganizationService } from './organization.service.js';
9
11
  import { passwordUtils } from '../utils/index.js';
10
12
  import { config } from '../config/index.js';
11
13
  import { refreshTokenModelSpec } from '../models/refresh-token.model.js';
12
- export class AuthService extends GenericApiService {
14
+ export class AuthService extends MultiTenantApiService {
13
15
  refreshTokenService;
14
16
  passwordResetTokenService;
15
17
  emailService;
18
+ organizationService;
16
19
  constructor(database) {
17
20
  super(database, 'users', 'user', UserSpec);
18
21
  this.refreshTokenService = new GenericApiService(database, 'refreshTokens', 'refreshToken', refreshTokenModelSpec);
19
22
  this.passwordResetTokenService = new PasswordResetTokenService(database);
20
23
  this.emailService = new EmailService();
24
+ this.organizationService = new OrganizationService(database);
21
25
  }
22
26
  async attemptLogin(req, res, email, password) {
23
27
  const lowerCaseEmail = email.toLowerCase();
@@ -56,31 +60,39 @@ export class AuthService extends GenericApiService {
56
60
  }
57
61
  return loginResponse;
58
62
  }
59
- async getUserById(id) {
60
- const user = await this.findOne(EmptyUserContext, { filters: { _id: { eq: id } } });
61
- return user;
62
- }
63
63
  async getUserByEmail(email) {
64
- const user = await this.findOne(EmptyUserContext, { filters: { email: { eq: email.toLowerCase() } } });
65
- return user;
64
+ const queryOptions = { filters: { email: { eq: email.toLowerCase() } } };
65
+ const rawUser = await this.database.findOne(queryOptions, 'users');
66
+ if (!rawUser) {
67
+ return null;
68
+ }
69
+ const userContext = {
70
+ _orgId: rawUser._orgId,
71
+ user: rawUser
72
+ };
73
+ return this.postprocessEntity(userContext, rawUser);
66
74
  }
67
75
  async createUser(userContext, user) {
76
+ if (user.email) {
77
+ const existingUser = await this.getUserByEmail(user.email);
78
+ if (existingUser) {
79
+ throw new BadRequestError('A user with this email address already exists');
80
+ }
81
+ }
82
+ if (user._orgId && userContext._orgId && userContext._orgId !== user._orgId && userContext.user?._id !== 'system') {
83
+ const org = await this.organizationService.findOne(userContext, { filters: { _id: { eq: user._orgId } } });
84
+ if (!org) {
85
+ throw new BadRequestError('The specified organization does not exist');
86
+ }
87
+ }
68
88
  const createdUser = await this.create(userContext, user);
69
89
  return createdUser;
70
90
  }
71
- async requestTokenUsingRefreshToken(req) {
72
- const refreshToken = req.query.refreshToken;
73
- const deviceId = this.getDeviceIdFromCookie(req);
91
+ async requestTokenUsingRefreshToken(userContext, refreshToken, deviceId) {
74
92
  let tokens = null;
75
- if (refreshToken && typeof refreshToken === 'string' && deviceId && typeof deviceId === 'string') {
76
- let userId = null;
77
- const activeRefreshToken = await this.getActiveRefreshToken(EmptyUserContext, refreshToken, deviceId);
78
- if (activeRefreshToken) {
79
- userId = activeRefreshToken.userId;
80
- if (userId) {
81
- tokens = await this.createNewTokens(userId, deviceId, activeRefreshToken.expiresOn);
82
- }
83
- }
93
+ const activeRefreshToken = await this.getActiveRefreshToken(userContext, refreshToken, deviceId);
94
+ if (activeRefreshToken) {
95
+ tokens = await this.createNewTokens(userContext, activeRefreshToken);
84
96
  }
85
97
  return tokens;
86
98
  }
@@ -98,27 +110,15 @@ export class AuthService extends GenericApiService {
98
110
  };
99
111
  return result;
100
112
  }
101
- async createNewTokens(userId, deviceId, refreshTokenExpiresOn) {
102
- let createdRefreshTokenObject = null;
103
- const user = await this.getUserById(userId);
104
- const newRefreshToken = await this.createNewRefreshToken(userId, deviceId, refreshTokenExpiresOn, user?._orgId);
105
- if (newRefreshToken) {
106
- createdRefreshTokenObject = newRefreshToken;
107
- }
108
- let tokenResponse = null;
109
- if (user && createdRefreshTokenObject) {
110
- const payload = {
111
- user: user,
112
- _orgId: user._orgId
113
- };
114
- const accessToken = this.generateJwt(payload);
115
- const accessTokenExpiresOn = this.getExpiresOnFromSeconds(config.auth.jwtExpirationInSeconds);
116
- tokenResponse = {
117
- accessToken,
118
- refreshToken: createdRefreshTokenObject.token,
119
- expiresOn: accessTokenExpiresOn
120
- };
121
- }
113
+ async createNewTokens(userContext, activeRefreshToken) {
114
+ const payload = userContext;
115
+ const accessToken = this.generateJwt(payload);
116
+ const accessTokenExpiresOn = this.getExpiresOnFromSeconds(config.auth.jwtExpirationInSeconds);
117
+ const tokenResponse = {
118
+ accessToken,
119
+ refreshToken: activeRefreshToken.token,
120
+ expiresOn: accessTokenExpiresOn
121
+ };
122
122
  return tokenResponse;
123
123
  }
124
124
  async getActiveRefreshToken(userContext, refreshToken, deviceId) {
@@ -181,13 +181,10 @@ export class AuthService extends GenericApiService {
181
181
  deleteRefreshTokensForDevice(deviceId) {
182
182
  return this.refreshTokenService.deleteMany(EmptyUserContext, { filters: { deviceId: { eq: deviceId } } });
183
183
  }
184
- generateJwt(payload) {
185
- if (payload._orgId !== undefined) {
186
- payload._orgId = String(payload._orgId);
187
- }
184
+ generateJwt(userContext) {
188
185
  const jwtExpiryConfig = config.auth.jwtExpirationInSeconds;
189
186
  const jwtExpirationInSeconds = (typeof jwtExpiryConfig === 'string') ? parseInt(jwtExpiryConfig) : jwtExpiryConfig;
190
- const accessToken = JwtService.sign(payload, config.clientSecret, {
187
+ const accessToken = JwtService.sign(userContext, config.clientSecret, {
191
188
  expiresIn: jwtExpirationInSeconds
192
189
  });
193
190
  return accessToken;
@@ -28,8 +28,9 @@ export class MultiTenantApiService extends GenericApiService {
28
28
  throw new BadRequestError('A valid userContext was not provided to MultiTenantApiService.prepareEntity');
29
29
  }
30
30
  const preparedEntity = await super.preprocessEntity(userContext, entity, isCreate, allowId);
31
- const orgIdField = this.tenantDecorator.getOrgIdField();
32
- preparedEntity[orgIdField] = userContext._orgId;
31
+ if (isCreate && userContext.user._id !== 'system') {
32
+ preparedEntity._orgId = userContext._orgId;
33
+ }
33
34
  return preparedEntity;
34
35
  }
35
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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": {