@wral/studio.mods.auth 1.0.1 → 2.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 +3 -3
- package/dist/auth.cjs.js +119 -205
- package/dist/auth.es.js +466 -696
- package/dist/lib.cjs.js +1 -1
- package/dist/lib.es.js +4 -6
- package/example-app.mjs +34 -0
- package/index.html +19 -72
- package/package.json +3 -2
- package/src/{components/forgot-password-form.mjs → forgot-password-form.mjs} +69 -69
- package/src/helper.mjs +5 -16
- package/src/index.mjs +108 -12
- package/src/layout.mjs +38 -0
- package/src/{components/login-form.mjs → login-form.mjs} +125 -122
- package/src/token.mjs +34 -30
- package/vellum/README.md +1 -0
- package/vellum/app-manager.mjs +126 -0
- package/vellum/example-app.mjs +34 -0
- package/vellum/index.mjs +26 -0
- package/vellum/layout.mjs +85 -0
- package/vellum/mod-setup.mjs +22 -0
- package/vellum/render.mjs +21 -0
- package/vellum/themes/default/index.css +89 -0
- package/src/auth.mjs +0 -208
- package/src/auth.test.mjs +0 -97
- package/src/components/auth-app.mjs +0 -26
- package/src/config.mjs +0 -27
- package/src/login-layout.mjs +0 -32
- package/src/login.mjs +0 -20
- package/src/routes/change-password.mjs +0 -158
- package/src/routes/dashboard.mjs +0 -17
- package/src/routes/index.mjs +0 -15
- package/src/state.mjs +0 -61
- package/src/state.test.mjs +0 -58
- package/src/styles.mjs +0 -9
- package/src/utils.mjs +0 -3
- package/vellum-fixture.mjs +0 -86
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The main (default) layout for the application consists of 3 elements:
|
|
5
|
+
* - Menu: a navigation toolbar that exposes apps and other high-level tools
|
|
6
|
+
* - Main: the main workspace of the application
|
|
7
|
+
* - Task: an optional small task panel for detail-oriented user interaction
|
|
8
|
+
*/
|
|
9
|
+
export class MenuMainTaskLayout extends LitElement {
|
|
10
|
+
|
|
11
|
+
static get styles() {
|
|
12
|
+
return css`
|
|
13
|
+
.container {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
width: 100%;
|
|
18
|
+
background-color: var(--color-gray-50);
|
|
19
|
+
}
|
|
20
|
+
#menu {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
width: 250px;
|
|
24
|
+
background-color: var(--color-gray-100);
|
|
25
|
+
padding: var(--spacing-md, 0.5rem);
|
|
26
|
+
gap: var(--spacing-lg, 0.5rem);
|
|
27
|
+
}
|
|
28
|
+
#menu .toolbar {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
gap: var(--spacing-md);
|
|
32
|
+
flex-grow: 1;
|
|
33
|
+
}
|
|
34
|
+
#main {
|
|
35
|
+
flex-grow: 1;
|
|
36
|
+
padding: var(--spacing-lg, 0.5rem);
|
|
37
|
+
}
|
|
38
|
+
#task {
|
|
39
|
+
background-color: var(--color-gray-100);
|
|
40
|
+
box-shadow: var(--drop-shadow-md);
|
|
41
|
+
}
|
|
42
|
+
#task :slotted(*) {
|
|
43
|
+
padding: var(--spacing-lg, 0.5rem);
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render() {
|
|
49
|
+
return html`
|
|
50
|
+
<div class="container">
|
|
51
|
+
<nav id="menu">
|
|
52
|
+
<div class="profile">👤</div>
|
|
53
|
+
<div class="toolbar">
|
|
54
|
+
<slot name="menu"></slot>
|
|
55
|
+
</div>
|
|
56
|
+
</nav>
|
|
57
|
+
<main id="main">
|
|
58
|
+
<slot name="main"></slot>
|
|
59
|
+
</main>
|
|
60
|
+
<aside id="task">
|
|
61
|
+
<slot name="task"></slot>
|
|
62
|
+
</aside>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function init(toolkit) {
|
|
69
|
+
const name = 'menu-main-task'; // layout name - should be unique to app
|
|
70
|
+
customElements.define('menu-main-task-layout', MenuMainTaskLayout);
|
|
71
|
+
const templateFn = () =>
|
|
72
|
+
html`<menu-main-task-layout></menu-main-task-layout>`;
|
|
73
|
+
|
|
74
|
+
// Register this layout with the layout manager
|
|
75
|
+
toolkit.dispatchAction({
|
|
76
|
+
type: 'layout:register',
|
|
77
|
+
detail: { name, templateFn },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Push this layout to the top of the display stack
|
|
81
|
+
toolkit.dispatchAction({
|
|
82
|
+
type: 'layout:push',
|
|
83
|
+
detail: { name },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init as initLayoutManager,
|
|
3
|
+
} from '@thefarce/vellum.mods.managers.layouts.simple-web-component';
|
|
4
|
+
import {
|
|
5
|
+
init as initLayout,
|
|
6
|
+
} from './layout.mjs';
|
|
7
|
+
import {
|
|
8
|
+
init as initAppManager,
|
|
9
|
+
} from './app-manager.mjs';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* This module is the bootstrapping Vellum Mod for ths Studio App.
|
|
13
|
+
* It is responsible for setting up the Application with the basic action
|
|
14
|
+
* management and layout.
|
|
15
|
+
*/
|
|
16
|
+
export async function init(toolkit, mod) {
|
|
17
|
+
console.log("calling setup init");
|
|
18
|
+
await initLayoutManager(toolkit, mod);
|
|
19
|
+
await initLayout(toolkit, mod);
|
|
20
|
+
await initAppManager(toolkit, mod);
|
|
21
|
+
console.log("called setup init");
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { render as renderLit } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given some content, and a containing DOM element, render
|
|
5
|
+
* the content into the container.
|
|
6
|
+
* @param {string|Node|TemplateResult} content
|
|
7
|
+
* @param {HTMLElement} container
|
|
8
|
+
* @return void - manipulates container as a side-effect
|
|
9
|
+
*/
|
|
10
|
+
export function render(content, container) {
|
|
11
|
+
if (typeof content === 'string') {
|
|
12
|
+
// dangerously set the innerHTML of the container
|
|
13
|
+
container.innerHTML = content;
|
|
14
|
+
} else if (content instanceof Node) {
|
|
15
|
+
// remove all child nodes and replace with the new content
|
|
16
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
17
|
+
container.appendChild(content);
|
|
18
|
+
} else {
|
|
19
|
+
renderLit(content, container); // assume Lit TemplateResult
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--spacing-sm: 0.5rem;
|
|
3
|
+
--spacing-md: 1rem;
|
|
4
|
+
--spacing-lg: 2rem;
|
|
5
|
+
--spacing-xl: 3rem;
|
|
6
|
+
|
|
7
|
+
--radius-sm: 5px;
|
|
8
|
+
--radius-md: 8px;
|
|
9
|
+
--radius-lg: 10px;
|
|
10
|
+
--radius-full: 50%;
|
|
11
|
+
--radius-dynamic: clamp(var(--radius-sm), 1vw, var(--radius-lg));
|
|
12
|
+
|
|
13
|
+
--color-primary : #001D68;
|
|
14
|
+
--color-primary-light : #193377;
|
|
15
|
+
--color-primary-dark : #00144A;
|
|
16
|
+
--color-secondary : #2594E3;
|
|
17
|
+
--color-secondary-light : #46A4E7;
|
|
18
|
+
--color-secondary-light-1 : #c3d6e3;
|
|
19
|
+
--color-secondary-dark : #1D76B5;
|
|
20
|
+
--color-error : var(--color-red-dark);
|
|
21
|
+
--color-red : #D1232A;
|
|
22
|
+
--color-red-light : #FF2B32;
|
|
23
|
+
--color-red-dark : #D1232A;
|
|
24
|
+
--color-yellow : #FFEC19;
|
|
25
|
+
--color-yellow-light : #FFF15E;
|
|
26
|
+
--color-yellow-dark : #E5D416;
|
|
27
|
+
--color-green : #72B509;
|
|
28
|
+
--color-green-light : #8EC33A;
|
|
29
|
+
--color-green-dark : #66A208;
|
|
30
|
+
--color-purple : #33109C;
|
|
31
|
+
--color-purple-light : #4727A5;
|
|
32
|
+
--color-purple-dark : #280C7C;
|
|
33
|
+
--color-orange : #FF9505;
|
|
34
|
+
--color-orange-light : #FFA21F;
|
|
35
|
+
--color-orange-dark : #F58F00;
|
|
36
|
+
--color-white : #FFFFFF;
|
|
37
|
+
--color-black : #030711;
|
|
38
|
+
|
|
39
|
+
/* Adjust these colors in the dark theme as well */
|
|
40
|
+
--color-gray-50 : #F7F5F4; /* lightest */
|
|
41
|
+
--color-gray-100 : #E7E5E4;
|
|
42
|
+
--color-gray-200 : #D7D4D2;
|
|
43
|
+
--color-gray-300 : #7C7C7C;
|
|
44
|
+
--color-gray-400 : #6C6C6C;
|
|
45
|
+
--color-gray-500 : #565454;
|
|
46
|
+
--color-gray-600 : #424242;
|
|
47
|
+
--color-gray-700 : #263238;
|
|
48
|
+
--color-gray-800 : #1C2321; /* darkest */
|
|
49
|
+
--color-gray-900 : #1C2321; /* darkest */
|
|
50
|
+
--color-gray-950 : #1C2321; /* darkest */
|
|
51
|
+
|
|
52
|
+
--drop-shadow-md: 0 0 10px var(--color-gray-200);
|
|
53
|
+
--text-color: var(--color-gray-800);
|
|
54
|
+
--text-color-light: var(--color-gray-700);
|
|
55
|
+
|
|
56
|
+
--font-heading: Inter, sans-serif;
|
|
57
|
+
--font-body: Inter, sans-serif;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Dark Theme
|
|
62
|
+
*/
|
|
63
|
+
@media (prefers-color-scheme: dark) {
|
|
64
|
+
:root {
|
|
65
|
+
/* Invert grayscales */
|
|
66
|
+
--color-gray-950: #F7F5F4;
|
|
67
|
+
--color-gray-900: #E7E5E4;
|
|
68
|
+
--color-gray-800: #D7D4D2;
|
|
69
|
+
--color-gray-700: #7C7C7C;
|
|
70
|
+
--color-gray-600: #6C6C6C;
|
|
71
|
+
--color-gray-500: #565454;
|
|
72
|
+
--color-gray-400: #424242;
|
|
73
|
+
--color-gray-300: #263238;
|
|
74
|
+
--color-gray-200: #1C2321;
|
|
75
|
+
--color-gray-100: #1C2321;
|
|
76
|
+
--color-gray-50: #111111;
|
|
77
|
+
|
|
78
|
+
--color-primary: var(--color-primary-light);
|
|
79
|
+
--color-error: var(--color-red-light);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
html, body {
|
|
84
|
+
margin: 0;
|
|
85
|
+
padding: 0;
|
|
86
|
+
background-color: var(--color-gray-1);
|
|
87
|
+
color: var(--text-color);
|
|
88
|
+
font-family: var(--font-body);
|
|
89
|
+
}
|
package/src/auth.mjs
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
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
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
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}
|
|
98
|
-
*/
|
|
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;
|
|
208
|
-
}
|
package/src/auth.test.mjs
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
makeAuth,
|
|
3
|
-
tokenRequestHandler,
|
|
4
|
-
} from './auth.mjs';
|
|
5
|
-
import { JSDOM } from 'jsdom';
|
|
6
|
-
import { jest } from '@jest/globals';
|
|
7
|
-
|
|
8
|
-
function mockToolkit() {
|
|
9
|
-
const dom = new JSDOM(`<!DOCTYPE html><body><studio-app>
|
|
10
|
-
<studio-mod id="mod-auth"></studio-mod>
|
|
11
|
-
</studio-app></body>`);
|
|
12
|
-
return {
|
|
13
|
-
element: dom.window.document.querySelector('studio-app'),
|
|
14
|
-
mod: {
|
|
15
|
-
element: dom.window.document.querySelector('#mod-auth'),
|
|
16
|
-
},
|
|
17
|
-
dispatchAction: jest.fn(),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('auth mod', () => {
|
|
22
|
-
|
|
23
|
-
describe('makeAuth', () => {
|
|
24
|
-
|
|
25
|
-
test('should return an object with method-like properties', () => {
|
|
26
|
-
const mod = makeAuth({
|
|
27
|
-
toolkit: mockToolkit(),
|
|
28
|
-
});
|
|
29
|
-
expect(mod).toHaveProperty('mount');
|
|
30
|
-
expect(mod).toHaveProperty('handleTokenRequest');
|
|
31
|
-
expect(mod).toHaveProperty('handleDestroyAuth');
|
|
32
|
-
expect(mod).toHaveProperty('presentLoginForm');
|
|
33
|
-
expect(mod).toHaveProperty('getToken');
|
|
34
|
-
expect(mod).toHaveProperty('saveToken');
|
|
35
|
-
expect(mod).toHaveProperty('getFreshToken');
|
|
36
|
-
expect(mod).toHaveProperty('requestRender');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('mount', () => {
|
|
42
|
-
test('registers action handlers and layout', () => {
|
|
43
|
-
const mod = makeAuth({
|
|
44
|
-
toolkit: mockToolkit(),
|
|
45
|
-
});
|
|
46
|
-
mod.mount();
|
|
47
|
-
expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
|
|
48
|
-
type: 'action:register',
|
|
49
|
-
detail: {
|
|
50
|
-
actionType: 'auth:requestToken',
|
|
51
|
-
handler: expect.any(Function),
|
|
52
|
-
modName: 'auth',
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
|
|
56
|
-
type: 'action:register',
|
|
57
|
-
detail: {
|
|
58
|
-
actionType: 'auth:destroy',
|
|
59
|
-
handler: expect.any(Function),
|
|
60
|
-
modName: 'auth',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
expect(mod.config.toolkit.dispatchAction).toHaveBeenCalledWith({
|
|
64
|
-
type: 'layout:register',
|
|
65
|
-
detail: {
|
|
66
|
-
name: 'auth:login',
|
|
67
|
-
slots: ['main'],
|
|
68
|
-
templateFn: expect.any(Function),
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('tokenRequestHandler', () => {
|
|
75
|
-
test('returns a handler that resolves to a fresh token', async () => {
|
|
76
|
-
const handler = tokenRequestHandler({
|
|
77
|
-
getFreshToken: jest.fn(async () => 'token'),
|
|
78
|
-
});
|
|
79
|
-
const callback = jest.fn();
|
|
80
|
-
await handler({ callback });
|
|
81
|
-
expect(callback).toHaveBeenCalledWith('token');
|
|
82
|
-
});
|
|
83
|
-
test('presents login form if getFreshToken fails', async () => {
|
|
84
|
-
const presentLoginForm = jest.fn();
|
|
85
|
-
const handler = tokenRequestHandler({
|
|
86
|
-
log: jest.fn(),
|
|
87
|
-
getFreshToken: jest.fn().mockRejectedValue(new Error('fail')),
|
|
88
|
-
presentLoginForm,
|
|
89
|
-
});
|
|
90
|
-
const callback = jest.fn();
|
|
91
|
-
await handler({ callback });
|
|
92
|
-
expect(callback).not.toHaveBeenCalled();
|
|
93
|
-
expect(presentLoginForm).toHaveBeenCalled();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { LitElement } from 'lit';
|
|
2
|
-
import { renderRoute } from '../routes/index.mjs';
|
|
3
|
-
import appStyles from '../styles.mjs';
|
|
4
|
-
|
|
5
|
-
class AuthApp extends LitElement {
|
|
6
|
-
static get styles() {
|
|
7
|
-
return [appStyles];
|
|
8
|
-
}
|
|
9
|
-
static get properties() {
|
|
10
|
-
return {
|
|
11
|
-
state: { type: Object },
|
|
12
|
-
auth: { type: Object },
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
render() {
|
|
17
|
-
const { state, auth } = this;
|
|
18
|
-
return renderRoute(state, auth);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default AuthApp;
|
|
23
|
-
|
|
24
|
-
if (!customElements.get('auth-app')) {
|
|
25
|
-
customElements.define('auth-app', AuthApp);
|
|
26
|
-
}
|
package/src/config.mjs
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gets configuration for the Auth Mod from the attributes
|
|
3
|
-
* of the mod element (in DOM).
|
|
4
|
-
* @param {VellumToolkit} toolkit
|
|
5
|
-
* @returns {Object}
|
|
6
|
-
*/
|
|
7
|
-
export function getConfig(toolkit, mod) {
|
|
8
|
-
const modElement = mod.element;
|
|
9
|
-
const attrs = modElement.attributes;
|
|
10
|
-
console.log(attrs);
|
|
11
|
-
return {
|
|
12
|
-
api: attrs.api.value || requiredConfig('auth:api is required'),
|
|
13
|
-
forceLogin: attrs['force-login'] !== undefined, // TODO: handle false
|
|
14
|
-
storageKey: attrs['storage-key']?.value,
|
|
15
|
-
mount: attrs['path']?.value || '/auth',
|
|
16
|
-
toolkit,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Helper for required config elements
|
|
22
|
-
* This defines what to do when a required config is missing
|
|
23
|
-
* @param {string} message
|
|
24
|
-
*/
|
|
25
|
-
function requiredConfig(message) {
|
|
26
|
-
throw new Error(message);
|
|
27
|
-
}
|
package/src/login-layout.mjs
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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;
|