@loomcore/api 0.1.38 → 0.1.39

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/README.md CHANGED
@@ -8,7 +8,7 @@ This library provides a foundational structure for creating robust APIs, with a
8
8
 
9
9
  - **Generic API Controller**: A base `ApiController` that automatically scaffolds a full suite of RESTful endpoints for any data model. This includes support for CRUD operations, batch updates, pagination, and counting.
10
10
  - **Built-in Multi-tenancy**: Optional, configuration-driven multi-tenancy. When enabled, all database operations are automatically scoped to the current user's organization, ensuring strict data isolation.
11
- - **Authentication & Authorization**: Integrated JWT-based authentication middleware (`isAuthenticated`) to easily secure endpoints and support for OAuth 2.0 Code Flow, including the use of refresh tokens.
11
+ - **Authentication & Authorization**: Integrated JWT-based authentication middleware (`isAuthorized`) to easily secure endpoints and support for OAuth 2.0 Code Flow, including the use of refresh tokens.
12
12
  - **User & Organization Management**: Pre-built services and controllers for common user and organization management tasks.
13
13
  - **Password Management**: Includes services for handling password reset requests and workflows.
14
14
  - **Email Service Integration**: A ready-to-use service for sending transactional emails (e.g., for password resets or welcome messages).
@@ -131,17 +131,8 @@ async function simulateloginWithTestUser() {
131
131
  return `Bearer ${loginResponse.tokens.accessToken}`;
132
132
  }
133
133
  function getAuthToken() {
134
- const metaOrgUser = getTestMetaOrgUser();
135
- const metaOrg = getTestMetaOrg();
136
- const payload = {
137
- user: {
138
- _id: metaOrgUser._id,
139
- email: metaOrgUser.email
140
- },
141
- organization: metaOrg,
142
- _orgId: metaOrgUser._orgId
143
- };
144
- const token = JwtService.sign(payload, JWT_SECRET, { expiresIn: 3600 });
134
+ const userContext = getTestMetaOrgUserContext();
135
+ const token = JwtService.sign(userContext, JWT_SECRET, { expiresIn: 3600 });
145
136
  return `Bearer ${token}`;
146
137
  }
147
138
  function verifyToken(token) {
@@ -1,7 +1,7 @@
1
1
  import { BadRequestError } from '../errors/index.js';
2
2
  import { entityUtils } from '@loomcore/common/utils';
3
- import { isAuthenticated } from '../middleware/index.js';
4
3
  import { apiUtils } from '../utils/index.js';
4
+ import { isAuthorized } from '../middleware/index.js';
5
5
  export class ApiController {
6
6
  app;
7
7
  service;
@@ -19,15 +19,15 @@ export class ApiController {
19
19
  this.mapRoutes(app);
20
20
  }
21
21
  mapRoutes(app) {
22
- app.get(`/api/${this.slug}`, isAuthenticated, this.get.bind(this));
23
- app.get(`/api/${this.slug}/all`, isAuthenticated, this.getAll.bind(this));
24
- app.get(`/api/${this.slug}/count`, isAuthenticated, this.getCount.bind(this));
25
- app.get(`/api/${this.slug}/:id`, isAuthenticated, this.getById.bind(this));
26
- app.post(`/api/${this.slug}`, isAuthenticated, this.create.bind(this));
27
- app.patch(`/api/${this.slug}/batch`, isAuthenticated, this.batchUpdate.bind(this));
28
- app.put(`/api/${this.slug}/:id`, isAuthenticated, this.fullUpdateById.bind(this));
29
- app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
30
- app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
22
+ app.get(`/api/${this.slug}`, isAuthorized(), this.get.bind(this));
23
+ app.get(`/api/${this.slug}/all`, isAuthorized(), this.getAll.bind(this));
24
+ app.get(`/api/${this.slug}/count`, isAuthorized(), this.getCount.bind(this));
25
+ app.get(`/api/${this.slug}/:id`, isAuthorized(), this.getById.bind(this));
26
+ app.post(`/api/${this.slug}`, isAuthorized(), this.create.bind(this));
27
+ app.patch(`/api/${this.slug}/batch`, isAuthorized(), this.batchUpdate.bind(this));
28
+ app.put(`/api/${this.slug}/:id`, isAuthorized(), this.fullUpdateById.bind(this));
29
+ app.patch(`/api/${this.slug}/:id`, isAuthorized(), this.partialUpdateById.bind(this));
30
+ app.delete(`/api/${this.slug}/:id`, isAuthorized(), this.deleteById.bind(this));
31
31
  }
32
32
  validate(entity, isPartial = false) {
33
33
  const validationErrors = this.service.validate(entity, isPartial);
@@ -1,9 +1,9 @@
1
1
  import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSpec, passwordValidator, PublicUserContextSpec, } from '@loomcore/common/models';
2
2
  import { entityUtils } from '@loomcore/common/utils';
3
3
  import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
4
- import { isAuthenticated } from '../middleware/index.js';
5
4
  import { apiUtils } from '../utils/index.js';
6
5
  import { AuthService } from '../services/index.js';
6
+ import { isAuthorized } from '../middleware/index.js';
7
7
  export class AuthController {
8
8
  authService;
9
9
  constructor(app, database) {
@@ -13,10 +13,10 @@ export class AuthController {
13
13
  }
14
14
  mapRoutes(app) {
15
15
  app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
16
- app.post(`/api/auth/register`, isAuthenticated, this.registerUser.bind(this));
16
+ app.post(`/api/auth/register`, isAuthorized(), this.registerUser.bind(this));
17
17
  app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
18
- app.get(`/api/auth/get-user-context`, isAuthenticated, this.getUserContext.bind(this));
19
- app.patch(`/api/auth/change-password`, isAuthenticated, this.changePassword.bind(this));
18
+ app.get(`/api/auth/get-user-context`, isAuthorized(), this.getUserContext.bind(this));
19
+ app.patch(`/api/auth/change-password`, isAuthorized(), this.changePassword.bind(this));
20
20
  app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
21
21
  app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
22
22
  }
@@ -1,8 +1,8 @@
1
1
  import { ApiController } from './api.controller.js';
2
- import { isAuthenticated } from '../middleware/index.js';
3
2
  import { apiUtils } from '../utils/index.js';
4
3
  import { BadRequestError } from '../errors/index.js';
5
4
  import { OrganizationService } from '../services/index.js';
5
+ import { isAuthorized } from '../middleware/index.js';
6
6
  export class OrganizationsController extends ApiController {
7
7
  orgService;
8
8
  constructor(app, database) {
@@ -12,8 +12,8 @@ export class OrganizationsController extends ApiController {
12
12
  }
13
13
  mapRoutes(app) {
14
14
  super.mapRoutes(app);
15
- app.get(`/api/${this.slug}/get-by-name/:name`, isAuthenticated, this.getByName.bind(this));
16
- app.get(`/api/${this.slug}/get-by-code/:code`, isAuthenticated, this.getByCode.bind(this));
15
+ app.get(`/api/${this.slug}/get-by-name/:name`, isAuthorized(), this.getByName.bind(this));
16
+ app.get(`/api/${this.slug}/get-by-code/:code`, isAuthorized(), this.getByCode.bind(this));
17
17
  }
18
18
  async getByName(req, res, next) {
19
19
  console.log('in OrganizationController.getByName');
@@ -1,6 +1,6 @@
1
1
  import { UserSpec, PublicUserSpec } from '@loomcore/common/models';
2
2
  import { ApiController } from './api.controller.js';
3
- import { isAuthenticated } from '../middleware/index.js';
3
+ import { isAuthorized } from '../middleware/index.js';
4
4
  import { UserService } from '../services/index.js';
5
5
  export class UsersController extends ApiController {
6
6
  userService;
@@ -10,13 +10,13 @@ export class UsersController extends ApiController {
10
10
  this.userService = userService;
11
11
  }
12
12
  mapRoutes(app) {
13
- app.get(`/api/${this.slug}`, isAuthenticated, this.get.bind(this));
14
- app.get(`/api/${this.slug}/all`, isAuthenticated, this.getAll.bind(this));
15
- app.get(`/api/${this.slug}/find`, isAuthenticated, this.get.bind(this));
16
- app.get(`/api/${this.slug}/count`, isAuthenticated, this.getCount.bind(this));
17
- app.get(`/api/${this.slug}/:id`, isAuthenticated, this.getById.bind(this));
18
- app.post(`/api/${this.slug}`, isAuthenticated, this.create.bind(this));
19
- app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
20
- app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
13
+ app.get(`/api/${this.slug}`, isAuthorized(), this.get.bind(this));
14
+ app.get(`/api/${this.slug}/all`, isAuthorized(), this.getAll.bind(this));
15
+ app.get(`/api/${this.slug}/find`, isAuthorized(), this.get.bind(this));
16
+ app.get(`/api/${this.slug}/count`, isAuthorized(), this.getCount.bind(this));
17
+ app.get(`/api/${this.slug}/:id`, isAuthorized(), this.getById.bind(this));
18
+ app.post(`/api/${this.slug}`, isAuthorized(), this.create.bind(this));
19
+ app.patch(`/api/${this.slug}/:id`, isAuthorized(), this.partialUpdateById.bind(this));
20
+ app.delete(`/api/${this.slug}/:id`, isAuthorized(), this.deleteById.bind(this));
21
21
  }
22
22
  }
@@ -1,3 +1,3 @@
1
1
  export * from './error-handler.js';
2
- export * from './is-authenticated.js';
2
+ export * from './is-authorized.js';
3
3
  export * from './ensure-user-context.js';
@@ -1,3 +1,3 @@
1
1
  export * from './error-handler.js';
2
- export * from './is-authenticated.js';
2
+ export * from './is-authorized.js';
3
3
  export * from './ensure-user-context.js';
@@ -0,0 +1,3 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ declare const isAuthorized: (allowedFeatures?: string[]) => (req: Request, res: Response, next: NextFunction) => void;
3
+ export { isAuthorized };
@@ -0,0 +1,40 @@
1
+ import { UserContextSpec } from '@loomcore/common/models';
2
+ import { UnauthenticatedError, UnauthorizedError } from '../errors/index.js';
3
+ import { JwtService } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const isAuthorized = (allowedFeatures) => {
6
+ return (req, res, next) => {
7
+ let token = null;
8
+ if (req.headers?.authorization) {
9
+ let authHeader = req.headers.authorization;
10
+ const authHeaderArray = authHeader.split('Bearer ');
11
+ if (authHeaderArray?.length > 1) {
12
+ token = authHeaderArray[1];
13
+ }
14
+ }
15
+ if (!token) {
16
+ throw new UnauthenticatedError();
17
+ }
18
+ try {
19
+ const rawPayload = JwtService.verify(token, config.clientSecret);
20
+ const userContext = UserContextSpec.decode(rawPayload);
21
+ req.userContext = userContext;
22
+ if (userContext.authorizations.some(authorization => authorization.feature === 'admin')) {
23
+ next();
24
+ }
25
+ else if (allowedFeatures && allowedFeatures.length) {
26
+ if (!userContext.authorizations.some(authorization => allowedFeatures?.includes(authorization.feature))) {
27
+ throw new UnauthorizedError();
28
+ }
29
+ next();
30
+ }
31
+ else {
32
+ next();
33
+ }
34
+ }
35
+ catch (err) {
36
+ throw new UnauthenticatedError();
37
+ }
38
+ };
39
+ };
40
+ export { isAuthorized };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
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": {
@@ -1,2 +0,0 @@
1
- import { Request, Response, NextFunction } from 'express';
2
- export declare const isAuthenticated: (req: Request, res: Response, next: NextFunction) => void;
@@ -1,27 +0,0 @@
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
- };