@naturalcycles/backend-lib 9.73.1 → 9.75.0

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.
@@ -1,6 +1,9 @@
1
1
  import type { BackendRequestHandler } from '../server/server.model.js';
2
2
  import type { BaseAdminService } from './base.admin.service.js';
3
3
  import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js';
4
+ /**
5
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg`.
6
+ */
4
7
  export interface RequireAdminCfg {
5
8
  /**
6
9
  * @default '/login.html'
@@ -25,14 +28,28 @@ export interface RequireAdminCfg {
25
28
  */
26
29
  andComparison?: boolean;
27
30
  }
31
+ /**
32
+ * @deprecated Use `AuthMiddleware`.
33
+ */
28
34
  export type AdminMiddleware = (reqPermissions?: string[], cfg?: RequireAdminCfg) => BackendRequestHandler;
35
+ /**
36
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
37
+ */
29
38
  export declare function createAdminMiddleware(adminService: BaseAdminService, cfgDefaults?: RequireAdminCfg): AdminMiddleware;
30
39
  /**
31
40
  * Require Admin permission.
32
41
  * If not authenticated - will redirect to `loginHtmlPath` (default to /login.html).
33
42
  * If authenticated, but not authorized - will throw 403.
34
43
  * Otherwise will just pass.
44
+ *
45
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
35
46
  */
36
47
  export declare function requireAdminPermissions(adminService: BaseAdminService, reqPermissions?: string[], cfg?: RequireAdminCfg): BackendRequestHandler;
48
+ /**
49
+ * @deprecated Use `loginHtmlHandler`.
50
+ */
37
51
  export declare function loginHtml(firebaseServiceCfg: FirebaseSharedServiceCfg): BackendRequestHandler;
52
+ /**
53
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
54
+ */
38
55
  export declare function getLoginHtmlRedirect(href: string): string;
@@ -2,6 +2,9 @@ import { _memoized } from '@naturalcycles/js-lib/decorators';
2
2
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
3
3
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
4
4
  import { srcDir } from '../paths.cnst.js';
5
+ /**
6
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
7
+ */
5
8
  export function createAdminMiddleware(adminService, cfgDefaults = {}) {
6
9
  return (reqPermissions, cfg) => requireAdminPermissions(adminService, reqPermissions, {
7
10
  ...cfgDefaults,
@@ -13,6 +16,8 @@ export function createAdminMiddleware(adminService, cfgDefaults = {}) {
13
16
  * If not authenticated - will redirect to `loginHtmlPath` (default to /login.html).
14
17
  * If authenticated, but not authorized - will throw 403.
15
18
  * Otherwise will just pass.
19
+ *
20
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
16
21
  */
17
22
  export function requireAdminPermissions(adminService, reqPermissions = [], cfg = {}) {
18
23
  const { loginHtmlPath = '/login.html', urlStartsWith, apiHost, autoLogin = true, andComparison = true, } = cfg;
@@ -36,6 +41,9 @@ export function requireAdminPermissions(adminService, reqPermissions = [], cfg =
36
41
  }
37
42
  };
38
43
  }
44
+ /**
45
+ * @deprecated Use `loginHtmlHandler`.
46
+ */
39
47
  export function loginHtml(firebaseServiceCfg) {
40
48
  const { apiKey: firebaseApiKey, authDomain: firebaseAuthDomain, adminAuthProvider: firebaseAuthProvider = 'GoogleAuthProvider', } = firebaseServiceCfg;
41
49
  return (_req, res) => {
@@ -54,6 +62,9 @@ const getLoginHtml = _memoized((cfg) => {
54
62
  .replaceAll('<%= firebaseAuthDomain %>', cfg.firebaseAuthDomain)
55
63
  .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider);
56
64
  });
65
+ /**
66
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
67
+ */
57
68
  export function getLoginHtmlRedirect(href) {
58
69
  return `
59
70
  <html>
@@ -3,6 +3,8 @@ import type { Auth } from 'firebase-admin/auth';
3
3
  import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
4
4
  /**
5
5
  * Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
6
+ *
7
+ * @deprecated Use `AuthService`, with `FirebaseAuthService` for `authenticate`.
6
8
  */
7
9
  export declare class BaseAdminService {
8
10
  private loadFirebaseAuth;
@@ -61,6 +63,9 @@ export declare class BaseAdminService {
61
63
  */
62
64
  getFirebaseAuthLoginHandler(): BackendRequestHandler;
63
65
  }
66
+ /**
67
+ * @deprecated Use `AuthServiceCfg` and `FirebaseAuthServiceCfg`.
68
+ */
64
69
  export interface AdminServiceCfg {
65
70
  /**
66
71
  * @default 'admin_token'
@@ -73,16 +78,25 @@ export interface AdminServiceCfg {
73
78
  */
74
79
  authEnabled?: boolean;
75
80
  }
81
+ /**
82
+ * @deprecated Use `PermissionInfo`.
83
+ */
76
84
  export interface AdminInfo {
77
85
  email: string;
78
86
  permissions: string[];
79
87
  }
88
+ /**
89
+ * @deprecated Use `ResolvedAuth`.
90
+ */
80
91
  export interface ResolvedAdmin {
81
92
  /** undefined - no valid admin token */
82
93
  email?: string;
83
94
  /** undefined - not an Admin */
84
95
  permissions?: Set<string>;
85
96
  }
97
+ /**
98
+ * @deprecated Use `AuthCheckOptions`.
99
+ */
86
100
  export interface CheckPermissionsOptions {
87
101
  /**
88
102
  * false - one granted permission is enough.
@@ -91,6 +105,9 @@ export interface CheckPermissionsOptions {
91
105
  */
92
106
  andComparison?: boolean;
93
107
  }
108
+ /**
109
+ * @deprecated Use `AuthService.has` / `AuthService.require`.
110
+ */
94
111
  export interface CheckPermissionsResult {
95
112
  /** undefined - no valid admin token */
96
113
  email?: string;
@@ -4,6 +4,8 @@ import { _assert } from '@naturalcycles/js-lib/error';
4
4
  import { dimGrey, green, red } from '@naturalcycles/nodejs-lib/colors';
5
5
  /**
6
6
  * Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
7
+ *
8
+ * @deprecated Use `AuthService`, with `FirebaseAuthService` for `authenticate`.
7
9
  */
8
10
  export class BaseAdminService {
9
11
  loadFirebaseAuth;
@@ -168,7 +170,7 @@ export class BaseAdminService {
168
170
  err?.code?.includes('id-token-expired')) {
169
171
  return; // skip logging, expected error
170
172
  }
171
- req.error(`getEmailByToken error:`, err);
173
+ req.error({ msg: `getEmailByToken error:`, err });
172
174
  }
173
175
  }
174
176
  async getFirebaseAuth() {
@@ -191,7 +193,10 @@ export class BaseAdminService {
191
193
  */
192
194
  // oxlint-disable-next-line max-params
193
195
  async onPermissionCheck(req, email, reqPermissions, required, granted, meta = {}) {
194
- req.log(`${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(reqPermissions.join(', '))}]: ${granted ? green('GRANTED') : red('DENIED')}`, meta);
196
+ req.log({
197
+ ...meta,
198
+ msg: `${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(reqPermissions.join(', '))}]: ${granted ? green('GRANTED') : red('DENIED')}`,
199
+ });
195
200
  }
196
201
  /**
197
202
  * Install it on POST /admin/login url
@@ -0,0 +1,41 @@
1
+ import type { Auth } from 'firebase-admin/auth';
2
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
3
+ import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js';
4
+ /**
5
+ * Verifies Firebase Auth tokens.
6
+ */
7
+ export declare class FirebaseAuthService {
8
+ private loadFirebaseAuth;
9
+ cfg: FirebaseAuthServiceCfg;
10
+ constructor(loadFirebaseAuth: () => Promise<FirebaseTokenVerifier>, cfg: FirebaseAuthServiceCfg);
11
+ /**
12
+ * Reads the configured cookie first, then the header.
13
+ */
14
+ getToken(req: BackendRequest): string | undefined;
15
+ /**
16
+ * Returns undefined if there's no token, or it's expired.
17
+ * Rejects on any other verification failure.
18
+ */
19
+ getEmailByToken(token: string | undefined): Promise<string | undefined>;
20
+ private getFirebaseAuth;
21
+ /**
22
+ * POST handler that stores the token from the `Authentication` header in `tokenCookie` (not validated).
23
+ * `Authentication: logout` clears it. The bundled login.html posts to `/admin/login`.
24
+ */
25
+ getLoginHandler(): BackendRequestHandler;
26
+ }
27
+ /**
28
+ * Serves login.html, configured for Firebase Auth.
29
+ */
30
+ export declare function loginHtmlHandler(firebaseServiceCfg: FirebaseSharedServiceCfg): BackendRequestHandler;
31
+ /**
32
+ * The part of Firebase `Auth` that FirebaseAuthService uses.
33
+ */
34
+ export type FirebaseTokenVerifier = Pick<Auth, 'verifyIdToken'>;
35
+ export interface FirebaseAuthServiceCfg {
36
+ /**
37
+ * Read by `getToken`, set by `getLoginHandler`.
38
+ */
39
+ tokenCookie?: string;
40
+ tokenHeader?: string;
41
+ }
@@ -0,0 +1,103 @@
1
+ import { __decorate } from "tslib";
2
+ import { _Memo, _memoized } from '@naturalcycles/js-lib/decorators';
3
+ import { _assert } from '@naturalcycles/js-lib/error';
4
+ import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
5
+ import { srcDir } from '../paths.cnst.js';
6
+ /**
7
+ * Verifies Firebase Auth tokens.
8
+ */
9
+ export class FirebaseAuthService {
10
+ loadFirebaseAuth;
11
+ cfg;
12
+ constructor(loadFirebaseAuth, cfg) {
13
+ this.loadFirebaseAuth = loadFirebaseAuth;
14
+ this.cfg = cfg;
15
+ }
16
+ /**
17
+ * Reads the configured cookie first, then the header.
18
+ */
19
+ getToken(req) {
20
+ const { tokenCookie, tokenHeader } = this.cfg;
21
+ if (tokenCookie) {
22
+ const token = req.cookies?.[tokenCookie];
23
+ if (token)
24
+ return token;
25
+ }
26
+ if (tokenHeader)
27
+ return req.header(tokenHeader);
28
+ }
29
+ /**
30
+ * Returns undefined if there's no token, or it's expired.
31
+ * Rejects on any other verification failure.
32
+ */
33
+ async getEmailByToken(token) {
34
+ if (!token)
35
+ return;
36
+ const auth = await this.getFirebaseAuth();
37
+ try {
38
+ const decodedToken = await auth.verifyIdToken(token);
39
+ return decodedToken.email;
40
+ }
41
+ catch (err) {
42
+ if (err?.hasCode?.('id-token-expired'))
43
+ return;
44
+ throw err;
45
+ }
46
+ }
47
+ async getFirebaseAuth() {
48
+ return await this.loadFirebaseAuth();
49
+ }
50
+ /**
51
+ * POST handler that stores the token from the `Authentication` header in `tokenCookie` (not validated).
52
+ * `Authentication: logout` clears it. The bundled login.html posts to `/admin/login`.
53
+ */
54
+ getLoginHandler() {
55
+ const { tokenCookie } = this.cfg;
56
+ _assert(tokenCookie, 'getLoginHandler requires tokenCookie to be configured');
57
+ return async (req, res) => {
58
+ const token = req.header('authentication');
59
+ _assert(token, `401 Unauthenticated`, {
60
+ userFriendly: true,
61
+ backendResponseStatusCode: 401,
62
+ });
63
+ let maxAge = 1000 * 60 * 60 * 24 * 30; // 30 days
64
+ if (token === 'logout') {
65
+ // delete the cookie
66
+ maxAge = 0;
67
+ }
68
+ res
69
+ .cookie(tokenCookie, token, {
70
+ maxAge,
71
+ sameSite: 'lax', // can be: none, lax, strict
72
+ // comment these 2 lines to debug on localhost
73
+ httpOnly: true,
74
+ secure: true,
75
+ })
76
+ .status(204)
77
+ .end();
78
+ };
79
+ }
80
+ }
81
+ __decorate([
82
+ _Memo()
83
+ ], FirebaseAuthService.prototype, "getFirebaseAuth", null);
84
+ /**
85
+ * Serves login.html, configured for Firebase Auth.
86
+ */
87
+ export function loginHtmlHandler(firebaseServiceCfg) {
88
+ const { apiKey: firebaseApiKey, authDomain: firebaseAuthDomain, adminAuthProvider: firebaseAuthProvider = 'GoogleAuthProvider', } = firebaseServiceCfg;
89
+ return (_req, res) => {
90
+ res.send(getLoginHtml({
91
+ firebaseApiKey,
92
+ firebaseAuthDomain,
93
+ firebaseAuthProvider,
94
+ }));
95
+ };
96
+ }
97
+ const getLoginHtml = _memoized((cfg) => {
98
+ return fs2
99
+ .readText(`${srcDir}/admin/login.html`)
100
+ .replaceAll('<%= firebaseApiKey %>', cfg.firebaseApiKey)
101
+ .replaceAll('<%= firebaseAuthDomain %>', cfg.firebaseAuthDomain)
102
+ .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider);
103
+ });
@@ -1,4 +1,5 @@
1
1
  export * from './adminMiddleware.js';
2
2
  export * from './base.admin.service.js';
3
3
  export * from './firebase.shared.service.js';
4
+ export * from './firebaseAuth.service.js';
4
5
  export * from './secureHeaderMiddleware.js';
@@ -1,4 +1,5 @@
1
1
  export * from './adminMiddleware.js';
2
2
  export * from './base.admin.service.js';
3
3
  export * from './firebase.shared.service.js';
4
+ export * from './firebaseAuth.service.js';
4
5
  export * from './secureHeaderMiddleware.js';
@@ -1,5 +1,8 @@
1
1
  import type { AdminMiddleware, RequireAdminCfg } from './adminMiddleware.js';
2
2
  import type { BaseAdminService } from './base.admin.service.js';
3
+ /**
4
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg` with `secureHeader`.
5
+ */
3
6
  export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
4
7
  adminService: BaseAdminService;
5
8
  /**
@@ -14,5 +17,7 @@ export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
14
17
  /**
15
18
  * Secures the endpoint by requiring a secret header to be present.
16
19
  * Throws Error401Admin otherwise.
20
+ *
21
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware` with `secureHeader`.
17
22
  */
18
23
  export declare function createSecureHeaderMiddleware(cfg: SecureHeaderMiddlewareCfg): AdminMiddleware;
@@ -4,6 +4,8 @@ import { requireAdminPermissions } from './adminMiddleware.js';
4
4
  /**
5
5
  * Secures the endpoint by requiring a secret header to be present.
6
6
  * Throws Error401Admin otherwise.
7
+ *
8
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware` with `secureHeader`.
7
9
  */
8
10
  export function createSecureHeaderMiddleware(cfg) {
9
11
  return reqPermissions => requireSecureHeaderOrAdmin(cfg, reqPermissions);
@@ -0,0 +1,119 @@
1
+ import type { AnyObject, Promisable } from '@naturalcycles/js-lib/types';
2
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
3
+ /**
4
+ * Authenticates requests and checks permissions.
5
+ */
6
+ export declare class AuthService<CTX = AnyObject> {
7
+ constructor(cfg: AuthServiceCfg<CTX>);
8
+ cfg: AuthServiceCfg<CTX> & {
9
+ authEnabled: boolean;
10
+ };
11
+ /**
12
+ * Responds JSON 401 / 403.
13
+ */
14
+ getMiddleware(cfg?: AuthMiddlewareCfg<CTX>): AuthMiddleware;
15
+ /**
16
+ * Like `getMiddleware`, but 401 redirects to the login page.
17
+ */
18
+ getLoginRedirectMiddleware(cfg: AuthLoginRedirectMiddlewareCfg<CTX>): AuthMiddleware;
19
+ private createHandler;
20
+ /**
21
+ * Runs `authenticate`, then `getPermissions` and `isAlwaysAuthorized` if there's a subject.
22
+ * Ignores `authEnabled`.
23
+ */
24
+ resolve(req: BackendRequest): Promise<ResolvedAuth>;
25
+ /**
26
+ * Throws 401 if there's no subject, 403 if not granted.
27
+ * Returns the granted permissions (all of the subject's if alwaysAuthorized),
28
+ * or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
29
+ */
30
+ require(resolved: ResolvedAuth | undefined, reqPermissions?: string[], opt?: AuthCheckOptions<CTX>): PermissionInfo;
31
+ private checkSubject;
32
+ private assertAuthenticated;
33
+ private authRequiredData;
34
+ private assertGranted;
35
+ /**
36
+ * undefined - not granted.
37
+ */
38
+ private getGrantedPermissions;
39
+ private getLoginHref;
40
+ private getLoginHtmlRedirect;
41
+ }
42
+ export interface AuthServiceCfg<CTX> {
43
+ /**
44
+ * Returns the subject, or undefined if not authenticated.
45
+ */
46
+ authenticate: (req: BackendRequest) => Promise<string | undefined>;
47
+ /**
48
+ * undefined - no permission set (never granted).
49
+ */
50
+ getPermissions: (subject: string, req: BackendRequest) => Promise<string[] | undefined>;
51
+ /**
52
+ * true - every check is granted for this subject.
53
+ */
54
+ isAlwaysAuthorized?: (subject: string, permissions: string[] | undefined, req: BackendRequest) => Promisable<boolean>;
55
+ /**
56
+ * Called on every `require` check that has a subject, while auth is enabled. Must not throw.
57
+ */
58
+ onCheck?: (check: AuthCheck, ctx: (CTX & AnyObject) | undefined) => void;
59
+ /**
60
+ * false - every check is granted. For tests / local debugging. Read on every check.
61
+ *
62
+ * @default true
63
+ */
64
+ authEnabled?: boolean;
65
+ }
66
+ export interface AuthCheck {
67
+ subject: string;
68
+ granted: boolean;
69
+ alwaysAuthorized: boolean;
70
+ reqPermissions: string[];
71
+ /** The granted permissions, or reqPermissions if denied */
72
+ checkedPermissions: string[];
73
+ }
74
+ export interface AuthLoginRedirectMiddlewareCfg<CTX> extends AuthMiddlewareCfg<CTX> {
75
+ loginHtmlPath: string;
76
+ }
77
+ export interface AuthMiddlewareCfg<CTX> {
78
+ /**
79
+ * Defaults to `resolve(req)`.
80
+ */
81
+ getResolved?: (req: BackendRequest) => Promisable<ResolvedAuth | undefined>;
82
+ getCtx?: (req: BackendRequest) => (CTX & AnyObject) | undefined;
83
+ /**
84
+ * @default true
85
+ */
86
+ andComparison?: boolean;
87
+ /**
88
+ * A request with this header passes without auth. A wrong value responds 401.
89
+ */
90
+ secureHeader?: SecureHeaderCfg;
91
+ }
92
+ export interface SecureHeaderCfg {
93
+ /** e.g. 'Authorization' */
94
+ name: string;
95
+ value: string;
96
+ }
97
+ export type AuthMiddleware = (reqPermissions?: string[]) => BackendRequestHandler;
98
+ export interface AuthCheckOptions<CTX> {
99
+ /**
100
+ * false - one granted permission is enough.
101
+ *
102
+ * @default true
103
+ */
104
+ andComparison?: boolean;
105
+ /** Passed to onCheck: the app's CTX, plus any extra keys */
106
+ ctx?: CTX & AnyObject;
107
+ }
108
+ export interface PermissionInfo {
109
+ subject: string;
110
+ permissions: string[];
111
+ }
112
+ export interface ResolvedAuth {
113
+ /** undefined - not authenticated */
114
+ subject?: string;
115
+ /** undefined - no permission set */
116
+ permissions?: string[];
117
+ /** Result of `isAlwaysAuthorized` */
118
+ alwaysAuthorized?: boolean;
119
+ }
@@ -0,0 +1,147 @@
1
+ import { _assert, AppError } from '@naturalcycles/js-lib/error';
2
+ import { timingSafeStringEqual } from '@naturalcycles/nodejs-lib';
3
+ /**
4
+ * Authenticates requests and checks permissions.
5
+ */
6
+ export class AuthService {
7
+ constructor(cfg) {
8
+ this.cfg = {
9
+ authEnabled: true,
10
+ ...cfg,
11
+ };
12
+ }
13
+ cfg;
14
+ /**
15
+ * Responds JSON 401 / 403.
16
+ */
17
+ getMiddleware(cfg = {}) {
18
+ return reqPermissions => this.createHandler(cfg, reqPermissions);
19
+ }
20
+ /**
21
+ * Like `getMiddleware`, but 401 redirects to the login page.
22
+ */
23
+ getLoginRedirectMiddleware(cfg) {
24
+ const loginHref = this.getLoginHref(cfg);
25
+ return reqPermissions => this.createHandler(cfg, reqPermissions, loginHref);
26
+ }
27
+ createHandler(cfg, reqPermissions = [], loginHref) {
28
+ const { getResolved = req => this.resolve(req), getCtx, andComparison = true, secureHeader, } = cfg;
29
+ return async (req, res, next) => {
30
+ if (!this.cfg.authEnabled)
31
+ return next();
32
+ if (secureHeader) {
33
+ const providedHeader = req.get(secureHeader.name);
34
+ if (providedHeader) {
35
+ if (timingSafeStringEqual(providedHeader, secureHeader.value)) {
36
+ return next();
37
+ }
38
+ return next(new AppError('secureHeader or authentication required', this.authRequiredData()));
39
+ }
40
+ }
41
+ try {
42
+ const resolved = await getResolved(req);
43
+ if (!resolved?.subject && loginHref) {
44
+ res.status(401).send(this.getLoginHtmlRedirect(loginHref));
45
+ return;
46
+ }
47
+ this.require(resolved, reqPermissions, { andComparison, ctx: getCtx?.(req) });
48
+ return next();
49
+ }
50
+ catch (err) {
51
+ return next(err);
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Runs `authenticate`, then `getPermissions` and `isAlwaysAuthorized` if there's a subject.
57
+ * Ignores `authEnabled`.
58
+ */
59
+ async resolve(req) {
60
+ const { authenticate, getPermissions, isAlwaysAuthorized } = this.cfg;
61
+ const subject = await authenticate(req);
62
+ if (!subject)
63
+ return {};
64
+ const permissions = await getPermissions(subject, req);
65
+ const alwaysAuthorized = isAlwaysAuthorized
66
+ ? await isAlwaysAuthorized(subject, permissions, req)
67
+ : false;
68
+ return { subject, permissions, alwaysAuthorized };
69
+ }
70
+ /**
71
+ * Throws 401 if there's no subject, 403 if not granted.
72
+ * Returns the granted permissions (all of the subject's if alwaysAuthorized),
73
+ * or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
74
+ */
75
+ require(resolved, reqPermissions = [], opt = {}) {
76
+ if (!this.cfg.authEnabled)
77
+ return { subject: 'authDisabled', permissions: [] };
78
+ this.assertAuthenticated(resolved);
79
+ const { subject, permissions, alwaysAuthorized } = resolved;
80
+ const grantedPermissions = this.checkSubject(resolved, reqPermissions, opt);
81
+ this.assertGranted(subject, grantedPermissions, reqPermissions);
82
+ if (alwaysAuthorized) {
83
+ return { subject, permissions: permissions?.slice() || [] };
84
+ }
85
+ return { subject, permissions: grantedPermissions.slice() };
86
+ }
87
+ checkSubject(resolved, reqPermissions, opt) {
88
+ const { subject, permissions, alwaysAuthorized = false } = resolved;
89
+ const { andComparison = true, ctx } = opt;
90
+ const grantedPermissions = alwaysAuthorized
91
+ ? reqPermissions
92
+ : this.getGrantedPermissions(permissions, reqPermissions, andComparison);
93
+ this.cfg.onCheck?.({
94
+ subject,
95
+ granted: !!grantedPermissions,
96
+ alwaysAuthorized,
97
+ reqPermissions,
98
+ checkedPermissions: grantedPermissions || reqPermissions,
99
+ }, ctx);
100
+ return grantedPermissions;
101
+ }
102
+ assertAuthenticated(resolved) {
103
+ _assert(resolved?.subject, 'authentication required', this.authRequiredData());
104
+ }
105
+ authRequiredData() {
106
+ return {
107
+ authRequired: true,
108
+ backendResponseStatusCode: 401,
109
+ userFriendly: true,
110
+ };
111
+ }
112
+ assertGranted(subject, grantedPermissions, reqPermissions) {
113
+ _assert(grantedPermissions, `Permissions required: [${reqPermissions.join(', ')}]`, {
114
+ permissionsRequired: reqPermissions,
115
+ subject,
116
+ backendResponseStatusCode: 403,
117
+ userFriendly: true,
118
+ });
119
+ }
120
+ /**
121
+ * undefined - not granted.
122
+ */
123
+ getGrantedPermissions(permissions, reqPermissions, andComparison) {
124
+ if (!permissions)
125
+ return;
126
+ const grantedPermissions = reqPermissions.filter(p => permissions.includes(p));
127
+ const granted = andComparison
128
+ ? grantedPermissions.length === reqPermissions.length
129
+ : grantedPermissions.length > 0;
130
+ return granted ? grantedPermissions : undefined;
131
+ }
132
+ getLoginHref(cfg) {
133
+ return `${cfg.loginHtmlPath}?autoLogin=1&returnUrl=\${encodeURIComponent(location.href)}`;
134
+ }
135
+ getLoginHtmlRedirect(href) {
136
+ return `
137
+ <html>
138
+ <body>401 Authentication Required
139
+ <script>
140
+ document.write(\`: <a href="${href}" id="loginLink">Login</a>\`)
141
+ document.getElementById('loginLink').click()
142
+ </script>
143
+ </body>
144
+ </html>
145
+ `;
146
+ }
147
+ }
@@ -0,0 +1 @@
1
+ export * from './auth.service.js';
@@ -0,0 +1 @@
1
+ export * from './auth.service.js';
@@ -1,5 +1,7 @@
1
1
  import { _anyToError, _isErrorObject } from '@naturalcycles/js-lib/error';
2
- import { _inspect } from '@naturalcycles/nodejs-lib';
2
+ import { commonLoggerCreate } from '@naturalcycles/js-lib/log';
3
+ import { _omit } from '@naturalcycles/js-lib/object/object.util.js';
4
+ import { _inspect, logEntryToString } from '@naturalcycles/nodejs-lib';
3
5
  import { getRequestLogger } from '../server/asyncLocalStorageMiddleware.js';
4
6
  const sentrySeverityMap = {
5
7
  debug: 'log',
@@ -60,7 +62,7 @@ export class SentrySharedService {
60
62
  const data = _isErrorObject(err) ? err.data : undefined;
61
63
  // Using request-aware logger here
62
64
  // Log both the error and attached ErrorData (if any)
63
- getRequestLogger().error(...[err_, data].filter(Boolean));
65
+ getRequestLogger().error({ err: err_, ...data });
64
66
  if (data?.report === false) {
65
67
  // Skip reporting the error
66
68
  return;
@@ -81,7 +83,7 @@ export class SentrySharedService {
81
83
  * Returns "eventId"
82
84
  */
83
85
  captureMessage(msg, level) {
84
- getRequestLogger()[sentrySeverityMap[level] || 'log']('captureMessage:', msg);
86
+ getRequestLogger()[sentrySeverityMap[level] || 'log'](`captureMessage: ${msg}`);
85
87
  return this.sentry.captureMessage(msg, level);
86
88
  }
87
89
  addBreadcrumb(breadcrumb) {
@@ -95,17 +97,14 @@ export class SentrySharedService {
95
97
  * @experimental
96
98
  */
97
99
  getCommonLogger() {
98
- return {
99
- debug: () => { }, // noop
100
- log: () => { }, // noop
101
- warn: () => { }, // noop
102
- error: (...args) => {
103
- const message = args.map(arg => _inspect(arg, INSPECT_OPT)).join(' ');
104
- this.sentry.addBreadcrumb({
105
- message,
106
- });
107
- this.sentry.captureException(_anyToError(args.length === 1 ? args[0] : args));
108
- },
109
- };
100
+ return commonLoggerCreate(entry => {
101
+ if (entry.level !== 'error')
102
+ return; // noop
103
+ const { msg, err } = entry;
104
+ this.sentry.addBreadcrumb({
105
+ message: logEntryToString(entry, INSPECT_OPT),
106
+ });
107
+ this.sentry.captureException(_anyToError(err ?? msg ?? _omit(entry, ['level'])));
108
+ });
110
109
  }
111
110
  }