@loomcore/api 0.2.26 → 0.2.28

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 (80) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/common-test.utils.js +7 -7
  3. package/dist/__tests__/test-objects.js +3 -27
  4. package/dist/controllers/api.controller.d.ts +1 -3
  5. package/dist/controllers/api.controller.js +28 -27
  6. package/dist/controllers/auth.controller.js +174 -126
  7. package/dist/controllers/authorizations.controller.d.ts +1 -1
  8. package/dist/controllers/authorizations.controller.js +4 -5
  9. package/dist/controllers/features.controller.d.ts +1 -1
  10. package/dist/controllers/features.controller.js +4 -5
  11. package/dist/controllers/organizations.controller.js +95 -44
  12. package/dist/controllers/query-api.controller.d.ts +1 -3
  13. package/dist/controllers/query-api.controller.js +11 -13
  14. package/dist/controllers/roles.controller.d.ts +3 -3
  15. package/dist/controllers/roles.controller.js +4 -5
  16. package/dist/controllers/user-roles.controller.d.ts +1 -1
  17. package/dist/controllers/user-roles.controller.js +4 -5
  18. package/dist/controllers/users.controller.d.ts +0 -2
  19. package/dist/controllers/users.controller.js +1 -3
  20. package/dist/databases/postgres/postgres.database.d.ts +2 -2
  21. package/dist/databases/postgres/postgres.database.js +2 -12
  22. package/dist/decorators/authorize.decorator.d.ts +13 -0
  23. package/dist/decorators/authorize.decorator.js +33 -0
  24. package/dist/errors/unauthorized.error.d.ts +1 -1
  25. package/dist/errors/unauthorized.error.js +5 -3
  26. package/dist/middleware/authorize/authenticate-request.d.ts +3 -0
  27. package/dist/middleware/authorize/authenticate-request.js +21 -0
  28. package/dist/middleware/authorize/authorize.middleware.d.ts +3 -0
  29. package/dist/middleware/authorize/authorize.middleware.js +23 -0
  30. package/dist/middleware/index.d.ts +1 -2
  31. package/dist/middleware/index.js +1 -2
  32. package/dist/models/index.d.ts +0 -1
  33. package/dist/models/index.js +0 -1
  34. package/dist/services/authorizations.service.d.ts +7 -0
  35. package/dist/services/authorizations.service.js +12 -0
  36. package/dist/services/features.service.d.ts +7 -0
  37. package/dist/services/features.service.js +12 -0
  38. package/dist/services/index.d.ts +4 -0
  39. package/dist/services/index.js +4 -0
  40. package/dist/services/refresh-token.service.d.ts +1 -1
  41. package/dist/services/refresh-token.service.js +1 -1
  42. package/dist/services/roles.service.d.ts +7 -0
  43. package/dist/services/roles.service.js +12 -0
  44. package/dist/services/user-roles.service.d.ts +7 -0
  45. package/dist/services/user-roles.service.js +12 -0
  46. package/dist/services/user.service.js +3 -3
  47. package/dist/services/utils/getUserContextAuthorizations.util.d.ts +2 -2
  48. package/dist/services/utils/getUserContextAuthorizations.util.js +3 -3
  49. package/dist/utils/auth/assert-owner-or-admin.util.d.ts +1 -1
  50. package/dist/utils/auth/assert-owner-or-admin.util.js +2 -2
  51. package/dist/utils/auth/assert-user-has-feature.util.d.ts +2 -0
  52. package/dist/utils/auth/assert-user-has-feature.util.js +10 -0
  53. package/dist/utils/auth/{assert-self-or-admin.util.d.ts → assert-user-or-admin.util.d.ts} +1 -1
  54. package/dist/utils/auth/{assert-self-or-admin.util.js → assert-user-or-admin.util.js} +1 -1
  55. package/dist/utils/auth/attempt-login.util.js +3 -3
  56. package/dist/utils/auth/auth-user-specs.util.d.ts +0 -1
  57. package/dist/utils/auth/auth-user-specs.util.js +1 -3
  58. package/dist/utils/auth/authorize-method.util.d.ts +2 -0
  59. package/dist/utils/auth/authorize-method.util.js +8 -0
  60. package/dist/utils/auth/create-new-refresh-token.util.d.ts +1 -1
  61. package/dist/utils/auth/create-new-tokens.util.d.ts +1 -2
  62. package/dist/utils/auth/get-active-refresh-token.util.d.ts +1 -2
  63. package/dist/utils/auth/index.d.ts +3 -1
  64. package/dist/utils/auth/index.js +3 -1
  65. package/dist/utils/auth/is-admin.util.js +1 -2
  66. package/package.json +4 -3
  67. package/dist/middleware/is-authorized.d.ts +0 -4
  68. package/dist/middleware/is-authorized.js +0 -57
  69. package/dist/models/authorization.model.d.ts +0 -16
  70. package/dist/models/authorization.model.js +0 -11
  71. package/dist/models/feature.model.d.ts +0 -7
  72. package/dist/models/feature.model.js +0 -6
  73. package/dist/models/method-auth.model.d.ts +0 -9
  74. package/dist/models/method-auth.model.js +0 -12
  75. package/dist/models/refresh-token.model.d.ts +0 -19
  76. package/dist/models/refresh-token.model.js +0 -12
  77. package/dist/models/role.model.d.ts +0 -7
  78. package/dist/models/role.model.js +0 -6
  79. package/dist/models/user-role.model.d.ts +0 -8
  80. package/dist/models/user-role.model.js +0 -7
@@ -1,51 +1,102 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
1
35
  import { BadRequestError } from "../errors/index.js";
2
- import { adminWrites, isAuthorized } from "../middleware/index.js";
3
36
  import { OrganizationService } from "../services/index.js";
4
- import { apiUtils } from "../utils/index.js";
37
+ import { apiUtils, authorizeMethod } from "../utils/index.js";
5
38
  import { ApiController } from "./api.controller.js";
6
- export class OrganizationsController extends ApiController {
7
- orgService;
8
- constructor(app, database) {
9
- const orgService = new OrganizationService(database);
10
- super("organizations", app, orgService, adminWrites);
11
- this.orgService = orgService;
12
- }
13
- mapRoutes(app) {
14
- super.mapRoutes(app);
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));
18
- }
19
- async getByName(req, res, next) {
20
- const { name } = req.params;
21
- try {
22
- res.set("Content-Type", "application/json");
23
- const entity = await this.orgService.findOne(req.userContext, {
24
- filters: { name: { contains: name } },
25
- });
26
- if (!entity)
27
- throw new BadRequestError("Organization name not found");
28
- apiUtils.apiResponse(res, 200, { data: entity });
39
+ import { Authorize } from "../decorators/authorize.decorator.js";
40
+ let OrganizationsController = (() => {
41
+ let _classDecorators = [Authorize('admin')];
42
+ let _classDescriptor;
43
+ let _classExtraInitializers = [];
44
+ let _classThis;
45
+ let _classSuper = ApiController;
46
+ var OrganizationsController = class extends _classSuper {
47
+ static { _classThis = this; }
48
+ static {
49
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
50
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
51
+ OrganizationsController = _classThis = _classDescriptor.value;
52
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
53
+ __runInitializers(_classThis, _classExtraInitializers);
29
54
  }
30
- catch (err) {
31
- next(err);
32
- return;
55
+ orgService;
56
+ constructor(app, database) {
57
+ const orgService = new OrganizationService(database);
58
+ super("organizations", app, orgService);
59
+ this.orgService = orgService;
33
60
  }
34
- }
35
- async getByCode(req, res, next) {
36
- const { code } = req.params;
37
- try {
38
- res.set("Content-Type", "application/json");
39
- const entity = await this.orgService.findOne(req.userContext, {
40
- filters: { code: { eq: code } },
41
- });
42
- if (!entity)
43
- throw new BadRequestError("Organization code not found");
44
- apiUtils.apiResponse(res, 200, { data: entity });
61
+ mapRoutes(app) {
62
+ super.mapRoutes(app);
63
+ const authorize = (method) => authorizeMethod(this, method);
64
+ app.get(`/api/${this.slug}/get-by-name/:name`, authorize('getByName'), this.getByName.bind(this));
65
+ app.get(`/api/${this.slug}/get-by-code/:code`, authorize('getByCode'), this.getByCode.bind(this));
45
66
  }
46
- catch (err) {
47
- next(err);
48
- return;
67
+ async getByName(req, res, next) {
68
+ const { name } = req.params;
69
+ try {
70
+ res.set("Content-Type", "application/json");
71
+ const entity = await this.orgService.findOne(req.userContext, {
72
+ filters: { name: { contains: name } },
73
+ });
74
+ if (!entity)
75
+ throw new BadRequestError("Organization name not found");
76
+ apiUtils.apiResponse(res, 200, { data: entity });
77
+ }
78
+ catch (err) {
79
+ next(err);
80
+ return;
81
+ }
49
82
  }
50
- }
51
- }
83
+ async getByCode(req, res, next) {
84
+ const { code } = req.params;
85
+ try {
86
+ res.set("Content-Type", "application/json");
87
+ const entity = await this.orgService.findOne(req.userContext, {
88
+ filters: { code: { eq: code } },
89
+ });
90
+ if (!entity)
91
+ throw new BadRequestError("Organization code not found");
92
+ apiUtils.apiResponse(res, 200, { data: entity });
93
+ }
94
+ catch (err) {
95
+ next(err);
96
+ return;
97
+ }
98
+ }
99
+ };
100
+ return OrganizationsController = _classThis;
101
+ })();
102
+ export { OrganizationsController };
@@ -2,17 +2,15 @@ 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';
6
5
  export declare abstract class QueryApiController<T extends IEntity> {
7
6
  protected app: Application;
8
7
  protected service: IGenericQueryService<T>;
9
8
  protected slug: string;
10
9
  protected apiResourceName: string;
11
- protected routeAuth: MethodAuth;
12
10
  protected modelSpec?: IModelSpec;
13
11
  protected publicSpec?: IModelSpec;
14
12
  protected idSchema: TSchema;
15
- protected constructor(slug: string, app: Application, service: IGenericQueryService<T>, routeAuth?: MethodAuth, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
13
+ protected constructor(slug: string, app: Application, service: IGenericQueryService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
16
14
  mapRoutes(app: Application): void;
17
15
  getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
18
16
  get(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -1,22 +1,19 @@
1
1
  import { BadRequestError } from '../errors/index.js';
2
2
  import { Value } from '@sinclair/typebox/value';
3
3
  import { getIdSchema } from '@loomcore/common/validation';
4
- import { apiUtils } from '../utils/index.js';
5
- import { authenticated, isAuthorized, } from '../middleware/index.js';
4
+ import { apiUtils, authorizeMethod } from '../utils/index.js';
6
5
  export class QueryApiController {
7
6
  app;
8
7
  service;
9
8
  slug;
10
9
  apiResourceName;
11
- routeAuth;
12
10
  modelSpec;
13
11
  publicSpec;
14
12
  idSchema;
15
- constructor(slug, app, service, routeAuth = authenticated, resourceName = '', modelSpec, publicSpec) {
13
+ constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
16
14
  this.slug = slug;
17
15
  this.app = app;
18
16
  this.service = service;
19
- this.routeAuth = routeAuth;
20
17
  this.apiResourceName = resourceName;
21
18
  this.modelSpec = modelSpec;
22
19
  this.publicSpec = publicSpec;
@@ -24,11 +21,11 @@ export class QueryApiController {
24
21
  this.mapRoutes(app);
25
22
  }
26
23
  mapRoutes(app) {
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));
24
+ const authorize = (method) => authorizeMethod(this, method);
25
+ app.get(`/api/${this.slug}`, authorize('get'), this.get.bind(this));
26
+ app.get(`/api/${this.slug}/all`, authorize('getAll'), this.getAll.bind(this));
27
+ app.get(`/api/${this.slug}/count`, authorize('getCount'), this.getCount.bind(this));
28
+ app.get(`/api/${this.slug}/:id`, authorize('getById'), this.getById.bind(this));
32
29
  }
33
30
  async getAll(req, res, next) {
34
31
  res.set('Content-Type', 'application/json');
@@ -47,14 +44,15 @@ export class QueryApiController {
47
44
  if (!idParam) {
48
45
  throw new BadRequestError('ID parameter is required');
49
46
  }
47
+ let id;
50
48
  try {
51
- const id = Value.Convert(this.idSchema, idParam);
52
- const entity = await this.service.getById(req.userContext, id);
53
- apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
49
+ id = Value.Convert(this.idSchema, idParam);
54
50
  }
55
51
  catch (error) {
56
52
  throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
57
53
  }
54
+ const entity = await this.service.getById(req.userContext, id);
55
+ apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
58
56
  }
59
57
  async getCount(req, res, next) {
60
58
  res.set('Content-Type', 'application/json');
@@ -1,6 +1,6 @@
1
- import { Application } from "express";
2
- import { IDatabase } from "../databases/models/index.js";
3
- import { IRole } from "../models/role.model.js";
1
+ import type { Application } from "express";
2
+ import type { IDatabase } from "../databases/models/index.js";
3
+ import { type IRole } from "@loomcore/common/models";
4
4
  import { ApiController } from "./api.controller.js";
5
5
  export declare class RolesController extends ApiController<IRole> {
6
6
  constructor(app: Application, database: IDatabase);
@@ -1,10 +1,9 @@
1
- import { adminWrites } from "../middleware/index.js";
2
- import { RoleModelSpec } from "../models/role.model.js";
3
- import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
1
+ import { RoleModelSpec } from "@loomcore/common/models";
2
+ import { RolesService } from "../services/roles.service.js";
4
3
  import { ApiController } from "./api.controller.js";
5
4
  export class RolesController extends ApiController {
6
5
  constructor(app, database) {
7
- const roleService = new MultiTenantApiService(database, "roles", "role", RoleModelSpec);
8
- super("roles", app, roleService, adminWrites, "role", RoleModelSpec, RoleModelSpec);
6
+ const roleService = new RolesService(database);
7
+ super("roles", app, roleService, "role", RoleModelSpec);
9
8
  }
10
9
  }
@@ -1,6 +1,6 @@
1
1
  import type { Application } from "express";
2
2
  import type { IDatabase } from "../databases/models/index.js";
3
- import { type IUserRole } from "../models/user-role.model.js";
3
+ import { type IUserRole } from "@loomcore/common/models";
4
4
  import { ApiController } from "./api.controller.js";
5
5
  export declare class UserRolesController extends ApiController<IUserRole> {
6
6
  constructor(app: Application, database: IDatabase);
@@ -1,10 +1,9 @@
1
- import { adminWrites } from "../middleware/index.js";
2
- import { UserRoleModelSpec, } from "../models/user-role.model.js";
3
- import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
1
+ import { UserRoleModelSpec } from "@loomcore/common/models";
2
+ import { UserRolesService } from "../services/user-roles.service.js";
4
3
  import { ApiController } from "./api.controller.js";
5
4
  export class UserRolesController extends ApiController {
6
5
  constructor(app, database) {
7
- const userRoleService = new MultiTenantApiService(database, "user_roles", "user_role", UserRoleModelSpec);
8
- super("user-roles", app, userRoleService, adminWrites, "user-role", UserRoleModelSpec, UserRoleModelSpec);
6
+ const userRoleService = new UserRolesService(database);
7
+ super("user-roles", app, userRoleService, "user-role", UserRoleModelSpec);
9
8
  }
10
9
  }
@@ -1,14 +1,12 @@
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";
5
4
  import { UserService } from "../services/index.js";
6
5
  import { ApiController } from "./api.controller.js";
7
6
  export interface UsersControllerOptions {
8
7
  userService: UserService;
9
8
  userSpec: IModelSpec;
10
9
  publicUserSpec: IModelSpec;
11
- methodAuth: MethodAuth;
12
10
  }
13
11
  export declare class UsersController extends ApiController<IUser> {
14
12
  userService: UserService;
@@ -1,5 +1,4 @@
1
1
  import { PublicUserSpec, UserSpec, } from "@loomcore/common/models";
2
- import { authenticated } from "../middleware/index.js";
3
2
  import { UserService } from "../services/index.js";
4
3
  import { ApiController } from "./api.controller.js";
5
4
  export class UsersController extends ApiController {
@@ -8,9 +7,8 @@ export class UsersController extends ApiController {
8
7
  userService: new UserService(database),
9
8
  userSpec: UserSpec,
10
9
  publicUserSpec: PublicUserSpec,
11
- methodAuth: authenticated,
12
10
  }) {
13
- super("users", app, options.userService, options.methodAuth, "user", options.userSpec, options.publicUserSpec);
11
+ super("users", app, options.userService, "user", options.userSpec, options.publicUserSpec);
14
12
  this.userService = options.userService;
15
13
  }
16
14
  }
@@ -1,4 +1,4 @@
1
- import { IEntity, IModelSpec, IPagedResult, IQueryOptions, IUserContextAuthorization } from "@loomcore/common/models";
1
+ import { IEntity, IModelSpec, IPagedResult, IQueryOptions } from "@loomcore/common/models";
2
2
  import type { AppIdType } from "@loomcore/common/types";
3
3
  import { TSchema } from "@sinclair/typebox";
4
4
  import { DeleteResult, IDatabase } from "../models/index.js";
@@ -29,5 +29,5 @@ export declare class PostgresDatabase implements IDatabase {
29
29
  deleteMany(queryObject: IQueryOptions, pluralResourceName: string): Promise<DeleteResult>;
30
30
  find<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
31
31
  findOne<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T | null>;
32
- getUserAuthorizations(userId: AppIdType, orgId?: AppIdType): Promise<IUserContextAuthorization[]>;
32
+ getUserFeatures(userId: AppIdType, orgId?: AppIdType): Promise<string[]>;
33
33
  }
@@ -68,7 +68,7 @@ export class PostgresDatabase {
68
68
  async findOne(queryObject, pluralResourceName) {
69
69
  return findOneQuery(this.connection, queryObject, pluralResourceName);
70
70
  }
71
- async getUserAuthorizations(userId, orgId) {
71
+ async getUserFeatures(userId, orgId) {
72
72
  const now = new Date();
73
73
  let query = `
74
74
  SELECT DISTINCT
@@ -93,16 +93,6 @@ export class PostgresDatabase {
93
93
  values.push(orgId);
94
94
  }
95
95
  const result = await this.connection.query(query, values);
96
- const authorizations = [];
97
- for (const row of result.rows) {
98
- authorizations.push({
99
- _id: row._id,
100
- _orgId: row._orgId,
101
- role: row.role,
102
- feature: row.feature,
103
- config: row.config || undefined,
104
- });
105
- }
106
- return authorizations;
96
+ return result.rows.map((row) => row.feature);
107
97
  }
108
98
  }
@@ -0,0 +1,13 @@
1
+ import 'reflect-metadata';
2
+ export declare const AUTH_METADATA_KEY: unique symbol;
3
+ export type MatchMode = 'all' | 'any';
4
+ export interface AuthRequirement {
5
+ features: string[];
6
+ mode: MatchMode;
7
+ allowAnonymous?: boolean;
8
+ }
9
+ export declare function Authorize(features?: string | string[], options?: {
10
+ all?: boolean;
11
+ }): (value: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
12
+ export declare function AllowAnonymous(): (value: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
13
+ export declare function resolveAuthRequirement(controllerConstructor: Function, prototype: any, propertyKey: string): AuthRequirement | undefined;
@@ -0,0 +1,33 @@
1
+ import 'reflect-metadata';
2
+ export const AUTH_METADATA_KEY = Symbol('authorize:features');
3
+ export function Authorize(features = [], options = {}) {
4
+ const requirement = {
5
+ features: Array.isArray(features) ? features : [features],
6
+ mode: options.all ? 'all' : 'any',
7
+ };
8
+ return function (value, _context) {
9
+ Reflect.defineMetadata(AUTH_METADATA_KEY, requirement, value);
10
+ };
11
+ }
12
+ export function AllowAnonymous() {
13
+ return function (value, _context) {
14
+ const metadataValue = {
15
+ features: [],
16
+ mode: 'any',
17
+ allowAnonymous: true,
18
+ };
19
+ Reflect.defineMetadata(AUTH_METADATA_KEY, metadataValue, value);
20
+ };
21
+ }
22
+ export function resolveAuthRequirement(controllerConstructor, prototype, propertyKey) {
23
+ const method = prototype[propertyKey];
24
+ if (typeof method === 'function') {
25
+ const methodLevel = Reflect.getMetadata(AUTH_METADATA_KEY, method);
26
+ if (methodLevel)
27
+ return methodLevel;
28
+ }
29
+ const classLevel = Reflect.getMetadata(AUTH_METADATA_KEY, controllerConstructor);
30
+ if (classLevel)
31
+ return classLevel;
32
+ return undefined;
33
+ }
@@ -1,7 +1,7 @@
1
1
  import { CustomError } from '@loomcore/common/errors';
2
2
  export declare class UnauthorizedError extends CustomError {
3
3
  statusCode: number;
4
- constructor();
4
+ constructor(missing?: string[]);
5
5
  serializeErrors(): {
6
6
  message: string;
7
7
  }[];
@@ -1,11 +1,13 @@
1
1
  import { CustomError } from '@loomcore/common/errors';
2
2
  export class UnauthorizedError extends CustomError {
3
3
  statusCode = 403;
4
- constructor() {
5
- super('Unauthorized');
4
+ constructor(missing) {
5
+ super(missing?.length
6
+ ? `Missing required feature(s): ${missing.join(', ')}`
7
+ : 'Unauthorized');
6
8
  Object.setPrototypeOf(this, UnauthorizedError.prototype);
7
9
  }
8
10
  serializeErrors() {
9
- return [{ message: 'Unauthorized' }];
11
+ return [{ message: this.message }];
10
12
  }
11
13
  }
@@ -0,0 +1,3 @@
1
+ import type { IUserContext } from "@loomcore/common/models";
2
+ import type { Request } from "express";
3
+ export declare function authenticateRequest(req: Request): IUserContext;
@@ -0,0 +1,21 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { UnauthenticatedError } from "../../errors/unauthenticated.error.js";
3
+ import { getAuthConfig } from "../../utils/auth/get-auth-config.util.js";
4
+ import { getAuthUserContextSpec } from "../../utils/index.js";
5
+ function extractBearerToken(req) {
6
+ const authHeader = req.headers?.authorization;
7
+ if (!authHeader) {
8
+ return null;
9
+ }
10
+ const parts = authHeader.split("Bearer ");
11
+ return parts.length > 1 ? parts[1] : null;
12
+ }
13
+ export function authenticateRequest(req) {
14
+ const token = extractBearerToken(req);
15
+ if (!token) {
16
+ throw new UnauthenticatedError();
17
+ }
18
+ const authConfig = getAuthConfig();
19
+ const rawPayload = jwt.verify(token, authConfig.clientSecret);
20
+ return getAuthUserContextSpec().decode(rawPayload);
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { RequestHandler } from 'express';
2
+ import { AuthRequirement } from '../../decorators/authorize.decorator.js';
3
+ export declare function buildAuthGuard(requirement: AuthRequirement | undefined): RequestHandler;
@@ -0,0 +1,23 @@
1
+ import { UnauthorizedError } from '../../errors/index.js';
2
+ import { authenticateRequest } from './authenticate-request.js';
3
+ export function buildAuthGuard(requirement) {
4
+ return (req, _res, next) => {
5
+ if (requirement?.allowAnonymous) {
6
+ return next();
7
+ }
8
+ const userContext = authenticateRequest(req);
9
+ req.userContext = userContext;
10
+ if (!requirement || requirement.features.length === 0) {
11
+ return next();
12
+ }
13
+ const userFeatures = new Set(userContext.features);
14
+ const hasAccess = requirement.mode === 'all'
15
+ ? requirement.features.every((f) => userFeatures.has(f))
16
+ : requirement.features.some((f) => userFeatures.has(f));
17
+ if (!hasAccess) {
18
+ const missing = requirement.features.filter((f) => !userFeatures.has(f));
19
+ throw new UnauthorizedError(missing);
20
+ }
21
+ next();
22
+ };
23
+ }
@@ -1,5 +1,4 @@
1
1
  export * from "./ensure-user-context.js";
2
2
  export * from "./error-handler.js";
3
- export * from "./is-authorized.js";
4
- export * from "../models/method-auth.model.js";
5
3
  export * from "./request-lifecycle.js";
4
+ export * from "./authorize/authorize.middleware.js";
@@ -1,5 +1,4 @@
1
1
  export * from "./ensure-user-context.js";
2
2
  export * from "./error-handler.js";
3
- export * from "./is-authorized.js";
4
- export * from "../models/method-auth.model.js";
5
3
  export * from "./request-lifecycle.js";
4
+ export * from "./authorize/authorize.middleware.js";
@@ -2,5 +2,4 @@ export * from './app-config.interface.js';
2
2
  export * from './base-api-config.interface.js';
3
3
  export * from './database-config.interface.js';
4
4
  export * from './initial-database-config.interface.js';
5
- export * from './refresh-token.model.js';
6
5
  export * from './email-client.interface.js';
@@ -2,5 +2,4 @@ export * from './app-config.interface.js';
2
2
  export * from './base-api-config.interface.js';
3
3
  export * from './database-config.interface.js';
4
4
  export * from './initial-database-config.interface.js';
5
- export * from './refresh-token.model.js';
6
5
  export * from './email-client.interface.js';
@@ -0,0 +1,7 @@
1
+ import { IAuthorization, IUserContext } from "@loomcore/common/models";
2
+ import { IDatabase } from "../databases/index.js";
3
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
4
+ export declare class AuthorizationsService extends MultiTenantApiService<IAuthorization> {
5
+ constructor(database: IDatabase);
6
+ preProcessEntity(userContext: IUserContext, entity: Partial<IAuthorization>, isCreate: boolean, allowId?: boolean): Promise<Partial<IAuthorization>>;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { AuthorizationModelSpec } from "@loomcore/common/models";
2
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
3
+ import { assertUserHasFeature } from "../utils/index.js";
4
+ export class AuthorizationsService extends MultiTenantApiService {
5
+ constructor(database) {
6
+ super(database, "authorizations", "authorization", AuthorizationModelSpec);
7
+ }
8
+ async preProcessEntity(userContext, entity, isCreate, allowId = true) {
9
+ assertUserHasFeature(userContext, ['admin', 'system', 'authorizations']);
10
+ return super.preProcessEntity(userContext, entity, isCreate, allowId);
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ import { IFeature, IUserContext } from "@loomcore/common/models";
2
+ import { IDatabase } from "../databases/index.js";
3
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
4
+ export declare class FeaturesService extends MultiTenantApiService<IFeature> {
5
+ constructor(database: IDatabase);
6
+ preProcessEntity(userContext: IUserContext, entity: Partial<IFeature>, isCreate: boolean, allowId?: boolean): Promise<Partial<IFeature>>;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { FeatureModelSpec } from "@loomcore/common/models";
2
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
3
+ import { assertUserHasFeature } from "../utils/index.js";
4
+ export class FeaturesService extends MultiTenantApiService {
5
+ constructor(database) {
6
+ super(database, "features", "feature", FeatureModelSpec);
7
+ }
8
+ async preProcessEntity(userContext, entity, isCreate, allowId = true) {
9
+ assertUserHasFeature(userContext, ['admin', 'system', 'features']);
10
+ return super.preProcessEntity(userContext, entity, isCreate, allowId);
11
+ }
12
+ }
@@ -8,5 +8,9 @@ export * from "./organization.service.js";
8
8
  export * from "./organization-domain.service.js";
9
9
  export * from "./password-reset-token.service.js";
10
10
  export * from "./refresh-token.service.js";
11
+ export * from "./authorizations.service.js";
12
+ export * from "./features.service.js";
13
+ export * from "./roles.service.js";
14
+ export * from "./user-roles.service.js";
11
15
  export * from "./tenant-query-decorator.js";
12
16
  export * from "./user.service.js";
@@ -8,5 +8,9 @@ export * from "./organization.service.js";
8
8
  export * from "./organization-domain.service.js";
9
9
  export * from "./password-reset-token.service.js";
10
10
  export * from "./refresh-token.service.js";
11
+ export * from "./authorizations.service.js";
12
+ export * from "./features.service.js";
13
+ export * from "./roles.service.js";
14
+ export * from "./user-roles.service.js";
11
15
  export * from "./tenant-query-decorator.js";
12
16
  export * from "./user.service.js";
@@ -1,5 +1,5 @@
1
+ import { IRefreshToken } from "@loomcore/common/models";
1
2
  import type { IDatabase } from "../databases/models/index.js";
2
- import { type IRefreshToken } from "../models/refresh-token.model.js";
3
3
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
4
4
  export declare class RefreshTokenService extends MultiTenantApiService<IRefreshToken> {
5
5
  constructor(database: IDatabase);
@@ -1,4 +1,4 @@
1
- import { refreshTokenModelSpec, } from "../models/refresh-token.model.js";
1
+ import { refreshTokenModelSpec } from "@loomcore/common/models";
2
2
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
3
3
  export class RefreshTokenService extends MultiTenantApiService {
4
4
  constructor(database) {
@@ -0,0 +1,7 @@
1
+ import { IRole, IUserContext } from "@loomcore/common/models";
2
+ import { IDatabase } from "../databases/index.js";
3
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
4
+ export declare class RolesService extends MultiTenantApiService<IRole> {
5
+ constructor(database: IDatabase);
6
+ preProcessEntity(userContext: IUserContext, entity: Partial<IRole>, isCreate: boolean, allowId?: boolean): Promise<Partial<IRole>>;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { RoleModelSpec } from "@loomcore/common/models";
2
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
3
+ import { assertUserHasFeature } from "../utils/index.js";
4
+ export class RolesService extends MultiTenantApiService {
5
+ constructor(database) {
6
+ super(database, "roles", "role", RoleModelSpec);
7
+ }
8
+ async preProcessEntity(userContext, entity, isCreate, allowId = true) {
9
+ assertUserHasFeature(userContext, ['admin', 'system', 'roles']);
10
+ return super.preProcessEntity(userContext, entity, isCreate, allowId);
11
+ }
12
+ }