@loomcore/api 0.2.24 → 0.2.26

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.
@@ -3,9 +3,10 @@ import type { Application, Request, Response } from "express";
3
3
  import type { IDatabase } from "../databases/models/index.js";
4
4
  import { OrganizationService, UserService } from "../services/index.js";
5
5
  export interface AuthControllerOptions {
6
- userService?: UserService;
7
- userSpec?: IModelSpec;
8
- publicUserSpec?: IModelSpec;
6
+ userService: UserService;
7
+ userSpec: IModelSpec;
8
+ publicUserSpec: IModelSpec;
9
+ publicUserContextSpec: IModelSpec;
9
10
  }
10
11
  export declare class AuthController {
11
12
  database: IDatabase;
@@ -13,7 +14,7 @@ export declare class AuthController {
13
14
  organizationService: OrganizationService;
14
15
  userSpec: IModelSpec;
15
16
  publicUserSpec: IModelSpec;
16
- publicUserContextSpec: IModelSpec;
17
+ userContextSpec: IModelSpec;
17
18
  loginResponseSpec: IModelSpec;
18
19
  constructor(app: Application, database: IDatabase, options?: AuthControllerOptions);
19
20
  mapRoutes(app: Application): void;
@@ -1,10 +1,10 @@
1
- import { EmptyUserContext, passwordValidator, TokenResponseSpec, UserSpec, } from "@loomcore/common/models";
1
+ import { EmptyUserContext, passwordValidator, PublicUserContextSpec, PublicUserSpec, TokenResponseSpec, UserSpec, } from "@loomcore/common/models";
2
2
  import { entityUtils } from "@loomcore/common/utils";
3
3
  import { config } from "../config/base-api-config.js";
4
4
  import { BadRequestError, UnauthenticatedError } from "../errors/index.js";
5
5
  import { isAuthorized } from "../middleware/index.js";
6
6
  import { OrganizationService, UserService } from "../services/index.js";
7
- import { attemptLogin, changePassword, createUserContextSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, resolveAuthUserSpecs, sendResetPasswordEmail, setAuthUserContextSpec, } from "../utils/auth/index.js";
7
+ import { attemptLogin, changePassword, createLoginResponseSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail, setAuthUserContextSpec, } from "../utils/auth/index.js";
8
8
  import { apiUtils } from "../utils/index.js";
9
9
  export class AuthController {
10
10
  database;
@@ -12,29 +12,22 @@ export class AuthController {
12
12
  organizationService;
13
13
  userSpec;
14
14
  publicUserSpec;
15
- publicUserContextSpec;
15
+ userContextSpec;
16
16
  loginResponseSpec;
17
- constructor(app, database, options = {}) {
17
+ constructor(app, database, options = {
18
+ userService: new UserService(database),
19
+ userSpec: UserSpec,
20
+ publicUserSpec: PublicUserSpec,
21
+ publicUserContextSpec: PublicUserContextSpec,
22
+ }) {
18
23
  this.database = database;
19
- const userSpecFromService = options.userService?.getModelSpec();
20
- const inheritedUserSpec = userSpecFromService && userSpecFromService !== UserSpec
21
- ? userSpecFromService
22
- : undefined;
23
- const resolved = resolveAuthUserSpecs({
24
- userSpec: options.userSpec ?? inheritedUserSpec,
25
- publicUserSpec: options.publicUserSpec,
26
- });
27
- this.userSpec = resolved.userSpec;
28
- this.publicUserSpec = resolved.publicUserSpec;
29
- this.publicUserContextSpec = resolved.publicUserContextSpec;
30
- this.loginResponseSpec = resolved.loginResponseSpec;
31
- this.userService =
32
- options.userService ?? new UserService(database, this.userSpec);
24
+ this.userSpec = options.userSpec;
25
+ this.publicUserSpec = options.publicUserSpec;
26
+ this.userContextSpec = options.publicUserContextSpec;
27
+ setAuthUserContextSpec(this.userContextSpec);
28
+ this.loginResponseSpec = createLoginResponseSpec(this.userContextSpec);
29
+ this.userService = options.userService;
33
30
  this.organizationService = new OrganizationService(database);
34
- const jwtUserModelSpec = options.userSpec ?? inheritedUserSpec ?? options.publicUserSpec;
35
- if (jwtUserModelSpec) {
36
- setAuthUserContextSpec(createUserContextSpec(jwtUserModelSpec));
37
- }
38
31
  this.mapRoutes(app);
39
32
  }
40
33
  mapRoutes(app) {
@@ -87,7 +80,7 @@ export class AuthController {
87
80
  }
88
81
  async getUserContext(req, res) {
89
82
  const userContext = req.userContext;
90
- apiUtils.apiResponse(res, 200, { data: userContext }, this.publicUserContextSpec);
83
+ apiUtils.apiResponse(res, 200, { data: userContext }, this.userContextSpec);
91
84
  }
92
85
  afterAuth(_req, _res, _loginResponse) {
93
86
  console.log("in afterAuth");
@@ -1,15 +1,14 @@
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
+ import { type MethodAuth } from "../middleware/index.js";
5
5
  import { UserService } from "../services/index.js";
6
6
  import { ApiController } from "./api.controller.js";
7
- export declare const usersRouteAuth: MethodAuth;
8
7
  export interface UsersControllerOptions {
9
- userService?: UserService;
10
- userSpec?: IModelSpec;
11
- publicUserSpec?: IModelSpec;
12
- routeAuth?: MethodAuth;
8
+ userService: UserService;
9
+ userSpec: IModelSpec;
10
+ publicUserSpec: IModelSpec;
11
+ methodAuth: MethodAuth;
13
12
  }
14
13
  export declare class UsersController extends ApiController<IUser> {
15
14
  userService: UserService;
@@ -1,19 +1,16 @@
1
1
  import { PublicUserSpec, UserSpec, } from "@loomcore/common/models";
2
+ import { authenticated } from "../middleware/index.js";
2
3
  import { UserService } from "../services/index.js";
3
4
  import { ApiController } from "./api.controller.js";
4
- export const usersRouteAuth = {
5
- read: true,
6
- create: true,
7
- update: true,
8
- delete: ["admin"],
9
- };
10
5
  export class UsersController extends ApiController {
11
6
  userService;
12
- constructor(app, database, options = {}) {
13
- const userSpec = options.userSpec ?? UserSpec;
14
- const publicUserSpec = options.publicUserSpec ?? PublicUserSpec;
15
- const userService = options.userService ?? new UserService(database, userSpec);
16
- super("users", app, userService, options.routeAuth ?? usersRouteAuth, "user", userSpec, publicUserSpec);
17
- this.userService = userService;
7
+ constructor(app, database, options = {
8
+ userService: new UserService(database),
9
+ userSpec: UserSpec,
10
+ publicUserSpec: PublicUserSpec,
11
+ methodAuth: authenticated,
12
+ }) {
13
+ super("users", app, options.userService, options.methodAuth, "user", options.userSpec, options.publicUserSpec);
14
+ this.userService = options.userService;
18
15
  }
19
16
  }
@@ -1,5 +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
+ export * from "../models/method-auth.model.js";
5
5
  export * from "./request-lifecycle.js";
@@ -1,5 +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
+ export * from "../models/method-auth.model.js";
5
5
  export * from "./request-lifecycle.js";
@@ -1,4 +1,4 @@
1
1
  import type { NextFunction, Request, Response } from "express";
2
- import type { MethodAuth } from "./method-auth.model.js";
2
+ import type { MethodAuth } from "../models/method-auth.model.js";
3
3
  declare const isAuthorized: (config: MethodAuth) => (req: Request, _res: Response, next: NextFunction) => void;
4
4
  export { isAuthorized };
@@ -1,5 +1,5 @@
1
1
  import jwt from "jsonwebtoken";
2
- import { UnauthenticatedError, UnauthorizedError, } from "../errors/index.js";
2
+ import { BadRequestError, 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
5
  function resolveAuthMethod(req) {
@@ -18,13 +18,6 @@ function resolveAuthMethod(req) {
18
18
  return null;
19
19
  }
20
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
21
  function assertFeatureRequirement(userContext, requirement) {
29
22
  if (requirement === undefined) {
30
23
  throw new UnauthorizedError();
@@ -32,7 +25,7 @@ function assertFeatureRequirement(userContext, requirement) {
32
25
  if (requirement === true) {
33
26
  return;
34
27
  }
35
- if (!requirement.some((feature) => userHasFeature(userContext, feature))) {
28
+ if (!requirement.some((feature) => userContext.authorizations.some((authorization) => authorization.feature === feature))) {
36
29
  throw new UnauthorizedError();
37
30
  }
38
31
  }
@@ -53,13 +46,9 @@ const isAuthorized = (config) => {
53
46
  const rawPayload = jwt.verify(token, authConfig.clientSecret);
54
47
  const userContext = getAuthUserContextSpec().decode(rawPayload);
55
48
  req.userContext = userContext;
56
- if (isAdmin(userContext)) {
57
- next();
58
- return;
59
- }
60
49
  const method = resolveAuthMethod(req);
61
50
  if (!method) {
62
- throw new UnauthorizedError();
51
+ throw new BadRequestError("Invalid HTTP method");
63
52
  }
64
53
  assertFeatureRequirement(userContext, config[method]);
65
54
  next();
@@ -4,9 +4,6 @@ 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;
10
7
  getById(userContext: IUserContext, id: AppIdType): Promise<IUser>;
11
8
  get(userContext: IUserContext, queryOptions?: IQueryOptions): Promise<IPagedResult<IUser>>;
12
9
  getAll(userContext: IUserContext): Promise<IUser[]>;
@@ -1,61 +1,45 @@
1
1
  import { UserSpec, } from "@loomcore/common/models";
2
- import { BadRequestError, ServerError, UnauthorizedError, } from "../errors/index.js";
2
+ import { BadRequestError, ServerError } from "../errors/index.js";
3
+ import { assertAdmin, assertSelfOrAdmin, } from "../utils/auth/index.js";
3
4
  import { passwordUtils } from "../utils/password.utils.js";
4
5
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
5
6
  export class UserService extends MultiTenantApiService {
6
7
  constructor(database, modelSpec = UserSpec) {
7
8
  super(database, "users", "user", modelSpec);
8
9
  }
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
- }
26
10
  async getById(userContext, id) {
27
- this.assertSelfOrAdmin(userContext, id);
11
+ assertSelfOrAdmin(userContext, id);
28
12
  return super.getById(userContext, id);
29
13
  }
30
14
  async get(userContext, queryOptions) {
31
- this.assertAdmin(userContext);
15
+ assertAdmin(userContext);
32
16
  return super.get(userContext, queryOptions);
33
17
  }
34
18
  async getAll(userContext) {
35
- this.assertAdmin(userContext);
19
+ assertAdmin(userContext);
36
20
  return super.getAll(userContext);
37
21
  }
38
22
  async getCount(userContext) {
39
- this.assertAdmin(userContext);
23
+ assertAdmin(userContext);
40
24
  return super.getCount(userContext);
41
25
  }
42
26
  async fullUpdateById(_userContext, _id, _entity) {
43
27
  throw new ServerError("User full update is not allowed.");
44
28
  }
45
29
  async update(userContext, queryObject, entity) {
46
- this.assertAdmin(userContext);
30
+ assertAdmin(userContext);
47
31
  this.assertPasswordUpdateAllowed(userContext, null, entity, false);
48
32
  return super.update(userContext, queryObject, entity);
49
33
  }
50
34
  async batchUpdate(userContext, entities) {
51
- this.assertAdmin(userContext);
35
+ assertAdmin(userContext);
52
36
  for (const entity of entities) {
53
37
  this.assertPasswordUpdateAllowed(userContext, null, entity, false);
54
38
  }
55
39
  return super.batchUpdate(userContext, entities);
56
40
  }
57
41
  async partialUpdateById(userContext, id, entity, allowPasswordUpdate = false) {
58
- this.assertSelfOrAdmin(userContext, id);
42
+ assertSelfOrAdmin(userContext, id);
59
43
  this.assertPasswordUpdateAllowed(userContext, id, entity, allowPasswordUpdate);
60
44
  return super.partialUpdateById(userContext, id, entity);
61
45
  }
@@ -0,0 +1,2 @@
1
+ import type { IUserContext } from "@loomcore/common/models";
2
+ export declare function assertAdmin(userContext: IUserContext): void;
@@ -0,0 +1,7 @@
1
+ import { UnauthorizedError } from "../../errors/index.js";
2
+ import { isAdmin } from "./is-admin.util.js";
3
+ export function assertAdmin(userContext) {
4
+ if (!isAdmin(userContext)) {
5
+ throw new UnauthorizedError();
6
+ }
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { IAuditable, IUserContext } from "@loomcore/common/models";
2
+ export declare function assertOwnerOrAdmin(userContext: IUserContext, entity: IAuditable): void;
@@ -0,0 +1,10 @@
1
+ import { UnauthorizedError } from "../../errors/index.js";
2
+ import { isAdmin } from "./is-admin.util.js";
3
+ export function assertOwnerOrAdmin(userContext, entity) {
4
+ if (isAdmin(userContext)) {
5
+ return;
6
+ }
7
+ if (userContext.user._id !== entity._createdBy) {
8
+ throw new UnauthorizedError();
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { IUserContext } from "@loomcore/common/models";
2
+ import type { AppIdType } from "@loomcore/common/types";
3
+ export declare function assertSelfOrAdmin(userContext: IUserContext, userId: AppIdType): void;
@@ -0,0 +1,10 @@
1
+ import { UnauthorizedError } from "../../errors/index.js";
2
+ import { isAdmin } from "./is-admin.util.js";
3
+ export function assertSelfOrAdmin(userContext, userId) {
4
+ if (isAdmin(userContext)) {
5
+ return;
6
+ }
7
+ if (userContext.user._id !== userId) {
8
+ throw new UnauthorizedError();
9
+ }
10
+ }
@@ -1,15 +1,4 @@
1
1
  import { type IModelSpec } from "@loomcore/common/models";
2
- export declare function createUserContextSpec(userModelSpec: IModelSpec): IModelSpec;
3
- export declare function createLoginResponseSpec(publicUserContextSpec: IModelSpec): IModelSpec;
2
+ export declare function createLoginResponseSpec(userContextSpec: IModelSpec): IModelSpec;
4
3
  export declare function setAuthUserContextSpec(spec: IModelSpec): void;
5
4
  export declare function getAuthUserContextSpec(): IModelSpec;
6
- export declare function resetAuthUserContextSpec(): void;
7
- export declare function resolveAuthUserSpecs(options: {
8
- userSpec?: IModelSpec;
9
- publicUserSpec?: IModelSpec;
10
- }): {
11
- userSpec: IModelSpec;
12
- publicUserSpec: IModelSpec;
13
- publicUserContextSpec: IModelSpec;
14
- loginResponseSpec: IModelSpec;
15
- };
@@ -1,17 +1,10 @@
1
- import { OrganizationSpec, PublicUserSpec, TokenResponseSchema, UserContextAuthorizationSpec, UserContextSpec, UserSpec, } from "@loomcore/common/models";
1
+ import { TokenResponseSchema, UserContextSpec, } from "@loomcore/common/models";
2
2
  import { entityUtils } from "@loomcore/common/utils";
3
3
  import { Type } from "@sinclair/typebox";
4
- export function createUserContextSpec(userModelSpec) {
5
- return entityUtils.getModelSpec(Type.Object({
6
- user: userModelSpec.fullSchema,
7
- authorizations: Type.Array(UserContextAuthorizationSpec.fullSchema),
8
- organization: OrganizationSpec.fullSchema,
9
- }), { isEntity: false });
10
- }
11
- export function createLoginResponseSpec(publicUserContextSpec) {
4
+ export function createLoginResponseSpec(userContextSpec) {
12
5
  return entityUtils.getModelSpec(Type.Object({
13
6
  tokens: TokenResponseSchema,
14
- userContext: publicUserContextSpec.fullSchema,
7
+ userContext: userContextSpec.fullSchema,
15
8
  }), { isEntity: false });
16
9
  }
17
10
  let authUserContextSpec = UserContextSpec;
@@ -21,18 +14,3 @@ export function setAuthUserContextSpec(spec) {
21
14
  export function getAuthUserContextSpec() {
22
15
  return authUserContextSpec;
23
16
  }
24
- export function resetAuthUserContextSpec() {
25
- authUserContextSpec = UserContextSpec;
26
- }
27
- export function resolveAuthUserSpecs(options) {
28
- const userSpec = options.userSpec ?? UserSpec;
29
- const publicUserSpec = options.publicUserSpec ?? PublicUserSpec;
30
- const publicUserContextSpec = createUserContextSpec(publicUserSpec);
31
- const loginResponseSpec = createLoginResponseSpec(publicUserContextSpec);
32
- return {
33
- userSpec,
34
- publicUserSpec,
35
- publicUserContextSpec,
36
- loginResponseSpec,
37
- };
38
- }
@@ -1,3 +1,6 @@
1
+ export * from "./assert-admin.util.js";
2
+ export * from "./assert-owner-or-admin.util.js";
3
+ export * from "./assert-self-or-admin.util.js";
1
4
  export * from "./attempt-login.util.js";
2
5
  export * from "./auth-user-specs.util.js";
3
6
  export * from "./change-password.util.js";
@@ -12,6 +15,7 @@ export * from "./get-device-id-from-cookie.util.js";
12
15
  export * from "./get-expires-on-from-days.util.js";
13
16
  export * from "./get-expires-on-from-minutes.util.js";
14
17
  export * from "./get-expires-on-from-seconds.util.js";
18
+ export * from "./is-admin.util.js";
15
19
  export * from "./log-user-in.util.js";
16
20
  export * from "./request-token-using-refresh-token.util.js";
17
21
  export * from "./reset-password.util.js";
@@ -1,3 +1,6 @@
1
+ export * from "./assert-admin.util.js";
2
+ export * from "./assert-owner-or-admin.util.js";
3
+ export * from "./assert-self-or-admin.util.js";
1
4
  export * from "./attempt-login.util.js";
2
5
  export * from "./auth-user-specs.util.js";
3
6
  export * from "./change-password.util.js";
@@ -12,6 +15,7 @@ export * from "./get-device-id-from-cookie.util.js";
12
15
  export * from "./get-expires-on-from-days.util.js";
13
16
  export * from "./get-expires-on-from-minutes.util.js";
14
17
  export * from "./get-expires-on-from-seconds.util.js";
18
+ export * from "./is-admin.util.js";
15
19
  export * from "./log-user-in.util.js";
16
20
  export * from "./request-token-using-refresh-token.util.js";
17
21
  export * from "./reset-password.util.js";
@@ -0,0 +1,2 @@
1
+ import type { IUserContext } from "@loomcore/common/models";
2
+ export declare function isAdmin(userContext: IUserContext): boolean;
@@ -0,0 +1,4 @@
1
+ export function isAdmin(userContext) {
2
+ return userContext.authorizations.some((authorization) => authorization.feature === "admin" ||
3
+ authorization.feature === "system");
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
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": {