@wral/studio.mods.auth 0.3.7 → 1.0.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.
Files changed (45) hide show
  1. package/README.md +39 -47
  2. package/bitbucket-pipelines.yml +25 -1
  3. package/dist/auth.cjs.js +326 -1467
  4. package/dist/auth.es.js +1081 -3093
  5. package/dist/lib.cjs.js +1 -1
  6. package/dist/lib.es.js +13 -7
  7. package/eslint.config.mjs +41 -34
  8. package/index.html +83 -18
  9. package/jest.config.mjs +24 -0
  10. package/jest.setup.mjs +5 -0
  11. package/package.json +15 -28
  12. package/src/auth.mjs +204 -69
  13. package/src/auth.test.mjs +97 -0
  14. package/src/components/auth-app.mjs +26 -0
  15. package/src/components/forgot-password-form.mjs +217 -0
  16. package/src/components/login-form.mjs +288 -0
  17. package/src/config.mjs +27 -0
  18. package/src/helper.mjs +31 -0
  19. package/src/helper.test.mjs +44 -0
  20. package/src/index.mjs +17 -0
  21. package/src/login-layout.mjs +32 -0
  22. package/src/login.mjs +20 -0
  23. package/src/routes/change-password.mjs +158 -0
  24. package/src/routes/dashboard.mjs +17 -0
  25. package/src/routes/index.mjs +15 -0
  26. package/src/state.mjs +61 -0
  27. package/src/state.test.mjs +58 -0
  28. package/src/styles.mjs +9 -0
  29. package/src/token.mjs +40 -0
  30. package/src/utils.mjs +3 -0
  31. package/vellum-fixture.mjs +86 -0
  32. package/vite.config.mjs +12 -0
  33. package/components.html +0 -43
  34. package/development.md +0 -41
  35. package/src/components/mod-auth-login-form.mjs +0 -133
  36. package/src/components/studio-change-password.mjs +0 -84
  37. package/src/components/studio-login.mjs +0 -94
  38. package/src/components/studio-profile-view.mjs +0 -56
  39. package/src/components/studio-reset-password.mjs +0 -110
  40. package/src/lib.mjs +0 -16
  41. package/src/tool-dummy.mjs +0 -84
  42. package/src/util.mjs +0 -194
  43. package/src/util.test.mjs +0 -171
  44. package/vite.config.js +0 -12
  45. package/web-test-runner.config.mjs +0 -28
package/dist/lib.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function i(t){return new Promise((o,n)=>{t.pub("studio.wral::mod-auth","token-request",{callback:(u,e)=>{e&&n(e),o(u)}})})}exports.getToken=i;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function o(e){return new Promise((t,n)=>{(!e||!e.dispatchEvent)&&n(new Error("getToken must be called with a DOM element")),e.dispatchEvent(new CustomEvent("harness:action",{detail:{type:"auth:requestToken",detail:{callback:t}},bubbles:!0,composed:!0,cancelable:!0}))})}exports.getToken=o;
package/dist/lib.es.js CHANGED
@@ -1,10 +1,16 @@
1
- function a(t) {
2
- return new Promise((n, o) => {
3
- t.pub("studio.wral::mod-auth", "token-request", {
4
- callback: (u, e) => {
5
- e && o(e), n(u);
6
- }
7
- });
1
+ function a(e) {
2
+ return new Promise((t, n) => {
3
+ (!e || !e.dispatchEvent) && n(new Error("getToken must be called with a DOM element")), e.dispatchEvent(new CustomEvent("harness:action", {
4
+ detail: {
5
+ type: "auth:requestToken",
6
+ detail: {
7
+ callback: t
8
+ }
9
+ },
10
+ bubbles: !0,
11
+ composed: !0,
12
+ cancelable: !0
13
+ }));
8
14
  });
9
15
  }
10
16
  export {
package/eslint.config.mjs CHANGED
@@ -1,39 +1,46 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import jest from 'eslint-plugin-jest';
4
+
1
5
  export default [
6
+
7
+ // Base configuration
8
+ js.configs.recommended,
9
+
10
+ // Language options and globals
2
11
  {
3
- "ignores": ["coverage", "dist/**", "node_modules"],
4
- "rules": {
5
- "no-console": [
6
- "error",
7
- ],
8
- "no-unused-vars": [
9
- "error",
10
- ],
11
- "no-unused-private-class-members": [
12
- "error",
13
- ],
14
- "curly": [
15
- "warn",
16
- ],
17
- 'max-len': [
18
- 'warn',
19
- {
20
- code: 90,
21
- comments: 80,
22
- ignoreUrls: true,
23
- ignoreRegExpLiterals: true,
24
- },
25
- ],
26
- 'object-curly-spacing': ['off'],
27
- 'comma-dangle': [
28
- 'warn',
29
- {
30
- arrays: 'always-multiline',
31
- objects: 'always-multiline',
32
- imports: 'always-multiline',
33
- exports: 'always-multiline',
34
- functions: 'ignore',
35
- },
36
- ],
12
+ languageOptions: {
13
+ ecmaVersion: 2021, // Latest ECMAScript features
14
+ sourceType: 'module', // Enable ES6 modules
15
+ globals: {
16
+ ...globals.browser, // Browser globals
17
+ },
18
+ },
19
+ },
20
+
21
+ {
22
+ files: ['src/**/*.mjs'],
23
+ ignores: ['dist/**', 'node_modules/**','coverage/**'],
24
+ rules: {
25
+ indent: ['error', 'tab'], // Use tabs for indentation
26
+ 'linebreak-style': ['error', 'unix'], // Enforce Unix-style linebreaks
27
+ quotes: ['error', 'single'], // Prefer single quotes
28
+ semi: ['error', 'always'], // Enforce semicolons
29
+ 'max-len': ['error', { code: 90, tabWidth: 2 }], // Limit line length
30
+ },
31
+ },
32
+
33
+ // Jest-specific configuration for test files
34
+ {
35
+ files: ['**/*.test.mjs'], // Target Jest test files
36
+ plugins: {
37
+ jest, // Use the Jest plugin
38
+ },
39
+ rules: jest.configs.recommended.rules, // Apply recommended Jest rules
40
+ languageOptions: {
41
+ globals: {
42
+ ...globals.jest, // Jest-specific globals
43
+ },
37
44
  },
38
45
  },
39
46
  ];
package/index.html CHANGED
@@ -1,20 +1,85 @@
1
+ <!-- src/index.html -->
1
2
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title>My Studio</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <meta name="robots" content="noindex, nofollow">
8
- <!-- Load studio-app from CDN -->
9
- <script src="https://cdn.wral.studio/release/v2.2.3/studio-app.js"></script>
10
- </head>
11
- <body>
12
- <studio-app>
13
- <studio-mod src="http://localhost:5173/src/auth.mjs"
14
- apiBaseUrl="https://api.wral.com/dev/auth"
15
- ></studio-mod>
16
- <studio-mod src="http://localhost:5173/src/tool-dummy.mjs"
17
- ></studio-mod>
18
- </studio-app>
19
- </body>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>WRAL.studio</title>
8
+ <script type="module" src="/vellum-fixture.mjs"></script>
9
+ <meta name="robots" content="noindex, nofollow">
10
+ </head>
11
+ <body>
12
+ <style>
13
+ /*
14
+ TODO: move these styles into a theme
15
+ */
16
+ html {
17
+ --spacing-sm: 0.5rem;
18
+ --spacing-md: 1rem;
19
+ --spacing-lg: 2rem;
20
+ --spacing-xl: 3rem;
21
+
22
+ --radius-sm: 5px;
23
+ --radius-md: 8px;
24
+ --radius-lg: 10px;
25
+ --radius-full: 50%;
26
+ --radius-dynamic: clamp(var(--radius-sm), 1vw, var(--radius-lg));
27
+
28
+ --color-primary: #001D68;
29
+ --color-primary-light: #193377;
30
+ --color-primary-dark: #00144A;
31
+ --color-secondary: #2594E3;
32
+ --color-secondary-light: #46A4E7;
33
+ --color-secondary-light-1: #c3d6e3;
34
+ --color-secondary-dark: #1D76B5;
35
+ --color-error: var(--color-red-dark);
36
+ --color-red: #D1232A;
37
+ --color-red-light: #FF2B32;
38
+ --color-red-dark: #D1232A;
39
+ --color-yellow: #FFEC19;
40
+ --color-yellow-light: #FFF15E;
41
+ --color-yellow-dark: #E5D416;
42
+ --color-green: #72B509;
43
+ --color-green-light: #8EC33A;
44
+ --color-green-dark: #66A208;
45
+ --color-purple: #33109C;
46
+ --color-purple-light: #4727A5;
47
+ --color-purple-dark: #280C7C;
48
+ --color-orange: #FF9505;
49
+ --color-orange-light: #FFA21F;
50
+ --color-orange-dark: #F58F00;
51
+ --color-white: #FFFFFF;
52
+ --color-black: #030711;
53
+ --color-gray-1: #F7F5F4;
54
+ --color-gray-2: #E7E5E4;
55
+ --color-gray-3: #D7D4D2;
56
+ --color-gray-4: #7C7C7C;
57
+ --color-gray-5: #6C6C6C;
58
+ --color-gray-6: #565454;
59
+ --color-gray-7: #424242;
60
+ --color-gray-8: #263238;
61
+ --color-gray-9: #1C2321;
62
+
63
+ --font-heading: Inter, sans-serif;
64
+ --font-body: Arial, sans-serif;
65
+ --font-size-base: 16px;
66
+ --color-text: var(--color-gray-9);
67
+
68
+ --drop-shadow-md: 0 0 10px var(--color-gray-3);
69
+ }
70
+ html, body {
71
+ margin: 0;
72
+ padding: 0;
73
+ box-sizing: border-box;
74
+ }
75
+ </style>
76
+ <studio-app>
77
+ <vellum-mod name="setup" src="/vellum-fixture.mjs"></vellum-mod>
78
+
79
+ <vellum-mod name="auth" src="/src/index.mjs"
80
+ api="https://api.wral.com/auth"
81
+ force-login></vellum-mod>
82
+
83
+ </studio-app>
84
+ </body>
20
85
  </html>
@@ -0,0 +1,24 @@
1
+ export default {
2
+
3
+ setupFiles: ['<rootDir>/jest.setup.mjs'],
4
+
5
+ // Use jsdom for a browser-like testing environment
6
+ testEnvironment: 'jsdom',
7
+
8
+ // Look for tests ending with .test.mjs
9
+ testMatch: ['**/*.test.mjs'],
10
+
11
+ // Collect code coverage from files in /src
12
+ collectCoverage: true,
13
+ collectCoverageFrom: ['src/**/*.{js,mjs}'],
14
+
15
+ // Set coverage thresholds to 0 (TODO: bring them up to 100%)
16
+ coverageThreshold: {
17
+ global: {
18
+ branches: 0, // TODO: bring up to 100%
19
+ functions: 0, // TODO: bring up to 100%
20
+ lines: 0, // TODO: bring up to 100%
21
+ statements: 0, // TODO: bring up to 100%
22
+ }
23
+ }
24
+ };
package/jest.setup.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { TextEncoder, TextDecoder } from 'util';
2
+
3
+ /* eslint-disable no-undef */
4
+ global.TextEncoder = TextEncoder;
5
+ global.TextDecoder = TextDecoder;
package/package.json CHANGED
@@ -1,44 +1,31 @@
1
1
  {
2
2
  "name": "@wral/studio.mods.auth",
3
- "version": "0.3.7",
4
- "description": "Auth mod for Studio",
5
- "main": "dist/lib.cjs.js",
6
- "module": "dist/lib.es.js",
3
+ "version": "1.0.0",
4
+ "description": "Auth mod for Studio/Vellum",
5
+ "main": "dist/auth.cjs.js",
6
+ "module": "dist/auth.es.js",
7
7
  "scripts": {
8
- "prepare": "husky",
9
- "lint": "eslint . --config eslint.config.mjs",
10
- "update:modules": "npx npm-check-updates --interactive",
11
- "build": "vite build",
12
8
  "dev": "vite",
13
- "test": "web-test-runner --node-resolve --coverage --polyfills && npm run lint"
9
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
10
+ "lint": "eslint .",
11
+ "build": "vite build"
14
12
  },
15
13
  "repository": {
16
14
  "type": "git",
17
- "url": "git+https://bitbucket.org/cbcnm/studio.mods.auth.git"
15
+ "url": "git+https://bitbucket.org/cbcnm/studio.mods/auth.git"
18
16
  },
19
17
  "author": "Kenneth Barbour <kbarbour@wral.com>",
20
18
  "license": "UNLICENSED",
21
- "bugs": {
22
- "url": "https://bitbucket.org/cbcnm/studio.mods.auth/issues"
23
- },
24
- "homepage": "https://bitbucket.org/cbcnm/studio.mods.auth#readme",
25
19
  "dependencies": {
26
- "@shoelace-style/shoelace": "^2.16.0",
27
20
  "@wral/sdk-auth": "^0.2.1",
28
- "@wral/studio-tools": "^0.9.2",
29
- "@wral/studio-ui": "^2.4.8",
30
- "lit": "^3.2.0"
21
+ "lit": "^3.2.1"
31
22
  },
32
23
  "devDependencies": {
33
- "@open-wc/testing": "^4.0.0",
34
- "@web/test-runner": "^0.19.0",
35
- "@web/test-runner-coverage-v8": "^0.8.0",
36
- "@web/test-runner-playwright": "^0.11.0",
37
- "chai": "^5.1.1",
38
- "eslint": "^9.10.0",
39
- "husky": "^9.1.5",
40
- "playwright": "^1.47.0",
41
- "sinon": "^18.0.1",
42
- "vite": "^5.4.4"
24
+ "@thefarce/vellum": "^0.4.2",
25
+ "eslint": "^9.22.0",
26
+ "eslint-plugin-jest": "^28.11.0",
27
+ "jest": "^29.7.0",
28
+ "jest-environment-jsdom": "^29.7.0",
29
+ "vite": "^6.2.1"
43
30
  }
44
31
  }
package/src/auth.mjs CHANGED
@@ -1,73 +1,208 @@
1
- import { getToken, logout } from './util.mjs';
1
+ import { imply } from './utils.mjs';
2
+ import { loginLayout } from './login-layout.mjs';
3
+ import { html } from 'lit';
4
+ import { shouldRefreshToken } from './token.mjs';
5
+ import { pathnameToState } from './state.mjs';
6
+ import { createClient } from '@wral/sdk-auth';
7
+ import './components/auth-app.mjs';
8
+
9
+ export function makeAuth(config) {
10
+ const auth = {
11
+ config,
12
+ log: (...args) => console.log('[auth]', ...args),
13
+ };
14
+ auth.mount = imply(mount, auth);
15
+ auth.handleTokenRequest = tokenRequestHandler(auth);
16
+ auth.handleDestroyAuth = loginDestroyHandler(auth);
17
+ auth.presentLoginForm = imply(presentLoginForm, auth);
18
+ auth.getToken = imply(getToken, auth);
19
+ auth.saveToken = imply(saveToken, auth);
20
+ auth.getFreshToken = imply(getFreshToken, auth);
21
+ auth.requestRender = (state) => renderApp(auth, state);
22
+
23
+ return auth;
24
+ }
25
+
26
+ /**
27
+ * Call mount when initializing the mod for the first time.
28
+ * It registers handlers with the vellum toolkit.
29
+ * @param {Object} auth
30
+ */
31
+ export function mount(auth) {
32
+ const windowElem = auth.config.toolkit.element.ownerDocument.defaultView;
33
+ [{
34
+ type: 'action:register',
35
+ detail: {
36
+ actionType: 'auth:requestToken',
37
+ handler: auth.handleTokenRequest,
38
+ modName: 'auth',
39
+ },
40
+ },
41
+ {
42
+ type: 'action:register',
43
+ detail: {
44
+ actionType: 'auth:destroy',
45
+ handler: auth.handleDestroyAuth,
46
+ modName: 'auth',
47
+ },
48
+ },
49
+ {
50
+ type: 'layout:register',
51
+ detail: {
52
+ name: 'auth:login',
53
+ slots: ['main'],
54
+ templateFn: loginLayout(auth),
55
+ },
56
+ }].forEach(action => auth.config.toolkit.dispatchAction(action));
57
+ windowElem.addEventListener('popstate', historyStateHandler(auth));
58
+ }
59
+
60
+ export function renderApp(auth, state) {
61
+ const windowElem = auth.config.toolkit.element.ownerDocument.defaultView;
62
+ let appState = state;
63
+ if (!appState) {
64
+ appState = pathnameToState(auth, windowElem.location.pathname);
65
+ }
66
+ if (!appState) {
67
+ return; // nothing to do
68
+ }
69
+ auth.config.toolkit.dispatchAction({
70
+ type: 'layout:slot:replace',
71
+ detail: {
72
+ layout: 'menu-main-task-layout',
73
+ slot: 'main',
74
+ content: html`<auth-app .state=${appState} .auth=${auth}></auth-app>`,
75
+ },
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Returns an event handler for window.onpopstate.
81
+ * @param {Object} auth
82
+ * @returns {Function}
83
+ */
84
+ export function historyStateHandler(auth) {
85
+ return (event) => {
86
+ auth.log('onpopstate', event);
87
+ renderApp(auth);
88
+ };
89
+ }
2
90
 
3
91
  /**
4
- * Initializes authentication-related subscriptions with the given studio
5
- * instance and configuration. This function sets up listeners for token
6
- * requests and logout actions.
7
- *
8
- * @module studio-mod_auth
9
- * @param {Object} studio - The studio instance where the authentication
10
- * module is registered.
11
- * @param {Object} config - Configuration object for the initialization
12
- * (currently not used in the function).
92
+ * Generate a handler for token requests.
93
+ * This handler should attempt to resolve to a fresh token, or reject.
94
+ * If the currently held token is not fresh, the handler should present the
95
+ * login form to the user.
96
+ * @param {Object} auth
97
+ * @returns {Function}
13
98
  */
14
- export function init(studio, config) {
15
-
16
- /**
17
- * Event Subscription: 'token-request'
18
- * Handles requests for authentication tokens.
19
- *
20
- * @event studio.wral::mod-auth::token-request
21
- * @type {object}
22
- * @property {Object} state - Contains the state information from the
23
- * event trigger.
24
- * @property {Function} state.value.callback - Callback function to handle
25
- * the token response.
26
- * @param {string|null} state.value.token - Expected to receive parameters
27
- * `token` (string|null) if successful, or `error` (Error|null) if an
28
- * error occurs.
29
- *
30
- * Usage:
31
- * studio.pub('studio.wral::mod-auth', 'token-request', {
32
- * state: {
33
- * value: {
34
- * callback: (token, error) => {
35
- * if (error) {
36
- * console.error('Error retrieving token:', error);
37
- * } else {
38
- * console.log('Received token:', token);
39
- * }
40
- * }
41
- * }
42
- * }
43
- * });
44
- */
45
- studio.sub('studio.wral::mod-auth', 'token-request', ({state}) => {
46
- const { callback } = state.value;
47
- if (callback) {
48
- getToken({ studio, config })
49
- .then(token => callback(token))
50
- .catch(error => callback(null, error));
51
- } else {
52
- /** Warn consumers of this mod if a message has been sent without a
53
- * callback. */
54
- /* eslint-disable no-console */
55
- console.warn("No token callback defined");
56
- }
57
- });
58
-
59
- /**
60
- * Event Subscription: 'logout'
61
- * Initiates the logout process upon receiving the event.
62
- *
63
- * @event studio.wral::mod-auth::logout
64
- *
65
- * Usage:
66
- * studio.pub('studio.wral::mod-auth', 'logout');
67
- */
68
- studio.sub('studio.wral::mod-auth', 'logout', () => {
69
- logout({ studio });
70
- });
71
-
72
- // studio.pub('system::workspace', 'present', studioProfileView);
99
+ export function tokenRequestHandler(auth) {
100
+ let callbacks = [];
101
+ let isLoginPresent = false;
102
+
103
+ const onLogin = token => {
104
+ callbacks.forEach(callback => {
105
+ callback(token);
106
+ });
107
+ callbacks = [];
108
+ };
109
+
110
+ return async ({ callback }) => {
111
+ let token;
112
+ try {
113
+ token = await auth.getFreshToken();
114
+ callback(token);
115
+ } catch (err) {
116
+ callbacks.push(callback);
117
+ if (!isLoginPresent) {
118
+ auth.presentLoginForm({ onLogin });
119
+ isLoginPresent = true;
120
+ }
121
+ auth.log(err);
122
+ }
123
+ };
124
+ }
125
+
126
+ export function loginDestroyHandler(auth) {
127
+ return () => {
128
+ const storageKey = getTokenStorageKey(auth);
129
+ window.localStorage.removeItem(storageKey);
130
+ auth.config.toolkit.dispatchAction({
131
+ type: 'layout:pop',
132
+ });
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Present the login form to the user.
138
+ * This also handles the dismissal of the login form.
139
+ * @param {Object} auth
140
+ * @param {Function} onLogin
141
+ */
142
+ export function presentLoginForm(auth, { onLogin }) {
143
+ const { toolkit } = auth.config;
144
+ const urlParams = new URLSearchParams(
145
+ toolkit.element.ownerDocument.defaultView.location.search);
146
+ const passwordResetConfirmation = urlParams.get('password_reset_confirmation');
147
+
148
+ // After the login form is dismissed, restore the previous layout
149
+ const dismiss = (event) => {
150
+ auth.log('login success', { event });
151
+ const { token } = event.detail;
152
+ auth.log('Login form dismissed', { args: [token] });
153
+ toolkit.dispatchAction({
154
+ type: 'layout:pop',
155
+ });
156
+ auth.saveToken({token});
157
+ onLogin({ token });
158
+ };
159
+
160
+ toolkit.dispatchAction({
161
+ type: 'layout:push',
162
+ detail: {
163
+ layout: 'auth:login',
164
+ },
165
+ });
166
+ toolkit.dispatchAction({
167
+ type: 'layout:slot:push',
168
+ detail: {
169
+ layout: 'auth:login',
170
+ slot: 'main',
171
+ content: html`<auth-login-form
172
+ api=${auth.config.api}
173
+ confirmation="${passwordResetConfirmation}"
174
+ @login-success=${dismiss}>
175
+ </auth-login-form>`,
176
+ },
177
+ });
178
+ }
179
+
180
+ export function saveToken(auth, { token }) {
181
+ auth.log('saving token', { args: [token] });
182
+ const key = getTokenStorageKey(auth);
183
+ window.localStorage.setItem(key, token);
184
+ }
185
+
186
+ export function getToken(auth) {
187
+ const key = getTokenStorageKey(auth);
188
+ return window.localStorage.getItem(key);
189
+ }
190
+
191
+ export async function getFreshToken(auth) {
192
+ const token = getToken(auth);
193
+ if (!token || token.length < 1) {
194
+ throw new Error('No token found');
195
+ }
196
+ if (shouldRefreshToken(token)) {
197
+ auth.log('refreshing token', { args: [token] });
198
+ const client = createClient({
199
+ baseUrl: auth.config.api,
200
+ });
201
+ return await client.refreshToken({ token }).then(({ token }) => token);
202
+ }
203
+ return token;
204
+ }
205
+
206
+ export function getTokenStorageKey({ storageKey='token' }) {
207
+ return storageKey;
73
208
  }