@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
@@ -0,0 +1,44 @@
1
+ import * as helper from './helper.mjs';
2
+ import { jest } from '@jest/globals';
3
+
4
+ describe('auth mod helper', () => {
5
+
6
+ describe('exports', () => {
7
+ it('exports a getToken function', () => {
8
+ expect(typeof helper.getToken).toBe('function');
9
+ });
10
+ });
11
+
12
+ describe('getToken', () => {
13
+ it('returns a promise', () => {
14
+ const result = helper.getToken({dispatchEvent: jest.fn()});
15
+ expect(result).toBeInstanceOf(Promise);
16
+ });
17
+ it('throws if not called with a DOM element', async () => {
18
+ await expect(() => helper.getToken()).rejects.toThrow();
19
+ });
20
+ it('dispatches an event with a callback', async () => {
21
+ const myElem = {
22
+ dispatchEvent: jest.fn(),
23
+ };
24
+ const promise = helper.getToken(myElem);
25
+ expect(myElem.dispatchEvent).toHaveBeenCalledWith(
26
+ new CustomEvent('harness:action', {
27
+ detail: {
28
+ type: 'auth:requestToken',
29
+ detail: {
30
+ callback: expect.any(Function),
31
+ },
32
+ },
33
+ bubbles: true,
34
+ composed: true,
35
+ cancelable: true,
36
+ }),
37
+ );
38
+ const callback = myElem.dispatchEvent.mock.calls[0][0].detail.detail.callback;
39
+ callback('token');
40
+ expect(await promise).toBe('token');
41
+ });
42
+ });
43
+
44
+ });
package/src/index.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import { getToken as getTokenHelper } from './helper.mjs';
2
+ import { getConfig } from './config.mjs';
3
+ import { makeAuth } from './auth.mjs';
4
+
5
+ export function init(toolkit, mod) {
6
+ const config = getConfig(toolkit, mod);
7
+ const auth = makeAuth(config);
8
+ auth.mount();
9
+ auth.requestRender(); // initial render
10
+
11
+ /* If we'd like to force a login immediately, request the token. */
12
+ if (config.forceLogin) {
13
+ getTokenHelper(toolkit.element).then((token) => {
14
+ auth.log('You have logged in successfully!', token);
15
+ });
16
+ }
17
+ }
@@ -0,0 +1,32 @@
1
+ import { html, css } from 'lit';
2
+ import './components/login-form.mjs';
3
+
4
+ /**
5
+ * Generate a login layout
6
+ */
7
+ export function loginLayout() {
8
+ return function(slots) {
9
+ const styles = css`
10
+ .login-layout {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 1rem;
14
+ justify-content: center;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ background-color: var(--color-gray-100, #f5f5f5);
18
+ }
19
+ .login-layout > * {
20
+ max-width: 400px;
21
+ }
22
+ `;
23
+ return html`
24
+ <style>${styles}</style>
25
+ <div class="login-layout">
26
+ ${slots.main}
27
+ </div>
28
+ `;
29
+ };
30
+ }
31
+
32
+ export default loginLayout();
package/src/login.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import { createClient } from '@wral/sdk-auth';
2
+ /**
3
+ * Makes an API call to login, and resolves to a promise OR
4
+ * rejects with an error or additional authentication challenges.
5
+ * @param {Object} auth
6
+ * @param {string} config.api base url of the api (without /v1)
7
+ * @param {Object} params
8
+ * @param {string} params.username user string
9
+ * @param {string} params.password password string
10
+ * @returns {Promise} resolves to a token
11
+ */
12
+ export function login({api}, credentials) {
13
+ const client = createClient({
14
+ baseUrl: api,
15
+ });
16
+ console.log('sending credentials to API', api);
17
+ return client.mintToken(credentials);
18
+ }
19
+
20
+ export default login;
@@ -0,0 +1,158 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { getToken } from '../helper.mjs';
3
+ import { createClient } from '@wral/sdk-auth';
4
+
5
+ export function render(state, auth) {
6
+ return html`
7
+ <auth-change-password-form .auth=${auth}></auth-change-password-form>
8
+ `;
9
+ }
10
+
11
+ class ChangePasswordForm extends LitElement {
12
+
13
+ static get styles() {
14
+ return css`
15
+ :host {
16
+ color: var(--color-text);
17
+ display: block;
18
+ font-family: var(--font-body);
19
+ }
20
+ h1,h2,h3,h4,h5,h6,label { font-family: var(--font-heading) }
21
+ label {
22
+ display: block;
23
+ font-size: var(--font-size-base);
24
+ margin-top: var(--spacing-md, 0.5rem);
25
+ }
26
+ input[type="text"],
27
+ input[type="password"] {
28
+ width: 100%;
29
+ margin: 5px 0 15px 0;
30
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
31
+ box-sizing: border-box;
32
+ border: 1px solid var(--color-gray-300, #ccc);
33
+ border-radius: var(--radius-sm, 5px);
34
+ color: var(--color-gray-9, #333);
35
+ background-color: var(--color-gray-0, transparent);
36
+ }
37
+ input[type="submit"] {
38
+ background-color: var(--color-primary, inherit);
39
+ color: var(--color-gray-0, #fff);
40
+ border: none;
41
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
42
+ border-radius: var(--radius-dynamic, 5px);
43
+ width: 200px;
44
+ cursor: pointer;
45
+ }
46
+ .error {
47
+ color: var(--color-error);
48
+ }
49
+ `;
50
+ }
51
+
52
+ static get properties() {
53
+ return {
54
+ auth: { type: Object },
55
+ };
56
+ }
57
+
58
+ constructor() {
59
+ super();
60
+ this.errors = {};
61
+ this.success = null;
62
+ }
63
+
64
+ connectedCallback() {
65
+ super.connectedCallback();
66
+ // discard result, we're only interested in the side-effect of being logged in
67
+ getToken(this);
68
+ }
69
+
70
+ async onSubmit(e) {
71
+ e.preventDefault();
72
+
73
+ // Ensure passwords match
74
+ if (e.target.newPassword.value !== e.target.confirmPassword.value) {
75
+ this.errors['confirmPassword'] = 'Passwords do not match';
76
+ this.requestUpdate();
77
+ return;
78
+ }
79
+
80
+ // Check that new password is actually different
81
+ if (e.target.newPassword.value === e.target.currentPassword.value) {
82
+ this.errors['newPassword'] = 'The new password is the same as your '
83
+ + 'current password.';
84
+ this.requestUpdate();
85
+ return;
86
+ }
87
+
88
+ const apiKey = await getToken(this);
89
+
90
+ // TODO: send request to API
91
+ const authClient = createClient({
92
+ baseUrl: this.auth.config.api,
93
+ apiKey,
94
+ });
95
+ await authClient.updatePassword({
96
+ currentPassword: e.target.currentPassword.value,
97
+ newPassword: e.target.newPassword.value,
98
+ }).then(() => {
99
+ this.errors = {};
100
+ this.success = 'Password changed.';
101
+ this.requestUpdate();
102
+ }).catch((error) => {
103
+ this.errors['submit'] = error.message;
104
+ this.requestUpdate();
105
+ });
106
+ }
107
+
108
+ render() {
109
+ return html`
110
+ <h1>Change your password</h1>
111
+ <p>Enter your current password and choose a new password.</p>
112
+ <form @submit=${this.onSubmit}>
113
+ <div class="field">
114
+ <label for="currentPassword">Current Password</label>
115
+ <input
116
+ type="password"
117
+ name="currentPassword"
118
+ placeholder="Current Password"
119
+ required
120
+ />
121
+ ${this.errors['currentPassword']
122
+ ? html`<p class="error">${this.errors['currentPassword']}</p>` : ''}
123
+ </div>
124
+ <div class="field">
125
+ <label for="newPassword">New Password</label>
126
+ <input
127
+ type="password"
128
+ name="newPassword"
129
+ placeholder="New Password"
130
+ required
131
+ />
132
+ ${this.errors['newPassword']
133
+ ? html`<p class="error">${this.errors['newPassword']}</p>` : ''}
134
+ </div>
135
+ <div class="field">
136
+ <label for="confirmPassword">Confirm Password</label>
137
+ <input
138
+ type="password"
139
+ name="confirmPassword"
140
+ placeholder="Confirm Password"
141
+ required
142
+ />
143
+ ${this.errors['confirmPassword']
144
+ ? html`<p class="error">${this.errors['confirmPassword']}</p>` : ''}
145
+ </div>
146
+ <input type="submit" value="Change Password" />
147
+ ${this.errors['submit']
148
+ ? html`<p class="error">${this.errors['submit']}</p>` : ''}
149
+ ${this.success ? html`<p class="success">${this.success}</p>` : ''}
150
+ </form>
151
+ `;
152
+ }
153
+
154
+ }
155
+
156
+ if (!customElements.get('auth-change-password-form')) {
157
+ customElements.define('auth-change-password-form', ChangePasswordForm);
158
+ }
@@ -0,0 +1,17 @@
1
+ import { html } from 'lit';
2
+ import { pushState } from '../state.mjs';
3
+
4
+
5
+ export function render(state, auth) {
6
+ const { history } = auth.config.toolkit.element.ownerDocument.defaultView;
7
+ const navigateTo = (path, title) => () => {
8
+ pushState(auth, history, { path, title });
9
+ };
10
+ return html`
11
+ <h1>Auth Dashboard</h1>
12
+ <ul>
13
+ <li><a href="#" @click=${navigateTo('/change-password')}>Change Password</a></li>
14
+ <li><a href="#" @click=${auth.handleDestroyAuth}>Logout</a></li>
15
+ </ul>
16
+ `;
17
+ }
@@ -0,0 +1,15 @@
1
+ import * as dashboard from './dashboard.mjs';
2
+ import * as changePassword from './change-password.mjs';
3
+
4
+ export const routes = [
5
+ { path: '/', render: dashboard.render },
6
+ { path: '/change-password', render: changePassword.render },
7
+ ];
8
+
9
+ export function renderRoute(state, auth) {
10
+ const { path } = state;
11
+ const route = routes.find((r) => r.path === path);
12
+ if (route) {
13
+ return route.render(state, auth);
14
+ }
15
+ }
package/src/state.mjs ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Ensure pathname has a consistent format, with a leading slash, and no
3
+ * trailing slash. This lets the application be more tolerant of input.
4
+ * @param {string} pathname
5
+ * @returns {string}
6
+ */
7
+ export function coercePathname(pathname) {
8
+ return (pathname.startsWith('/') ? pathname : `/${pathname}`) // ensure leading slash
9
+ .replace(/\/{2,}/g, '/') // remove consecutive slashes
10
+ .replace(/(?<=.)\/$/, ''); // remove trailing slash (not at beginning)
11
+ }
12
+
13
+ /**
14
+ * Push a new state to the history (presumably window.history).
15
+ * After the application changes state, call pushState to enable this history
16
+ * to be tracked.
17
+ *
18
+ * To take advantage of the forward and back button behavior, be sure to handle
19
+ * window.onpopstate events.
20
+ * @param {Object} auth
21
+ * @param {History} history
22
+ * @param {Object} state
23
+ * @returns {void}
24
+ */
25
+ export function pushState(auth, history, state) {
26
+ const pathname = stateToPathname(auth, state);
27
+ history.pushState(state, state.title, pathname);
28
+ auth.requestRender(state);
29
+ }
30
+
31
+ /**
32
+ * Produce a state object from a pathname
33
+ * @param {Object} auth
34
+ * @param {string} pathname
35
+ * @returns {Object}
36
+ */
37
+ export function pathnameToState(auth, pathname) {
38
+ const { mount } = auth.config;
39
+ const { history } = auth.config.toolkit.element.ownerDocument.defaultView;
40
+
41
+ // no state if pathname is not prefixed with mount
42
+ if (!pathname.startsWith(mount)) {
43
+ return null;
44
+ }
45
+ // remove mount from pathname
46
+ return {
47
+ path: coercePathname(pathname.slice(mount.length)),
48
+ push: (state) => pushState(auth, history, state),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Produce a pathname from a state object
54
+ * @param {Object} auth
55
+ * @param {Object} state
56
+ * @returns {string}
57
+ */
58
+ export function stateToPathname(auth, state) {
59
+ const { mount } = auth.config;
60
+ return coercePathname(`${mount}/${state.path}`);
61
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ coercePathname,
3
+ pushState,
4
+ pathnameToState,
5
+ } from './state.mjs';
6
+ import { jest } from '@jest/globals';
7
+
8
+ describe('state', () => {
9
+
10
+ describe('coercePathname', () => {
11
+ // input, expected
12
+ const cases = [
13
+ ['', '/'],
14
+ ['//', '/'],
15
+ ['auth', '/auth'],
16
+ ['foo/bar', '/foo/bar'],
17
+ ['foo//bar', '/foo/bar'],
18
+ ['foo//bar/', '/foo/bar'],
19
+ ];
20
+ cases.forEach(([input, expected]) => {
21
+ it(`should coerce ${input} to ${expected}`, () => {
22
+ expect(coercePathname(input)).toBe(expected);
23
+ });
24
+ });
25
+ });
26
+
27
+ describe('pushState', () => {
28
+ it('pushes a state object to history', () => {
29
+ const history = {
30
+ pushState: jest.fn(),
31
+ };
32
+ const auth = {
33
+ requestRender: jest.fn(),
34
+ config: { mount: 'foo' },
35
+ };
36
+ const newState = {
37
+ title: 'Some Title',
38
+ path: 'bar',
39
+ };
40
+ pushState(auth, history, newState);
41
+ expect(history.pushState)
42
+ .toHaveBeenCalledWith(newState, newState.title, '/foo/bar');
43
+ });
44
+ });
45
+
46
+ describe.skip('pathnameToState', () => {
47
+ // TODO: refactor to not expect auth.config.toolkit.element.ownerDocument.defaultView
48
+ it('produces a state object from a pathname', () => {
49
+ const auth = {
50
+ config: { mount: 'foo' },
51
+ };
52
+ const pathname = '/foo/bar';
53
+ expect(pathnameToState(auth, pathname)).toEqual({
54
+ path: 'bar',
55
+ });
56
+ });
57
+ });
58
+ });
package/src/styles.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import { css } from 'lit';
2
+
3
+ export const styles = css`
4
+ :host {
5
+
6
+ }
7
+ `;
8
+
9
+ export default styles;
package/src/token.mjs ADDED
@@ -0,0 +1,40 @@
1
+
2
+ /**
3
+ * Decodes a JWT (JSON Web Token) to extract its payload as an object.
4
+ *
5
+ * @param {string} token - The JWT to decode.
6
+ * @returns {Object} The decoded payload of the token.
7
+ *
8
+ * @example
9
+ * // Decode a sample JWT
10
+ * const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
11
+ * + '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2M'
12
+ * + 'jM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
13
+ * const payload = decodeToken(token);
14
+ * console.log('Decoded Payload:', payload);
15
+ */
16
+ export function decodeToken(token) {
17
+ return JSON.parse(window.atob(token.split('.')[1]
18
+ .replace(/-/g, '+').replace(/_/g, '/')));
19
+ }
20
+
21
+ /**
22
+ * Determines if the token needs to be refreshed based on its expiry time.
23
+ *
24
+ * @param {string} token - The authentication token to check.
25
+ * @param {number} [offset=300] - The offset in milliseconds to consider
26
+ * for refreshing before actual expiry.
27
+ * @param {number} [now=Date.now()] - The current timestamp, primarily used
28
+ * for testing.
29
+ * @returns {boolean} True if the token needs to be refreshed, false otherwise.
30
+ *
31
+ * @example
32
+ * // Check if token needs to be refreshed
33
+ * const tokenNeedsRefresh = shouldRefreshToken('someToken', 300);
34
+ * console.log('Token needs refresh:', tokenNeedsRefresh);
35
+ */
36
+ export function shouldRefreshToken(token, offset = 300, now = Date.now()) {
37
+ const decoded = decodeToken(token);
38
+ return decoded.exp * 1000 - offset < now;
39
+ }
40
+
package/src/utils.mjs ADDED
@@ -0,0 +1,3 @@
1
+ export function imply(fn, ...implied) {
2
+ return (...args) => fn(...implied, ...args);
3
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * This is a Vellum fixture application that resembles an environment that would
3
+ * use this Auth mod.
4
+ */
5
+ import {
6
+ VellumHarness,
7
+ VellumModLoader,
8
+ initLayoutManager,
9
+ initThemeManager,
10
+ } from '@thefarce/vellum';
11
+ import { html, css } from 'lit';
12
+
13
+ export function init(...args) {
14
+ initLayoutManager(...args);
15
+ initThemeManager(...args);
16
+ const [ toolkit ] = args;
17
+ toolkit.dispatchAction({
18
+ type: 'layout:register',
19
+ detail: {
20
+ name: 'menu-main-task-layout',
21
+ slots: ['menu','main','task'],
22
+ templateFn: layoutTemplate,
23
+ },
24
+ });
25
+ toolkit.dispatchAction({
26
+ type: 'layout:push',
27
+ detail: { layout: 'menu-main-task-layout' },
28
+ });
29
+ }
30
+
31
+
32
+ /* Define some core web components */
33
+ Object.entries({
34
+ 'studio-app': VellumHarness,
35
+ 'studio-mod': VellumModLoader,
36
+ }).forEach(([name, webComponent]) => {
37
+ if (!customElements.get(name)) {
38
+ customElements.define(name, webComponent);
39
+ }
40
+ });
41
+
42
+ function layoutTemplate(slots) {
43
+ const styles = css`
44
+ .menu-main-task-layout {
45
+ display: flex;
46
+ height: 100vh;
47
+ }
48
+ .menu-main-task-layout > .toolbar-slot {
49
+ position: fixed;
50
+ width: var(--menu-width, 200px);
51
+ background-color: var(--color-gray-100, #f7f5f4);
52
+ height: 100vh;
53
+ box-sizing: border-box;
54
+ padding: var(--spacing-md, 1rem);
55
+ }
56
+ .menu-main-task-layout > .main-slot {
57
+ background-color: var(--color-gray-0, #fff);
58
+ flex-grow: 1;
59
+ margin-left: var(--menu-width, 250px);
60
+ padding: var(--spacing-lg, 2rem);
61
+ }
62
+ .menu-main-task-layout > .task-slot {
63
+ padding: var(--spacing-lg, 2rem);
64
+ box-sizing: border-box;
65
+ background-color: var(--color-gray-100, #f7f5f4);
66
+ }
67
+ `;
68
+ return html`
69
+ <style>${styles}</style>
70
+ <div class="menu-main-task-layout">
71
+ <aside class="toolbar-slot">
72
+ <p>Application Menu</p>
73
+ ${slots.menu && slots.menu.length > 0
74
+ ? slots.menu.map(item => item)
75
+ : html``}
76
+ </aside>
77
+ <main class="main-slot">
78
+ ${slots.main && slots.main.length > 0
79
+ ? slots.main.map(item => item)
80
+ : html``}
81
+ </main>
82
+ ${slots.task && slots.task.length > 0
83
+ ? html`<div class="task-slot">${slots.task.map(item => item)}</div>`
84
+ : html``}
85
+ </div>`;
86
+ }
@@ -0,0 +1,12 @@
1
+ export default {
2
+ build: {
3
+ lib: {
4
+ entry: {
5
+ 'auth': 'src/auth.mjs',
6
+ 'lib': 'src/helper.mjs',
7
+ },
8
+ formats: ['es', 'cjs'],
9
+ fileName: (format) => `[name].${format}.js`,
10
+ },
11
+ },
12
+ }
package/components.html DELETED
@@ -1,43 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Components</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <meta name="robots" content="noindex, nofollow">
8
- <script type="module" src="./src/components/studio-login.mjs"></script>
9
- <script type="module" src="./src/components/studio-reset-password.mjs"></script>
10
- <script type="module" src="./src/components/studio-change-password.mjs"></script>
11
- <!-- Load studio-app from CDN -->
12
- </head>
13
- <body>
14
- <h1>Studio Auth Mod</h1>
15
- <h2>Web Components</h2>
16
- <div>
17
- <h3>studio-login</h3>
18
- <p>This represents a login form that uses the WRAL Auth API.</p>
19
-
20
- <studio-login id="login"></studio-login>
21
- <script>
22
- const loginForm = document.querySelector('#login');
23
- loginForm.addEventListener('login-attempt', console.log);
24
- </script>
25
-
26
- </div>
27
- <div>
28
- <h3>studio-reset-password</h3>
29
- <p>This component provides an interface for requesting a password reset.</p>
30
- <studio-reset-password></studio-reset-password>
31
-
32
- <p>It can also be triggered with an existing confirmation code</p>
33
- <studio-reset-password triggered confirmationCode="123456"
34
- hideConfirmationCode></studio-reset-password>
35
- </div>
36
-
37
- <div>
38
- <h3>studio-change-password</h3>
39
- <p>This component provides an interface for requesting a password change.</p>
40
- <studio-change-password></studio-change-password>
41
- </div>
42
- </body>
43
- </html>
package/development.md DELETED
@@ -1,41 +0,0 @@
1
- # Development Notes
2
-
3
- ## Messages
4
-
5
- This mod communicates with other mods in by publishing and subscribing to
6
- events.
7
-
8
- ### Requesting an Auth Token
9
-
10
- Mods can request an auth token by publishing a `token-request` event with a
11
- callback. The callback will be invoked with the token or an error if the
12
- request fails.
13
-
14
- #### Example
15
-
16
- ```js
17
- studio.pub('studio.wral::mod-auth', 'token-request', {
18
- callback: (token, error) => {
19
- if (error) {
20
- console.error(error);
21
- } else {
22
- console.log(token);
23
- }
24
- },
25
- });
26
- ```
27
-
28
- ## Publications
29
-
30
- This mod publishes events for other mods to respond to.
31
-
32
- ### Present login form
33
-
34
- This mod publishes `system:auth::present-login-form` with the HTML for the login
35
- form.
36
-
37
- ```js
38
- studio.pub('system:auth', 'present-login-form');
39
- ```
40
-
41
- This mod registers the `<login-form>` web component.