@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.
@@ -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
- }
@@ -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;