@loomcore/api 0.2.21 → 0.2.22

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.
@@ -5,6 +5,7 @@ import { Value } from "@sinclair/typebox/value";
5
5
  import jwt from "jsonwebtoken";
6
6
  import { ObjectId } from "mongodb";
7
7
  import { ApiController } from "../controllers/api.controller.js";
8
+ import { authenticated } from "../middleware/index.js";
8
9
  import { MongoDBDatabase } from "../databases/mongo-db/mongo-db.database.js";
9
10
  import { LeftJoin } from "../databases/operations/left-join.operation.js";
10
11
  import { PostgresDatabase } from "../databases/postgres/postgres.database.js";
@@ -219,7 +220,7 @@ export class CategoryService extends GenericApiService {
219
220
  export class CategoryController extends ApiController {
220
221
  constructor(app, database) {
221
222
  const categoryService = new CategoryService(database);
222
- super("categories", app, categoryService, "category", CategorySpec);
223
+ super("categories", app, categoryService, authenticated, "category", CategorySpec);
223
224
  }
224
225
  }
225
226
  export function setupTestConfig(isMultiTenant = true, dbType) {
@@ -284,7 +285,7 @@ export class ProductService extends GenericApiService {
284
285
  export class ProductsController extends ApiController {
285
286
  constructor(app, database) {
286
287
  const productService = new ProductService(database);
287
- super("products", app, productService, "product", ProductSpec);
288
+ super("products", app, productService, authenticated, "product", ProductSpec);
288
289
  }
289
290
  async get(req, res, next) {
290
291
  res.set("Content-Type", "application/json");
@@ -359,7 +360,7 @@ export class MultiTenantProductsController extends ApiController {
359
360
  "internalNumber",
360
361
  ]);
361
362
  const PublicAggregatedProductSpec = entityUtils.getModelSpec(PublicAggregatedProductSchema);
362
- super("multi-tenant-products", app, productService, "product", ProductSpec, PublicAggregatedProductSpec);
363
+ super("multi-tenant-products", app, productService, authenticated, "product", ProductSpec, PublicAggregatedProductSpec);
363
364
  }
364
365
  }
365
366
  function configureJwtSecret() {
@@ -13,6 +13,7 @@ export declare function getTestMetaOrg(): IOrganization;
13
13
  export declare function getTestMetaOrgDomain(organizationId?: string | number): Partial<IOrganizationDomain>;
14
14
  export declare function getTestMetaOrgUser(): IUser;
15
15
  export declare function getTestMetaOrgUserContext(): IUserContext;
16
+ export declare function getTestMetaOrgAdminUserContext(): IUserContext;
16
17
  export declare function setTestOrgId(orgId: string | number): void;
17
18
  export declare function setTestOrgUserId(userId: string | number): void;
18
19
  export declare function getTestOrg(): IOrganization;
@@ -65,6 +65,21 @@ export function getTestMetaOrgUserContext() {
65
65
  ],
66
66
  };
67
67
  }
68
+ export function getTestMetaOrgAdminUserContext() {
69
+ return {
70
+ user: getTestMetaOrgUser(),
71
+ organization: getTestMetaOrg(),
72
+ authorizations: [
73
+ {
74
+ _id: "6939c54e57a1c6576a40c590",
75
+ _orgId: getTestMetaOrg()._id,
76
+ role: "admin",
77
+ feature: "admin",
78
+ config: {},
79
+ },
80
+ ],
81
+ };
82
+ }
68
83
  let TEST_ORG_ID = "6926167d06c0073a778a124f";
69
84
  let TEST_ORG_USER_ID = "6926167d06c0073a778a1250";
70
85
  export function setTestOrgId(orgId) {
@@ -2,15 +2,17 @@ import { Application, NextFunction, Request, Response } from 'express';
2
2
  import { IEntity, IModelSpec } from '@loomcore/common/models';
3
3
  import type { TSchema } from '@sinclair/typebox';
4
4
  import { IGenericApiService } from '../services/index.js';
5
+ import { type MethodAuth } from '../middleware/index.js';
5
6
  export declare abstract class ApiController<T extends IEntity> {
6
7
  protected app: Application;
7
8
  protected service: IGenericApiService<T>;
8
9
  protected slug: string;
9
10
  protected apiResourceName: string;
11
+ protected routeAuth: MethodAuth;
10
12
  protected modelSpec?: IModelSpec;
11
13
  protected publicSpec?: IModelSpec;
12
14
  protected idSchema: TSchema;
13
- protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
15
+ protected constructor(slug: string, app: Application, service: IGenericApiService<T>, routeAuth?: MethodAuth, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
14
16
  mapRoutes(app: Application): void;
15
17
  protected validate(entity: any, isPartial?: boolean): void;
16
18
  protected validateMany(entities: any[], isPartial?: boolean): void;
@@ -3,19 +3,21 @@ import { entityUtils } from '@loomcore/common/utils';
3
3
  import { Value } from '@sinclair/typebox/value';
4
4
  import { getIdSchema } from '@loomcore/common/validation';
5
5
  import { apiUtils } from '../utils/index.js';
6
- import { isAuthorized } from '../middleware/index.js';
6
+ import { authenticated, isAuthorized, } from '../middleware/index.js';
7
7
  export class ApiController {
8
8
  app;
9
9
  service;
10
10
  slug;
11
11
  apiResourceName;
12
+ routeAuth;
12
13
  modelSpec;
13
14
  publicSpec;
14
15
  idSchema;
15
- constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
16
+ constructor(slug, app, service, routeAuth = authenticated, resourceName = '', modelSpec, publicSpec) {
16
17
  this.slug = slug;
17
18
  this.app = app;
18
19
  this.service = service;
20
+ this.routeAuth = routeAuth;
19
21
  this.apiResourceName = resourceName;
20
22
  this.modelSpec = modelSpec;
21
23
  this.publicSpec = publicSpec;
@@ -23,15 +25,16 @@ export class ApiController {
23
25
  this.mapRoutes(app);
24
26
  }
25
27
  mapRoutes(app) {
26
- app.get(`/api/${this.slug}`, isAuthorized(), this.get.bind(this));
27
- app.get(`/api/${this.slug}/all`, isAuthorized(), this.getAll.bind(this));
28
- app.get(`/api/${this.slug}/count`, isAuthorized(), this.getCount.bind(this));
29
- app.get(`/api/${this.slug}/:id`, isAuthorized(), this.getById.bind(this));
30
- app.post(`/api/${this.slug}`, isAuthorized(), this.create.bind(this));
31
- app.patch(`/api/${this.slug}/batch`, isAuthorized(), this.batchUpdate.bind(this));
32
- app.put(`/api/${this.slug}/:id`, isAuthorized(), this.fullUpdateById.bind(this));
33
- app.patch(`/api/${this.slug}/:id`, isAuthorized(), this.partialUpdateById.bind(this));
34
- app.delete(`/api/${this.slug}/:id`, isAuthorized(), this.deleteById.bind(this));
28
+ const auth = isAuthorized(this.routeAuth);
29
+ app.get(`/api/${this.slug}`, auth, this.get.bind(this));
30
+ app.get(`/api/${this.slug}/all`, auth, this.getAll.bind(this));
31
+ app.get(`/api/${this.slug}/count`, auth, this.getCount.bind(this));
32
+ app.get(`/api/${this.slug}/:id`, auth, this.getById.bind(this));
33
+ app.post(`/api/${this.slug}`, auth, this.create.bind(this));
34
+ app.patch(`/api/${this.slug}/batch`, auth, this.batchUpdate.bind(this));
35
+ app.put(`/api/${this.slug}/:id`, auth, this.fullUpdateById.bind(this));
36
+ app.patch(`/api/${this.slug}/:id`, auth, this.partialUpdateById.bind(this));
37
+ app.delete(`/api/${this.slug}/:id`, auth, this.deleteById.bind(this));
35
38
  }
36
39
  validate(entity, isPartial = false) {
37
40
  const validationErrors = this.service.validate(entity, isPartial);
@@ -40,8 +40,8 @@ export class AuthController {
40
40
  mapRoutes(app) {
41
41
  app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
42
42
  app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
43
- app.get(`/api/auth/get-user-context`, isAuthorized(), this.getUserContext.bind(this));
44
- app.patch(`/api/auth/change-password`, isAuthorized(), this.changePassword.bind(this));
43
+ app.get(`/api/auth/get-user-context`, isAuthorized({ read: true }), this.getUserContext.bind(this));
44
+ app.patch(`/api/auth/change-password`, isAuthorized({ update: true }), this.changePassword.bind(this));
45
45
  app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
46
46
  app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
47
47
  }
@@ -1,9 +1,10 @@
1
+ import { adminWrites } from "../middleware/index.js";
1
2
  import { AuthorizationModelSpec, } from "../models/authorization.model.js";
2
3
  import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
3
4
  import { ApiController } from "./api.controller.js";
4
5
  export class AuthorizationsController extends ApiController {
5
6
  constructor(app, database) {
6
7
  const authorizationService = new MultiTenantApiService(database, "authorizations", "authorization", AuthorizationModelSpec);
7
- super("authorizations", app, authorizationService, "authorization", AuthorizationModelSpec);
8
+ super("authorizations", app, authorizationService, adminWrites, "authorization", AuthorizationModelSpec);
8
9
  }
9
10
  }
@@ -1,7 +1,7 @@
1
- import { IFeature } from "../models/feature.model.js";
1
+ import type { Application } from "express";
2
+ import type { IDatabase } from "../databases/models/index.js";
3
+ import { type IFeature } from "../models/feature.model.js";
2
4
  import { ApiController } from "./api.controller.js";
3
- import { Application } from "express";
4
- import { IDatabase } from "../databases/models/index.js";
5
5
  export declare class FeaturesController extends ApiController<IFeature> {
6
6
  constructor(app: Application, database: IDatabase);
7
7
  }
@@ -1,9 +1,10 @@
1
+ import { adminWrites } from "../middleware/index.js";
1
2
  import { FeatureModelSpec } from "../models/feature.model.js";
2
- import { ApiController } from "./api.controller.js";
3
3
  import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
4
+ import { ApiController } from "./api.controller.js";
4
5
  export class FeaturesController extends ApiController {
5
6
  constructor(app, database) {
6
- const featureService = new MultiTenantApiService(database, 'features', 'feature', FeatureModelSpec);
7
- super('features', app, featureService, 'feature', FeatureModelSpec, FeatureModelSpec);
7
+ const featureService = new MultiTenantApiService(database, "features", "feature", FeatureModelSpec);
8
+ super("features", app, featureService, adminWrites, "feature", FeatureModelSpec, FeatureModelSpec);
8
9
  }
9
10
  }
@@ -1,8 +1,8 @@
1
- import { Application, NextFunction, Request, Response } from 'express';
2
- import { IOrganization } from '@loomcore/common/models';
3
- import { ApiController } from './api.controller.js';
4
- import { OrganizationService } from '../services/index.js';
5
- import { IDatabase } from '../databases/models/index.js';
1
+ import type { IOrganization } from "@loomcore/common/models";
2
+ import type { Application, NextFunction, Request, Response } from "express";
3
+ import type { IDatabase } from "../databases/models/index.js";
4
+ import { OrganizationService } from "../services/index.js";
5
+ import { ApiController } from "./api.controller.js";
6
6
  export declare class OrganizationsController extends ApiController<IOrganization> {
7
7
  orgService: OrganizationService;
8
8
  constructor(app: Application, database: IDatabase);
@@ -1,28 +1,30 @@
1
- import { ApiController } from './api.controller.js';
2
- import { apiUtils } from '../utils/index.js';
3
- import { BadRequestError } from '../errors/index.js';
4
- import { OrganizationService } from '../services/index.js';
5
- import { isAuthorized } from '../middleware/index.js';
1
+ import { BadRequestError } from "../errors/index.js";
2
+ import { adminWrites, isAuthorized } from "../middleware/index.js";
3
+ import { OrganizationService } from "../services/index.js";
4
+ import { apiUtils } from "../utils/index.js";
5
+ import { ApiController } from "./api.controller.js";
6
6
  export class OrganizationsController extends ApiController {
7
7
  orgService;
8
8
  constructor(app, database) {
9
9
  const orgService = new OrganizationService(database);
10
- super('organizations', app, orgService);
10
+ super("organizations", app, orgService, adminWrites);
11
11
  this.orgService = orgService;
12
12
  }
13
13
  mapRoutes(app) {
14
14
  super.mapRoutes(app);
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));
15
+ const auth = isAuthorized(this.routeAuth);
16
+ app.get(`/api/${this.slug}/get-by-name/:name`, auth, this.getByName.bind(this));
17
+ app.get(`/api/${this.slug}/get-by-code/:code`, auth, this.getByCode.bind(this));
17
18
  }
18
19
  async getByName(req, res, next) {
19
- console.log('in OrganizationController.getByName');
20
20
  const { name } = req.params;
21
21
  try {
22
- res.set('Content-Type', 'application/json');
23
- const entity = await this.orgService.findOne(req.userContext, { filters: { name: { contains: name } } });
22
+ res.set("Content-Type", "application/json");
23
+ const entity = await this.orgService.findOne(req.userContext, {
24
+ filters: { name: { contains: name } },
25
+ });
24
26
  if (!entity)
25
- throw new BadRequestError('Name not found');
27
+ throw new BadRequestError("Organization name not found");
26
28
  apiUtils.apiResponse(res, 200, { data: entity });
27
29
  }
28
30
  catch (err) {
@@ -33,10 +35,12 @@ export class OrganizationsController extends ApiController {
33
35
  async getByCode(req, res, next) {
34
36
  const { code } = req.params;
35
37
  try {
36
- res.set('Content-Type', 'application/json');
37
- const entity = await this.orgService.findOne(req.userContext, { filters: { code: { eq: code } } });
38
+ res.set("Content-Type", "application/json");
39
+ const entity = await this.orgService.findOne(req.userContext, {
40
+ filters: { code: { eq: code } },
41
+ });
38
42
  if (!entity)
39
- throw new BadRequestError('Code not found');
43
+ throw new BadRequestError("Organization code not found");
40
44
  apiUtils.apiResponse(res, 200, { data: entity });
41
45
  }
42
46
  catch (err) {
@@ -2,15 +2,17 @@ import { Application, NextFunction, Request, Response } from 'express';
2
2
  import { IEntity, IModelSpec } from '@loomcore/common/models';
3
3
  import type { TSchema } from '@sinclair/typebox';
4
4
  import { IGenericQueryService } from '../services/index.js';
5
+ import { type MethodAuth } from '../middleware/index.js';
5
6
  export declare abstract class QueryApiController<T extends IEntity> {
6
7
  protected app: Application;
7
8
  protected service: IGenericQueryService<T>;
8
9
  protected slug: string;
9
10
  protected apiResourceName: string;
11
+ protected routeAuth: MethodAuth;
10
12
  protected modelSpec?: IModelSpec;
11
13
  protected publicSpec?: IModelSpec;
12
14
  protected idSchema: TSchema;
13
- protected constructor(slug: string, app: Application, service: IGenericQueryService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
15
+ protected constructor(slug: string, app: Application, service: IGenericQueryService<T>, routeAuth?: MethodAuth, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
14
16
  mapRoutes(app: Application): void;
15
17
  getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
16
18
  get(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -2,19 +2,21 @@ import { BadRequestError } from '../errors/index.js';
2
2
  import { Value } from '@sinclair/typebox/value';
3
3
  import { getIdSchema } from '@loomcore/common/validation';
4
4
  import { apiUtils } from '../utils/index.js';
5
- import { isAuthorized } from '../middleware/index.js';
5
+ import { authenticated, isAuthorized, } from '../middleware/index.js';
6
6
  export class QueryApiController {
7
7
  app;
8
8
  service;
9
9
  slug;
10
10
  apiResourceName;
11
+ routeAuth;
11
12
  modelSpec;
12
13
  publicSpec;
13
14
  idSchema;
14
- constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
15
+ constructor(slug, app, service, routeAuth = authenticated, resourceName = '', modelSpec, publicSpec) {
15
16
  this.slug = slug;
16
17
  this.app = app;
17
18
  this.service = service;
19
+ this.routeAuth = routeAuth;
18
20
  this.apiResourceName = resourceName;
19
21
  this.modelSpec = modelSpec;
20
22
  this.publicSpec = publicSpec;
@@ -22,10 +24,11 @@ export class QueryApiController {
22
24
  this.mapRoutes(app);
23
25
  }
24
26
  mapRoutes(app) {
25
- app.get(`/api/${this.slug}`, isAuthorized(), this.get.bind(this));
26
- app.get(`/api/${this.slug}/all`, isAuthorized(), this.getAll.bind(this));
27
- app.get(`/api/${this.slug}/count`, isAuthorized(), this.getCount.bind(this));
28
- app.get(`/api/${this.slug}/:id`, isAuthorized(), this.getById.bind(this));
27
+ const auth = isAuthorized(this.routeAuth);
28
+ app.get(`/api/${this.slug}`, auth, this.get.bind(this));
29
+ app.get(`/api/${this.slug}/all`, auth, this.getAll.bind(this));
30
+ app.get(`/api/${this.slug}/count`, auth, this.getCount.bind(this));
31
+ app.get(`/api/${this.slug}/:id`, auth, this.getById.bind(this));
29
32
  }
30
33
  async getAll(req, res, next) {
31
34
  res.set('Content-Type', 'application/json');
@@ -1,9 +1,10 @@
1
+ import { adminWrites } from "../middleware/index.js";
1
2
  import { RoleModelSpec } from "../models/role.model.js";
2
3
  import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
3
4
  import { ApiController } from "./api.controller.js";
4
5
  export class RolesController extends ApiController {
5
6
  constructor(app, database) {
6
7
  const roleService = new MultiTenantApiService(database, "roles", "role", RoleModelSpec);
7
- super("roles", app, roleService, "role", RoleModelSpec, RoleModelSpec);
8
+ super("roles", app, roleService, adminWrites, "role", RoleModelSpec, RoleModelSpec);
8
9
  }
9
10
  }
@@ -1,7 +1,7 @@
1
+ import type { Application } from "express";
2
+ import type { IDatabase } from "../databases/models/index.js";
3
+ import { type IUserRole } from "../models/user-role.model.js";
1
4
  import { ApiController } from "./api.controller.js";
2
- import { IDatabase } from "../databases/models/index.js";
3
- import { Application } from "express";
4
- import { IUserRole } from "../models/user-role.model.js";
5
5
  export declare class UserRolesController extends ApiController<IUserRole> {
6
6
  constructor(app: Application, database: IDatabase);
7
7
  }
@@ -1,9 +1,10 @@
1
- import { ApiController } from "./api.controller.js";
2
- import { UserRoleModelSpec } from "../models/user-role.model.js";
1
+ import { adminWrites } from "../middleware/index.js";
2
+ import { UserRoleModelSpec, } from "../models/user-role.model.js";
3
3
  import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
4
+ import { ApiController } from "./api.controller.js";
4
5
  export class UserRolesController extends ApiController {
5
6
  constructor(app, database) {
6
- const userRoleService = new MultiTenantApiService(database, 'user_roles', 'user_role', UserRoleModelSpec);
7
- super('user-roles', app, userRoleService, 'user-role', UserRoleModelSpec, UserRoleModelSpec);
7
+ const userRoleService = new MultiTenantApiService(database, "user_roles", "user_role", UserRoleModelSpec);
8
+ super("user-roles", app, userRoleService, adminWrites, "user-role", UserRoleModelSpec, UserRoleModelSpec);
8
9
  }
9
10
  }
@@ -1,8 +1,10 @@
1
1
  import { type IModelSpec, type IUser } from "@loomcore/common/models";
2
2
  import type { Application } from "express";
3
3
  import type { IDatabase } from "../databases/models/index.js";
4
+ import type { MethodAuth } from "../middleware/index.js";
4
5
  import { UserService } from "../services/index.js";
5
6
  import { ApiController } from "./api.controller.js";
7
+ export declare const usersRouteAuth: MethodAuth;
6
8
  export interface UsersControllerOptions {
7
9
  userService?: UserService;
8
10
  userSpec?: IModelSpec;
@@ -1,13 +1,19 @@
1
1
  import { PublicUserSpec, UserSpec, } from "@loomcore/common/models";
2
2
  import { UserService } from "../services/index.js";
3
3
  import { ApiController } from "./api.controller.js";
4
+ export const usersRouteAuth = {
5
+ read: true,
6
+ create: true,
7
+ update: true,
8
+ delete: ["admin"],
9
+ };
4
10
  export class UsersController extends ApiController {
5
11
  userService;
6
12
  constructor(app, database, options = {}) {
7
13
  const userSpec = options.userSpec ?? UserSpec;
8
14
  const publicUserSpec = options.publicUserSpec ?? PublicUserSpec;
9
15
  const userService = options.userService ?? new UserService(database, userSpec);
10
- super("users", app, userService, "user", userSpec, publicUserSpec);
16
+ super("users", app, userService, usersRouteAuth, "user", userSpec, publicUserSpec);
11
17
  this.userService = userService;
12
18
  }
13
19
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./ensure-user-context.js";
2
2
  export * from "./error-handler.js";
3
3
  export * from "./is-authorized.js";
4
+ export * from "./method-auth.model.js";
4
5
  export * from "./request-lifecycle.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./ensure-user-context.js";
2
2
  export * from "./error-handler.js";
3
3
  export * from "./is-authorized.js";
4
+ export * from "./method-auth.model.js";
4
5
  export * from "./request-lifecycle.js";
@@ -1,3 +1,4 @@
1
1
  import type { NextFunction, Request, Response } from "express";
2
- declare const isAuthorized: (allowedFeatures?: string[]) => (req: Request, _res: Response, next: NextFunction) => void;
2
+ import type { MethodAuth } from "./method-auth.model.js";
3
+ declare const isAuthorized: (config: MethodAuth) => (req: Request, _res: Response, next: NextFunction) => void;
3
4
  export { isAuthorized };
@@ -1,8 +1,42 @@
1
1
  import jwt from "jsonwebtoken";
2
- import { UnauthenticatedError, UnauthorizedError } from "../errors/index.js";
2
+ import { UnauthenticatedError, UnauthorizedError, } from "../errors/index.js";
3
3
  import { getAuthUserContextSpec } from "../utils/auth/auth-user-specs.util.js";
4
4
  import { getAuthConfig } from "../utils/auth/get-auth-config.util.js";
5
- const isAuthorized = (allowedFeatures) => {
5
+ function resolveAuthMethod(req) {
6
+ switch (req.method.toUpperCase()) {
7
+ case "GET":
8
+ case "HEAD":
9
+ return "read";
10
+ case "POST":
11
+ return "create";
12
+ case "PUT":
13
+ case "PATCH":
14
+ return "update";
15
+ case "DELETE":
16
+ return "delete";
17
+ default:
18
+ return null;
19
+ }
20
+ }
21
+ function userHasFeature(userContext, feature) {
22
+ return userContext.authorizations.some((authorization) => authorization.feature === feature);
23
+ }
24
+ function isAdmin(userContext) {
25
+ return (userHasFeature(userContext, "admin") ||
26
+ userHasFeature(userContext, "system"));
27
+ }
28
+ function assertFeatureRequirement(userContext, requirement) {
29
+ if (requirement === undefined) {
30
+ throw new UnauthorizedError();
31
+ }
32
+ if (requirement === true) {
33
+ return;
34
+ }
35
+ if (!requirement.some((feature) => userHasFeature(userContext, feature))) {
36
+ throw new UnauthorizedError();
37
+ }
38
+ }
39
+ const isAuthorized = (config) => {
6
40
  return (req, _res, next) => {
7
41
  let token = null;
8
42
  if (req.headers?.authorization) {
@@ -20,20 +54,22 @@ const isAuthorized = (allowedFeatures) => {
20
54
  const rawPayload = jwt.verify(token, authConfig.clientSecret);
21
55
  const userContext = getAuthUserContextSpec().decode(rawPayload);
22
56
  req.userContext = userContext;
23
- if (userContext.authorizations.some((authorization) => authorization.feature === "admin")) {
57
+ if (isAdmin(userContext)) {
24
58
  next();
59
+ return;
25
60
  }
26
- else if (allowedFeatures?.length) {
27
- if (!userContext.authorizations.some((authorization) => allowedFeatures.includes(authorization.feature))) {
28
- throw new UnauthorizedError();
29
- }
30
- next();
31
- }
32
- else {
33
- next();
61
+ const method = resolveAuthMethod(req);
62
+ if (!method) {
63
+ throw new UnauthorizedError();
34
64
  }
65
+ assertFeatureRequirement(userContext, config[method]);
66
+ next();
35
67
  }
36
68
  catch (err) {
69
+ if (err instanceof UnauthorizedError ||
70
+ err instanceof UnauthenticatedError) {
71
+ throw err;
72
+ }
37
73
  console.error(err);
38
74
  throw new UnauthenticatedError();
39
75
  }
@@ -0,0 +1,9 @@
1
+ export type FeatureRequirement = true | string[];
2
+ export interface MethodAuth {
3
+ read?: FeatureRequirement;
4
+ create?: FeatureRequirement;
5
+ update?: FeatureRequirement;
6
+ delete?: FeatureRequirement;
7
+ }
8
+ export declare const authenticated: MethodAuth;
9
+ export declare const adminWrites: MethodAuth;
@@ -0,0 +1,12 @@
1
+ export const authenticated = {
2
+ read: true,
3
+ create: true,
4
+ update: true,
5
+ delete: true,
6
+ };
7
+ export const adminWrites = {
8
+ read: true,
9
+ create: ["admin"],
10
+ update: ["admin"],
11
+ delete: ["admin"],
12
+ };
@@ -4,6 +4,9 @@ import type { IDatabase } from "../databases/models/index.js";
4
4
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
5
5
  export declare class UserService extends MultiTenantApiService<IUser> {
6
6
  constructor(database: IDatabase, modelSpec?: IModelSpec);
7
+ private isAdmin;
8
+ private assertAdmin;
9
+ private assertSelfOrAdmin;
7
10
  fullUpdateById(_userContext: IUserContext, _id: AppIdType, _entity: IUser): Promise<IUser>;
8
11
  update(userContext: IUserContext, queryObject: IQueryOptions, entity: Partial<IUser>): Promise<IUser[]>;
9
12
  batchUpdate(userContext: IUserContext, entities: Partial<IUser>[]): Promise<IUser[]>;
@@ -1,25 +1,45 @@
1
1
  import { UserSpec, } from "@loomcore/common/models";
2
- import { BadRequestError, ServerError } from "../errors/index.js";
2
+ import { BadRequestError, ServerError, UnauthorizedError, } from "../errors/index.js";
3
3
  import { passwordUtils } from "../utils/password.utils.js";
4
4
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
5
5
  export class UserService extends MultiTenantApiService {
6
6
  constructor(database, modelSpec = UserSpec) {
7
7
  super(database, "users", "user", modelSpec);
8
8
  }
9
+ isAdmin(userContext) {
10
+ return userContext.authorizations.some((authorization) => authorization.feature === "admin" ||
11
+ authorization.feature === "system");
12
+ }
13
+ assertAdmin(userContext) {
14
+ if (!this.isAdmin(userContext)) {
15
+ throw new UnauthorizedError();
16
+ }
17
+ }
18
+ assertSelfOrAdmin(userContext, id) {
19
+ if (this.isAdmin(userContext)) {
20
+ return;
21
+ }
22
+ if (userContext.user._id !== id) {
23
+ throw new UnauthorizedError();
24
+ }
25
+ }
9
26
  async fullUpdateById(_userContext, _id, _entity) {
10
27
  throw new ServerError("User full update is not allowed.");
11
28
  }
12
29
  async update(userContext, queryObject, entity) {
30
+ this.assertAdmin(userContext);
13
31
  this.assertPasswordUpdateAllowed(userContext, null, entity, false);
14
32
  return super.update(userContext, queryObject, entity);
15
33
  }
16
34
  async batchUpdate(userContext, entities) {
35
+ this.assertAdmin(userContext);
17
36
  for (const entity of entities) {
18
37
  this.assertPasswordUpdateAllowed(userContext, null, entity, false);
19
38
  }
20
39
  return super.batchUpdate(userContext, entities);
21
40
  }
22
41
  async partialUpdateById(userContext, id, entity, allowPasswordUpdate = false) {
42
+ this.assertSelfOrAdmin(userContext, id);
23
43
  this.assertPasswordUpdateAllowed(userContext, id, entity, allowPasswordUpdate);
24
44
  return super.partialUpdateById(userContext, id, entity);
25
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
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": {