@unchainedshop/roles 1.1.0-1

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.
@@ -0,0 +1,7 @@
1
+ This directory and the files immediately inside it are automatically generated
2
+ when you change this package's NPM dependencies. Commit the files in this
3
+ directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
4
+ so that others run the same versions of sub-dependencies.
5
+
6
+ You should NOT check in the node_modules directory that Meteor automatically
7
+ creates; if you are using git, the .gitignore file tells git to ignore it.
@@ -0,0 +1,10 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "dependencies": {
4
+ "lodash.clone": {
5
+ "version": "4.5.0",
6
+ "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
7
+ "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg=="
8
+ }
9
+ }
10
+ }
package/.versions ADDED
@@ -0,0 +1,18 @@
1
+ babel-compiler@7.9.0
2
+ babel-runtime@1.5.1
3
+ dynamic-import@0.7.2
4
+ ecmascript@0.16.2
5
+ ecmascript-runtime@0.8.0
6
+ ecmascript-runtime-client@0.12.1
7
+ ecmascript-runtime-server@0.11.0
8
+ fetch@0.1.1
9
+ inter-process-messaging@0.1.1
10
+ local-test:unchained:roles@1.1.0
11
+ meteor@1.10.0
12
+ modern-browsers@0.1.8
13
+ modules@0.18.0
14
+ modules-runtime@0.13.0
15
+ promise@0.12.0
16
+ react-fast-refresh@0.2.3
17
+ typescript@4.5.4
18
+ unchained:roles@1.1.0
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ### v3.0.0
4
+
5
+ - Meteor 1.7 Compatibility
6
+ - Fork of https://github.com/nicolaslopezj/roles
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Roles (Unchained Engine)
2
+
3
+ This package delivers inner logic for the ACL layer of all Unchained API's.
4
+
5
+ Thanks to Nicolás López for the initial version of Roles: https://github.com/nicolaslopezj/roles
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export { Roles, Role } from './src/roles';
@@ -0,0 +1,3 @@
1
+ export { Roles, Role } from './roles';
2
+ export { has } from './utils/has';
3
+ export { isFunction } from './utils/isFunction';
@@ -0,0 +1,4 @@
1
+ export { Roles, Role } from './roles';
2
+ export { has } from './utils/has';
3
+ export { isFunction } from './utils/isFunction';
4
+ //# sourceMappingURL=roles-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles-index.js","sourceRoot":"","sources":["../src/roles-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/lib/roles.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { RoleInterface, RolesInterface } from '@unchainedshop/types/roles';
2
+ export declare const Roles: RolesInterface;
3
+ /**
4
+ * Constructs a new role
5
+ */
6
+ export declare class Role implements RoleInterface {
7
+ name: string;
8
+ allowRules: {
9
+ [name: string]: any;
10
+ };
11
+ helpers: {
12
+ [name: string]: any;
13
+ };
14
+ constructor(name: string);
15
+ /**
16
+ * Adds a helper to a role
17
+ */
18
+ helper(helper: string, func: any): void;
19
+ /**
20
+ * Adds allow properties to a role
21
+ */
22
+ allow(action: any, allow: any): void;
23
+ }
package/lib/roles.js ADDED
@@ -0,0 +1,146 @@
1
+ import clone from 'lodash.clone';
2
+ import { has } from './utils/has';
3
+ import { isFunction } from './utils/isFunction';
4
+ export const Roles = {
5
+ roles: {},
6
+ actions: [],
7
+ helpers: [],
8
+ /**
9
+ * Creates a new action
10
+ */
11
+ registerAction(name) {
12
+ if (!this.actions.includes(name)) {
13
+ this.actions.push(name);
14
+ }
15
+ },
16
+ /**
17
+ * Creates a new helper
18
+ */
19
+ registerHelper(name) {
20
+ if (!this.helpers.includes(name)) {
21
+ this.helpers.push(name);
22
+ }
23
+ },
24
+ /**
25
+ * Get user roles
26
+ */
27
+ getUserRoles(userId, roles, includeSpecial) {
28
+ const newRoles = [...(roles || [])];
29
+ if (includeSpecial) {
30
+ newRoles.push('__all__');
31
+ if (!userId) {
32
+ newRoles.push('__notLoggedIn__');
33
+ }
34
+ else {
35
+ newRoles.push('__loggedIn__');
36
+ if (!newRoles.includes('admin')) {
37
+ newRoles.push('__notAdmin__');
38
+ }
39
+ }
40
+ }
41
+ return newRoles;
42
+ },
43
+ /**
44
+ * Returns true if the user passes the allow check
45
+ */
46
+ async allow(context, roles, action, [obj, params]) {
47
+ const userRoles = Roles.getUserRoles(context.userId, roles, true);
48
+ return userRoles.reduce(async (roleIsAllowedPromise, role) => {
49
+ const roleIsAllowed = await roleIsAllowedPromise;
50
+ if (roleIsAllowed)
51
+ return true;
52
+ if (Roles.roles[role] && Roles.roles[role].allowRules && Roles.roles[role].allowRules[action]) {
53
+ return Roles.roles[role].allowRules[action].reduce(async (rulesIsAllowedPromise, allowFn) => {
54
+ const ruleIsAllowed = await rulesIsAllowedPromise;
55
+ if (ruleIsAllowed)
56
+ return true;
57
+ return allowFn(obj, params, context);
58
+ }, Promise.resolve(false));
59
+ }
60
+ return roleIsAllowed;
61
+ }, Promise.resolve(false));
62
+ },
63
+ /**
64
+ * To check if a user has permisisons to execute an action
65
+ */
66
+ userHasPermission: async (context, action, args) => {
67
+ const roles = Array.isArray(context.user?.roles) ? context.user.roles : [];
68
+ const allows = await Roles.allow(context, roles, action, args);
69
+ return allows === true;
70
+ },
71
+ /**
72
+ * Adds roles to a user
73
+ */
74
+ async addUserToRoles(context, roles) {
75
+ let userRoles = roles;
76
+ if (!Array.isArray(userRoles)) {
77
+ userRoles = [userRoles];
78
+ }
79
+ return context.modules.users.addRoles(context.userId, userRoles);
80
+ },
81
+ };
82
+ /**
83
+ * Constructs a new role
84
+ */
85
+ export class Role {
86
+ name;
87
+ allowRules;
88
+ helpers;
89
+ constructor(name) {
90
+ this.name = name;
91
+ if (has(Roles.roles, name))
92
+ throw new Error(`"${name}" role is already defined`);
93
+ this.allowRules = {};
94
+ this.helpers = {};
95
+ Roles.roles[name] = this;
96
+ }
97
+ /**
98
+ * Adds a helper to a role
99
+ */
100
+ helper(helper, func) {
101
+ if (!Roles.helpers.includes(helper)) {
102
+ Roles.registerHelper(helper);
103
+ }
104
+ let helperFn = func;
105
+ if (!isFunction(helperFn)) {
106
+ const clonedValue = clone(helperFn);
107
+ helperFn = () => {
108
+ return clonedValue;
109
+ };
110
+ }
111
+ if (!this.helpers[helper]) {
112
+ this.helpers[helper] = [];
113
+ }
114
+ this.helpers[helper].push(helperFn);
115
+ }
116
+ /**
117
+ * Adds allow properties to a role
118
+ */
119
+ allow(action, allow) {
120
+ if (!Roles.actions.includes(action)) {
121
+ Roles.registerAction(action);
122
+ }
123
+ let allowFn = allow;
124
+ if (!isFunction(allowFn)) {
125
+ const clonedValue = clone(allowFn);
126
+ allowFn = () => {
127
+ return clonedValue;
128
+ };
129
+ }
130
+ this.allowRules[action] = this.allowRules[action] || [];
131
+ this.allowRules[action].push(allowFn);
132
+ }
133
+ }
134
+ /**
135
+ * The admin role, who recives the default actions.
136
+ */
137
+ Roles.adminRole = new Role('admin');
138
+ /**
139
+ * All the logged in users users
140
+ */
141
+ Roles.loggedInRole = new Role('__loggedIn__');
142
+ /**
143
+ * Always, no exception
144
+ */
145
+ Roles.allRole = new Role('__all__');
146
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,cAAc,CAAC;AAEjC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,MAAM,CAAC,MAAM,KAAK,GAAmB;IACnC,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;IACX,OAAO,EAAE,EAAE;IACX;;OAEG;IACH,cAAc,CAAC,IAAY;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YAChC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SACzB;IACH,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,IAAY;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YAChC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SACzB;IACH,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,cAAc;QACxC,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACpC,IAAI,cAAc,EAAE;YAClB,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzB,IAAI,CAAC,MAAM,EAAE;gBACX,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;aAClC;iBAAM;gBACL,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBAC9B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;oBAC/B,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;iBAC/B;aACF;SACF;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC;QAC/C,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAElE,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,oBAAsC,EAAE,IAAI,EAAE,EAAE;YAC7E,MAAM,aAAa,GAAG,MAAM,oBAAoB,CAAC;YAEjD,IAAI,aAAa;gBAAE,OAAO,IAAI,CAAC;YAE/B,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;gBAC7F,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAChD,KAAK,EAAE,qBAAuC,EAAE,OAAY,EAAE,EAAE;oBAC9D,MAAM,aAAa,GAAG,MAAM,qBAAqB,CAAC;oBAClD,IAAI,aAAa;wBAAE,OAAO,IAAI,CAAC;oBAE/B,OAAO,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;gBACvC,CAAC,EACD,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CACvB,CAAC;aACH;YAED,OAAO,aAAa,CAAC;QACvB,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED;;OAEG;IACH,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QAC/D,OAAO,MAAM,KAAK,IAAI,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK;QACjC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE;YAC7B,SAAS,GAAG,CAAC,SAAS,CAAC,CAAC;SACzB;QAED,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnE,CAAC;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,OAAO,IAAI;IAKI;IAJnB,UAAU,CAA0B;IAEpC,OAAO,CAA0B;IAEjC,YAAmB,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QAC7B,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,2BAA2B,CAAC,CAAC;QAEjF,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAElB,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,IAA4B,CAAC;IACnD,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,MAAc,EAAE,IAAS;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;YACnC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;SAC9B;QAED,IAAI,QAAQ,GAAG,IAAI,CAAC;QAEpB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;YACzB,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;YACpC,QAAQ,GAAG,GAAG,EAAE;gBACd,OAAO,WAAW,CAAC;YACrB,CAAC,CAAC;SACH;QAED,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;SAC3B;QAED,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,KAAK;QACjB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;YACnC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;SAC9B;QACD,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE;YACxB,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YACnC,OAAO,GAAG,GAAG,EAAE;gBACb,OAAO,WAAW,CAAC;YACrB,CAAC,CAAC;SACH;QACD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACxD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;CACF;AAED;;GAEG;AACH,KAAK,CAAC,SAAS,GAAG,IAAI,IAAI,CAAC,OAAO,CAAyB,CAAC;AAC5D;;GAEG;AACH,KAAK,CAAC,YAAY,GAAG,IAAI,IAAI,CAAC,cAAc,CAAyB,CAAC;AACtE;;GAEG;AACH,KAAK,CAAC,OAAO,GAAG,IAAI,IAAI,CAAC,SAAS,CAAyB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const has: (obj: {
2
+ [key: string]: any;
3
+ }, key: string) => boolean;
@@ -0,0 +1,8 @@
1
+ export const has = (obj, key) => {
2
+ const keyParts = key.split('.');
3
+ return (!!obj &&
4
+ (keyParts.length > 1
5
+ ? has(obj[key.split('.')[0]], keyParts.slice(1).join('.'))
6
+ : obj.hasOwnProperty.call(obj, key)));
7
+ };
8
+ //# sourceMappingURL=has.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"has.js","sourceRoot":"","sources":["../../src/utils/has.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAA2B,EAAE,GAAW,EAAW,EAAE;IACvE,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEhC,OAAO,CACL,CAAC,CAAC,GAAG;QACL,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YAClB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1D,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CACvC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const isFunction: (func: () => any) => boolean;
@@ -0,0 +1,4 @@
1
+ export const isFunction = (func) => {
2
+ return func && typeof func === 'function';
3
+ };
4
+ //# sourceMappingURL=isFunction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isFunction.js","sourceRoot":"","sources":["../../src/utils/isFunction.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAe,EAAW,EAAE;IACrD,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,UAAU,CAAC;AAC5C,CAAC,CAAC"}
package/package.js ADDED
@@ -0,0 +1,25 @@
1
+ Package.describe({
2
+ name: 'unchained:roles',
3
+ version: '1.1.0',
4
+ summary: 'Unchained Engine: Roles',
5
+ git: 'https://github.com/unchainedshop/unchained',
6
+ documentation: 'README.md',
7
+ });
8
+
9
+ Npm.depends({
10
+ 'lodash.clone': '4.5.0',
11
+ });
12
+
13
+ Package.onUse((api) => {
14
+ api.versionsFrom('2.7.3');
15
+
16
+ api.use('ecmascript');
17
+ api.use('typescript');
18
+
19
+ api.mainModule('src/roles-index.ts');
20
+ });
21
+
22
+ Package.onTest((api) => {
23
+ api.use('ecmascript');
24
+ api.use('unchained:roles');
25
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@unchainedshop/roles",
3
+ "version": "1.1.0-1",
4
+ "description": "Roles package for unchained engine",
5
+ "main": "lib/roles-index.js",
6
+ "types": "lib/roles-index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "clean": "rm -rf lib",
10
+ "build": "npm run clean && tsc -p tsconfig.build.json",
11
+ "watch": "tsc --watch",
12
+ "link:core": "npm link @unchainedshop/types",
13
+ "test": "jest --watch"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/unchainedshop/unchained.git"
18
+ },
19
+ "keywords": [
20
+ "unchained",
21
+ "ecommerce"
22
+ ],
23
+ "author": "Joël Meiller",
24
+ "license": "EUPL-1.2",
25
+ "bugs": {
26
+ "url": "https://github.com/unchainedshop/unchained/issues"
27
+ },
28
+ "homepage": "https://github.com/unchainedshop/unchained#readme",
29
+ "dependencies": {
30
+ "lodash.clone": "4.5.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^16.11.41",
34
+ "@unchainedshop/types": "../@unchainedshop/types",
35
+ "chai": "^4.3.6",
36
+ "typescript": "^4.7.3"
37
+ }
38
+ }
package/roles.test.js ADDED
@@ -0,0 +1,212 @@
1
+ import {
2
+ describe,
3
+ test,
4
+ expect,
5
+ it,
6
+ beforeAll,
7
+ afterEach,
8
+ jest,
9
+ } from '@jest/globals';
10
+ import { setupDatabase } from '../../tests/helpers';
11
+ import { User } from '../../tests/seeds/users';
12
+ import { isFunction, has } from './helpers';
13
+ import { Role, Roles } from './index';
14
+
15
+ describe('Roles', () => {
16
+ beforeAll(async () => {
17
+ // eslint-disable-next-line no-unused-vars
18
+ await setupDatabase();
19
+ });
20
+
21
+ afterEach(() => {
22
+ Roles.roles = {};
23
+ Roles.actions = [];
24
+ Roles.helpers = [];
25
+ });
26
+
27
+ describe('Role utilities', () => {
28
+ const testRole = new Role('test_role');
29
+ const actionName = 'view_secret';
30
+
31
+ it('should register an action rule', () => {
32
+ const allowFn = () => true;
33
+ testRole.allow(actionName, allowFn);
34
+ expect(testRole.allowRules[actionName]).toEqual(
35
+ expect.arrayContaining([allowFn])
36
+ );
37
+ });
38
+
39
+ it('should register a helper', () => {
40
+ const helperFn = () => true;
41
+ testRole.helper('test_helper', helperFn);
42
+ expect(testRole.helpers.test_helper).toEqual(
43
+ expect.arrayContaining([helperFn])
44
+ );
45
+ });
46
+
47
+ it('should return true if the user passes the allow check', () => {
48
+ const permissionTestRole = new Role('permission_test_role');
49
+ permissionTestRole.allow('view_data', () => true);
50
+
51
+ expect(Roles.allow('permission_user', 'view_data')).toBe(true);
52
+ });
53
+
54
+ it("should return false if the user doesn't pass the allow check", () => {
55
+ const permissionTestRole = new Role('permission_test_role');
56
+ permissionTestRole.allow('view_data', () => false);
57
+
58
+ expect(Roles.allow('permission_user', 'view_data')).toBe(false);
59
+ });
60
+
61
+ it('should return false given a false user id', () => {
62
+ const permissionTestRole = new Role('permission_test_role');
63
+ permissionTestRole.allow('view_data', () => false);
64
+
65
+ expect(Roles.allow('not_found_user', 'view_data')).toBe(false);
66
+ });
67
+
68
+ it('should return false given a false role', () => {
69
+ expect(Roles.allow('permission_user', 'view_data')).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('Add allow to role', () => {
74
+ it('allow', async () => {
75
+ const permissionTestRole = new Role('permission_test_role');
76
+ permissionTestRole.allow('view_data', () => true);
77
+
78
+ expect(Roles.userHasPermission('permission_user', 'view_data')).toBe(
79
+ true
80
+ );
81
+ });
82
+ });
83
+
84
+ describe('Retrieve Roles', () => {
85
+ it('should add roles to user document', async () => {
86
+ const result = Roles.addUserToRoles(User._id, 'test_role');
87
+ expect(result).toMatchObject({ ok: 1, nModified: 1, n: 1 });
88
+ });
89
+
90
+ it('should get roles attach to user', () => {
91
+ const roles = Roles.getUserRoles(User._id);
92
+ expect(roles).toEqual(expect.arrayContaining(['test_role']));
93
+ });
94
+
95
+ it('should get roles including special ones', () => {
96
+ const roles = Roles.getUserRoles(User._id, true);
97
+ expect(roles).toEqual(
98
+ expect.arrayContaining([
99
+ 'test_role',
100
+ '__all__',
101
+ '__loggedIn__',
102
+ '__notAdmin__',
103
+ ])
104
+ );
105
+ });
106
+
107
+ it('should get admin user roles including special ones', () => {
108
+ const roles = Roles.getUserRoles('admin', true);
109
+ expect(roles).toEqual(
110
+ expect.arrayContaining([
111
+ 'test_role',
112
+ 'admin',
113
+ '__all__',
114
+ '__loggedIn__',
115
+ ])
116
+ );
117
+ });
118
+
119
+ it('should get roles including special ones as not logged in user', () => {
120
+ const roles = Roles.getUserRoles(null, true);
121
+ expect(roles).toEqual(
122
+ expect.arrayContaining(['__all__', '__notLoggedIn__'])
123
+ );
124
+ });
125
+ });
126
+
127
+ describe('Role Helper Registration', () => {
128
+ it('should add a helper', () => {
129
+ Roles.registerHelper('test_helper');
130
+ expect(Roles.helpers).toEqual(['test_helper']);
131
+ });
132
+
133
+ it('should add a helper attaching it to adminRole', () => {
134
+ Roles.registerHelper('test_admin_helper', jest.fn());
135
+ expect(Roles.helpers).toEqual(
136
+ expect.arrayContaining(['test_admin_helper'])
137
+ );
138
+ });
139
+
140
+ it('should skip adding helper if it already exists', () => {
141
+ Roles.registerHelper('test_helper');
142
+ Roles.registerHelper('test_admin_helper');
143
+
144
+ expect(Roles.helpers).toEqual(
145
+ expect.arrayContaining(['test_helper', 'test_admin_helper'])
146
+ );
147
+ });
148
+ });
149
+
150
+ // Roles.registerAction
151
+ describe('Action registration', () => {
152
+ it('should add an action', () => {
153
+ Roles.registerAction('test_action');
154
+ expect(Roles.actions).toEqual(['test_action']);
155
+ });
156
+
157
+ it('should skip adding action if it already exists', () => {
158
+ Roles.registerAction('test_action');
159
+ expect(Roles.actions).toEqual(['test_action']);
160
+ });
161
+ });
162
+
163
+ // Roles.Role
164
+ describe('Role contruction', () => {
165
+ it('should construct a new role', () => {
166
+ expect(new Role('test_role')).toMatchObject({
167
+ name: 'test_role',
168
+ allowRules: {},
169
+ helpers: {},
170
+ });
171
+ });
172
+
173
+ it('should throw an error if given a role with similar name', () => {
174
+ // eslint-disable-next-line no-new
175
+ new Role('test_role');
176
+ expect(() => new Role('test_role')).toThrow();
177
+ });
178
+ });
179
+
180
+ describe('isFunction', () => {
181
+ test('it should return true give a function', () => {
182
+ expect(isFunction(() => {})).toBe(true);
183
+ });
184
+
185
+ test('it should return false given an improper function', () => {
186
+ expect(isFunction('false')).toBe(false);
187
+ });
188
+ });
189
+
190
+ describe('has', () => {
191
+ test('it should return true for existent key', () => {
192
+ const obj = {
193
+ foo: 'bar',
194
+ };
195
+ expect(has(obj, 'foo')).toBe(true);
196
+ });
197
+
198
+ test('it should return true for existent nested key', () => {
199
+ const obj = {
200
+ foo: { bar: 'baz' },
201
+ };
202
+ expect(has(obj, 'foo.bar')).toBe(true);
203
+ });
204
+
205
+ test('it should return false for non existent', () => {
206
+ const obj = {
207
+ foo: 'bar',
208
+ };
209
+ expect(has(obj, 'baz')).toBe(false);
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,4 @@
1
+ export { Roles, Role } from './roles';
2
+
3
+ export { has } from './utils/has';
4
+ export { isFunction } from './utils/isFunction';
package/src/roles.ts ADDED
@@ -0,0 +1,168 @@
1
+ import clone from 'lodash.clone';
2
+ import { RoleInterface, RolesInterface } from '@unchainedshop/types/roles';
3
+ import { has } from './utils/has';
4
+ import { isFunction } from './utils/isFunction';
5
+
6
+ export const Roles: RolesInterface = {
7
+ roles: {},
8
+ actions: [],
9
+ helpers: [],
10
+ /**
11
+ * Creates a new action
12
+ */
13
+ registerAction(name: string): void {
14
+ if (!this.actions.includes(name)) {
15
+ this.actions.push(name);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * Creates a new helper
21
+ */
22
+ registerHelper(name: string): void {
23
+ if (!this.helpers.includes(name)) {
24
+ this.helpers.push(name);
25
+ }
26
+ },
27
+
28
+ /**
29
+ * Get user roles
30
+ */
31
+ getUserRoles(userId, roles, includeSpecial) {
32
+ const newRoles = [...(roles || [])];
33
+ if (includeSpecial) {
34
+ newRoles.push('__all__');
35
+ if (!userId) {
36
+ newRoles.push('__notLoggedIn__');
37
+ } else {
38
+ newRoles.push('__loggedIn__');
39
+ if (!newRoles.includes('admin')) {
40
+ newRoles.push('__notAdmin__');
41
+ }
42
+ }
43
+ }
44
+
45
+ return newRoles;
46
+ },
47
+
48
+ /**
49
+ * Returns true if the user passes the allow check
50
+ */
51
+ async allow(context, roles, action, [obj, params]) {
52
+ const userRoles = Roles.getUserRoles(context.userId, roles, true);
53
+
54
+ return userRoles.reduce(async (roleIsAllowedPromise: Promise<boolean>, role) => {
55
+ const roleIsAllowed = await roleIsAllowedPromise;
56
+
57
+ if (roleIsAllowed) return true;
58
+
59
+ if (Roles.roles[role] && Roles.roles[role].allowRules && Roles.roles[role].allowRules[action]) {
60
+ return Roles.roles[role].allowRules[action].reduce(
61
+ async (rulesIsAllowedPromise: Promise<boolean>, allowFn: any) => {
62
+ const ruleIsAllowed = await rulesIsAllowedPromise;
63
+ if (ruleIsAllowed) return true;
64
+
65
+ return allowFn(obj, params, context);
66
+ },
67
+ Promise.resolve(false),
68
+ );
69
+ }
70
+
71
+ return roleIsAllowed;
72
+ }, Promise.resolve(false));
73
+ },
74
+
75
+ /**
76
+ * To check if a user has permisisons to execute an action
77
+ */
78
+ userHasPermission: async (context, action, args) => {
79
+ const roles = Array.isArray(context.user?.roles) ? context.user.roles : [];
80
+ const allows = await Roles.allow(context, roles, action, args);
81
+ return allows === true;
82
+ },
83
+
84
+ /**
85
+ * Adds roles to a user
86
+ */
87
+ async addUserToRoles(context, roles) {
88
+ let userRoles = roles;
89
+ if (!Array.isArray(userRoles)) {
90
+ userRoles = [userRoles];
91
+ }
92
+
93
+ return context.modules.users.addRoles(context.userId, userRoles);
94
+ },
95
+ };
96
+
97
+ /**
98
+ * Constructs a new role
99
+ */
100
+ export class Role implements RoleInterface {
101
+ allowRules: { [name: string]: any };
102
+
103
+ helpers: { [name: string]: any };
104
+
105
+ constructor(public name: string) {
106
+ if (has(Roles.roles, name)) throw new Error(`"${name}" role is already defined`);
107
+
108
+ this.allowRules = {};
109
+ this.helpers = {};
110
+
111
+ Roles.roles[name] = this as any as RoleInterface;
112
+ }
113
+
114
+ /**
115
+ * Adds a helper to a role
116
+ */
117
+ helper(helper: string, func: any) {
118
+ if (!Roles.helpers.includes(helper)) {
119
+ Roles.registerHelper(helper);
120
+ }
121
+
122
+ let helperFn = func;
123
+
124
+ if (!isFunction(helperFn)) {
125
+ const clonedValue = clone(helperFn);
126
+ helperFn = () => {
127
+ return clonedValue;
128
+ };
129
+ }
130
+
131
+ if (!this.helpers[helper]) {
132
+ this.helpers[helper] = [];
133
+ }
134
+
135
+ this.helpers[helper].push(helperFn);
136
+ }
137
+
138
+ /**
139
+ * Adds allow properties to a role
140
+ */
141
+ allow(action, allow) {
142
+ if (!Roles.actions.includes(action)) {
143
+ Roles.registerAction(action);
144
+ }
145
+ let allowFn = allow;
146
+ if (!isFunction(allowFn)) {
147
+ const clonedValue = clone(allowFn);
148
+ allowFn = () => {
149
+ return clonedValue;
150
+ };
151
+ }
152
+ this.allowRules[action] = this.allowRules[action] || [];
153
+ this.allowRules[action].push(allowFn);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The admin role, who recives the default actions.
159
+ */
160
+ Roles.adminRole = new Role('admin') as any as RoleInterface;
161
+ /**
162
+ * All the logged in users users
163
+ */
164
+ Roles.loggedInRole = new Role('__loggedIn__') as any as RoleInterface;
165
+ /**
166
+ * Always, no exception
167
+ */
168
+ Roles.allRole = new Role('__all__') as any as RoleInterface;
@@ -0,0 +1,10 @@
1
+ export const has = (obj: { [key: string]: any }, key: string): boolean => {
2
+ const keyParts = key.split('.');
3
+
4
+ return (
5
+ !!obj &&
6
+ (keyParts.length > 1
7
+ ? has(obj[key.split('.')[0]], keyParts.slice(1).join('.'))
8
+ : obj.hasOwnProperty.call(obj, key))
9
+ );
10
+ };
@@ -0,0 +1,3 @@
1
+ export const isFunction = (func: () => any): boolean => {
2
+ return func && typeof func === 'function';
3
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "allowSyntheticDefaultImports": true,
5
+ "declaration": true,
6
+ "esModuleInterop": true,
7
+ "experimentalDecorators": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "lib": ["esnext"],
10
+ "module": "esnext",
11
+ "moduleResolution": "node",
12
+ "noImplicitReturns": true,
13
+ "noUnusedLocals": false,
14
+ "outDir": "lib",
15
+ "preserveWatchOutput": true,
16
+ "skipLibCheck": true,
17
+ "sourceMap": true,
18
+ "target": "esnext",
19
+ "types": ["node"],
20
+ "baseUrl": ".", // This must be specified if "paths" is.
21
+ "paths": {
22
+ "meteor/unchained:*": ["node_modules/@unchainedshop/types/index.d.ts"]
23
+ }
24
+ },
25
+ "include": ["src"]
26
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "types": ["node", "mocha"]
6
+ },
7
+ "include": ["src", "tests"]
8
+ }