@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.
- package/README.md +39 -47
- package/bitbucket-pipelines.yml +25 -1
- package/dist/auth.cjs.js +326 -1467
- package/dist/auth.es.js +1081 -3093
- package/dist/lib.cjs.js +1 -1
- package/dist/lib.es.js +13 -7
- package/eslint.config.mjs +41 -34
- package/index.html +83 -18
- package/jest.config.mjs +24 -0
- package/jest.setup.mjs +5 -0
- package/package.json +15 -28
- package/src/auth.mjs +204 -69
- package/src/auth.test.mjs +97 -0
- package/src/components/auth-app.mjs +26 -0
- package/src/components/forgot-password-form.mjs +217 -0
- package/src/components/login-form.mjs +288 -0
- package/src/config.mjs +27 -0
- package/src/helper.mjs +31 -0
- package/src/helper.test.mjs +44 -0
- package/src/index.mjs +17 -0
- package/src/login-layout.mjs +32 -0
- package/src/login.mjs +20 -0
- package/src/routes/change-password.mjs +158 -0
- package/src/routes/dashboard.mjs +17 -0
- package/src/routes/index.mjs +15 -0
- package/src/state.mjs +61 -0
- package/src/state.test.mjs +58 -0
- package/src/styles.mjs +9 -0
- package/src/token.mjs +40 -0
- package/src/utils.mjs +3 -0
- package/vellum-fixture.mjs +86 -0
- package/vite.config.mjs +12 -0
- package/components.html +0 -43
- package/development.md +0 -41
- package/src/components/mod-auth-login-form.mjs +0 -133
- package/src/components/studio-change-password.mjs +0 -84
- package/src/components/studio-login.mjs +0 -94
- package/src/components/studio-profile-view.mjs +0 -56
- package/src/components/studio-reset-password.mjs +0 -110
- package/src/lib.mjs +0 -16
- package/src/tool-dummy.mjs +0 -84
- package/src/util.mjs +0 -194
- package/src/util.test.mjs +0 -171
- package/vite.config.js +0 -12
- 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
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,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
|
+
}
|
package/vite.config.mjs
ADDED
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.
|