@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
@@ -1,133 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { createClient } from '@wral/sdk-auth';
3
- import './studio-login.mjs';
4
- import './studio-change-password.mjs';
5
-
6
- /**
7
- * This is a login form for the auth module. It consumes the generic
8
- * studio-login component.
9
- *
10
- * This component is responsible for sending requests to the auth API
11
- * to generate a JWT token, and for enforcing a password change flow.
12
- *
13
- * @attribute apibaseurl
14
- *
15
- * @example
16
- * <mod-auth-login-form
17
- * apibaseurl="https://api.wral.com/auth"
18
- * @login-success=${({ detail }) => console.log(detail.token)}
19
- * >
20
- * </mod-auth-login-form>
21
- *
22
- * @fires login-success
23
- */
24
- class ModAuthLoginForm extends LitElement {
25
-
26
- constructor() {
27
- super();
28
- this.apibaseurl = '';
29
- this.error = '';
30
- this.isAwaiting = false;
31
- this.lastResult = undefined;
32
- this.forceChangePassword = false;
33
- this.handleLogin = this.handleLogin.bind(this);
34
- this.handleLoginResult = this.handleLoginResult.bind(this);
35
- this.handleLoginError = this.handleLoginError.bind(this);
36
- this.renderPasswordChangeForm = this.renderPasswordChangeForm.bind(this);
37
- }
38
-
39
- static properties = {
40
- apibaseurl: { type: String },
41
- error: { type: String },
42
- };
43
-
44
- static styles = css`
45
- :host {
46
- display: block;
47
- max-width: 400px;
48
- margin: 0 auto;
49
- }
50
- `;
51
-
52
- handleLogin(event) {
53
- this.isAwaiting = true;
54
- const { username, password } = event.detail;
55
- const client = createClient({ baseUrl: this.apibaseurl });
56
- this.requestUpdate();
57
- client.mintToken({ username, password })
58
- .then(this.handleLoginResult)
59
- .catch(this.handleLoginError);
60
- }
61
-
62
- handleLoginResult(result) {
63
- this.error = '';
64
- this.isAwaiting = false;
65
- this.lastResult = result;
66
- if (result.challenge?.name === 'NEW_PASSWORD_REQUIRED') {
67
- this.forceChangePassword = true;
68
- this.error = 'You must set a new password to continue.';
69
- } else {
70
- this.dispatchEvent(new CustomEvent('login-success', { detail: result }));
71
- }
72
- this.requestUpdate();
73
- }
74
-
75
- handleLoginError(error) {
76
- // Debugging login errors is a high priority
77
- /* eslint-disable no-console */
78
- console.error("Error logging in:", error);
79
- this.error = error?.body?.error?.message || 'Invalid username or password';
80
- this.isAwaiting = false;
81
- this.requestUpdate();
82
- }
83
-
84
- handleChangePassword(event) {
85
- const { currentPassword, newPassword } = event.detail;
86
- const { token } = this.lastResult;
87
- const client = createClient({ baseUrl: this.apibaseurl });
88
- this.requestUpdate();
89
- client.updatePassword({ token, currentPassword, newPassword })
90
- .then(() => {
91
- this.forceChangePassword = false;
92
- this.error = 'Login with your new password.';
93
- this.isAwaiting = false;
94
- this.requestUpdate();
95
- })
96
- .catch((error) => {
97
- this.error = error?.body?.error?.message || error.message;
98
- this.isAwaiting = false;
99
- this.requestUpdate();
100
- });
101
- }
102
-
103
- renderPasswordChangeForm() {
104
- return html`
105
- <div>
106
- ${this.error ? html`<div class="error">${this.error}</div>` : null}
107
- <studio-change-password @change-password=${this.handleChangePassword}>
108
- </studio-change-password>
109
- </div>
110
- `;
111
- }
112
-
113
- renderLoginForm() {
114
- return html`
115
- ${this.error ? html`<div class="error">${this.error}</div>` : null}
116
- <studio-login
117
- @login-attempt=${this.handleLogin}
118
- ?disabled=${this.isAwaiting}
119
- ></studio-login>
120
- `;
121
- }
122
-
123
- render() {
124
- // TODO: handle resets with a route
125
- if (this.forceChangePassword) {
126
- return this.renderPasswordChangeForm();
127
- } else {
128
- return this.renderLoginForm();
129
- }
130
- }
131
- }
132
-
133
- window.customElements.define('mod-auth-login-form', ModAuthLoginForm);
@@ -1,84 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
-
3
- /**
4
- * Describes a generic change password form.
5
- *
6
- * @attribute currentPassword - The current password
7
- * @attribute newPassword - The new password
8
- * @attribute confirmPassword - The new password confirmation
9
- *
10
- * @fires change-password - Fired when the user submits the change password form
11
- */
12
- class StudioChangePassword extends LitElement {
13
- static styles = css`
14
- :host {
15
- display: block;
16
- box-sizing: border-box;
17
- }
18
- input, button {
19
- width: 100%;
20
- margin-bottom: 10px;
21
- padding: 8px;
22
- box-sizing: border-box;
23
- }
24
- `;
25
-
26
- static properties = {
27
- currentPassword: { type: String },
28
- newPassword: { type: String },
29
- confirmPassword: { type: String },
30
- };
31
-
32
- constructor() {
33
- super();
34
- this.currentPassword = '';
35
- this.newPassword = '';
36
- this.confirmPassword = '';
37
- }
38
-
39
- render() {
40
- return html`
41
- <form @submit="${this.handleSubmit}">
42
- <input type="password" name="currentPassword"
43
- autocomplete="current-password"
44
- placeholder="Current Password"
45
- .value="${this.currentPassword}"
46
- @input="${this.updateProperty('currentPassword')}"
47
- >
48
- <input type="password" name="newPassword"
49
- placeholder="New Password"
50
- .value="${this.newPassword}"
51
- @input="${this.updateProperty('newPassword')}"
52
- >
53
- <input type="password" name="confirmPassword"
54
- placeholder="Confirm New Password"
55
- .value="${this.confirmPassword}"
56
- @input="${this.updateProperty('confirmPassword')}"
57
- >
58
- <button type="submit">Change Password</button>
59
- </form>
60
- `;
61
- }
62
-
63
- updateProperty(property) {
64
- return (e) => {
65
- this[property] = e.target.value;
66
- };
67
- }
68
-
69
- handleSubmit(e) {
70
- e.preventDefault();
71
- if (this.newPassword === this.confirmPassword) {
72
- this.dispatchEvent(new CustomEvent('change-password', {
73
- detail: {
74
- currentPassword: this.currentPassword,
75
- newPassword: this.newPassword,
76
- },
77
- }));
78
- } else {
79
- alert('The new passwords do not match.');
80
- }
81
- }
82
- }
83
-
84
- customElements.define('studio-change-password', StudioChangePassword);
@@ -1,94 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
- import '@shoelace-style/shoelace/dist/components/card/card.js';
3
- import '@shoelace-style/shoelace/dist/components/input/input.js';
4
- import '@shoelace-style/shoelace/dist/components/button/button.js';
5
-
6
- /**
7
- * Login Form for the Studio
8
- * @customElement studio-login
9
- * @example
10
- * <studio-login></studio-login>
11
- *
12
- * @fires login-attempt - Fired when the user submits the login form
13
- */
14
- class StudioLogin extends LitElement {
15
- static styles = css`
16
- :host {
17
- display: flex;
18
- justify-content: center;
19
- align-items: center;
20
- height: 100vh;
21
- box-sizing: border-box;
22
- }
23
- sl-card {
24
- width: 300px;
25
- --padding: var(--sl-card-padding);
26
- --background-color: var(--sl-card-background-color);
27
- --border-radius: var(--sl-card-border-radius);
28
- --box-shadow: var(--sl-card-shadow);
29
- }
30
- sl-input, sl-button {
31
- width: 100%;
32
- margin-bottom: 10px;
33
- }
34
- `;
35
-
36
- static properties = {
37
- username: { type: String },
38
- password: { type: String },
39
- disabled: { type: Boolean, reflect: true },
40
- };
41
-
42
- constructor() {
43
- super();
44
- this.username = '';
45
- this.password = '';
46
- this.disabled = false;
47
- }
48
-
49
- handleSubmit(e) {
50
- e.preventDefault();
51
- this.dispatchEvent(
52
- new CustomEvent('login-attempt', {
53
- detail: {
54
- username: this.username,
55
- password: this.password,
56
- },
57
- bubbles: true,
58
- composed: true,
59
- })
60
- );
61
- }
62
-
63
- handleInputChange(e) {
64
- const { name, value } = e.target;
65
- this[name] = value;
66
- }
67
-
68
- render() {
69
- return html`
70
- <sl-card>
71
- <form @submit="${this.handleSubmit}">
72
- <sl-input
73
- name="username"
74
- placeholder="Username or Email"
75
- .value="${this.username}"
76
- autocomplete="username"
77
- @sl-change="${this.handleInputChange}">
78
- </sl-input>
79
- <sl-input
80
- type="password"
81
- name="password"
82
- placeholder="Password"
83
- .value="${this.password}"
84
- autocomplete="current-password"
85
- @sl-change="${this.handleInputChange}">
86
- </sl-input>
87
- <sl-button type="submit" ?disabled="${this.disabled}">Login</sl-button>
88
- </form>
89
- </sl-card>
90
- `;
91
- }
92
- }
93
-
94
- customElements.define('studio-login', StudioLogin);
@@ -1,56 +0,0 @@
1
- import { LitElement, html } from 'lit';
2
- import { getToken } from '../lib.mjs';
3
- import { getStudio } from '@wral/studio-tools';
4
-
5
- /**
6
- * Parses the payload of a JWT, without any validation.
7
- * @param {string} token - The JWT token
8
- * @returns {object} The parsed token's payload
9
- */
10
- function parseToken(token) {
11
- return JSON.parse(atob(token.split('.')[1]));
12
- }
13
-
14
- class StudioProfileView extends LitElement {
15
-
16
- constructor() {
17
- super();
18
- this.token = null;
19
- this.studio = getStudio();
20
- }
21
-
22
- firstUpdated() {
23
- this.fetchToken().then(() => {
24
- this.requestUpdate();
25
- })
26
- }
27
-
28
- async fetchToken() {
29
- return this.token = await getToken(this.studio);
30
- }
31
-
32
- render() {
33
- if (!this.token) {
34
- return html`<div>You are not logged in</div>`;
35
- }
36
- const payload = parseToken(this.token);
37
- return html`<div>
38
- <h2>Your Profile</h2>
39
- <div class="username">${payload.username || payload.sub}</div>
40
- <div>
41
- <h3>Permissions</h3>
42
- <ul>
43
- ${(payload.scope || '').split(' ')
44
- .map(permission => html`<li>${permission}</li>`)}
45
- </ul>
46
- </div>
47
- <div>
48
- <studio-change-password></studio-change-password>
49
- </div>
50
- </div>`;
51
- }
52
- }
53
-
54
- customElements.define('studio-profile-view', StudioProfileView);
55
-
56
- export default html`<studio-profile-view id="profile"></studio-profile-view>`
@@ -1,110 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
-
3
- /**
4
- * Describes a reset password form
5
- *
6
- * @example
7
- * <studio-reset-password></studio-reset-password>
8
- *
9
- * @attribute hide-confirmation-code - Hides the confirmation code input
10
- * @attribute triggered - Indicates that the reset password form was triggered
11
- *
12
- * @fires request-trigger - Fired when the user submits the login form
13
- * @fires reset-password - Fired when the user submits the reset password form
14
- *
15
- * @TODO: refactor password reset as a form and add browser validation
16
- */
17
- class StudioResetPassword extends LitElement {
18
- static styles = css`
19
- :host {
20
- display: block;
21
- box-sizing: border-box;
22
- }
23
- input, button {
24
- width: 100%;
25
- margin-bottom: 10px;
26
- box-sizing: border-box;
27
- padding: 8px;
28
- }
29
- `;
30
-
31
- static properties = {
32
- email: { type: String },
33
- confirmationCode: { type: String },
34
- hideConfirmationCode: { type: Boolean },
35
- newPassword: { type: String },
36
- triggered: { type: Boolean },
37
- };
38
-
39
- constructor() {
40
- super();
41
- this.email = '';
42
- this.confirmationCode = '';
43
- this.newPassword = '';
44
- this.hideConfirmationCode = false;
45
- }
46
-
47
- handleInput(field) {
48
- return (e) => {
49
- this[field] = e.target.value;
50
- };
51
- }
52
-
53
- triggerRequest() {
54
- const event = new CustomEvent('request-trigger', {
55
- detail: { email: this.email },
56
- cancelable: true,
57
- });
58
- this.dispatchEvent(event);
59
-
60
- // If the event is not cancelled, set the stage to 2
61
- if (!event.defaultPrevented) {
62
- this.triggered = true;
63
- }
64
- }
65
-
66
- handleSubmit(e) {
67
- e.preventDefault();
68
- this.dispatchEvent(new CustomEvent('password-reset', {
69
- detail: {
70
- email: this.email,
71
- confirmationCode: this.confirmationCode,
72
- newPassword: this.newPassword,
73
- },
74
- }));
75
- }
76
-
77
- render() {
78
- return html`
79
- <form @submit="${this.handleSubmit}">
80
- ${!this.triggered ? html`
81
- <input type="email"
82
- name="email"
83
- placeholder="Email"
84
- autocomplete="email"
85
- .value="${this.email}"
86
- required
87
- @input="${this.handleInput('email')}">
88
- <button type="button" @click="${this.triggerRequest}">Request Reset</button>
89
- ` : html`
90
- <input name="confirmationCode"
91
- type=${this.hideConfirmationCode ? 'hidden' : 'text'}
92
- autocomplete="one-time-code"
93
- placeholder="Confirmation Code"
94
- .value="${this.confirmationCode}"
95
- @input="${this.handleInput('confirmationCode')}">
96
- <input type="password" name="newPassword"
97
- autocomplete="new-password"
98
- placeholder="New Password"
99
- .value="${this.newPassword}"
100
- @input="${this.handleInput('newPassword')}"
101
- required
102
- >
103
- <button type="submit">Reset Password</button>
104
- `}
105
- </form>
106
- `;
107
- }
108
- }
109
-
110
- customElements.define('studio-reset-password', StudioResetPassword);
package/src/lib.mjs DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Get an auth token
3
- * @param {Studio} studio
4
- * @return {Promise<string>} bearer token
5
- */
6
- export function getToken(studio) {
7
- return new Promise((resolve, reject) => {
8
- studio.pub('studio.wral::mod-auth', 'token-request', {
9
- callback: (token, error) => {
10
- if (error) {
11
- reject(error);
12
- }
13
- resolve(token);
14
- }});
15
- });
16
- }
@@ -1,84 +0,0 @@
1
- import { LitElement, css, html } from 'lit';
2
- import { getStudio } from '@wral/studio-tools';
3
- import { defineTool } from '@wral/studio-ui/define-tool';
4
- import { getToken } from './lib.mjs';
5
-
6
- import './components/studio-profile-view.mjs';
7
-
8
- /* Console use should be permitted in this component, since its only
9
- * purpose is as a fixture for development and debugging. */
10
- /* eslint-disable no-console */
11
-
12
- class DummyTool extends LitElement {
13
- constructor() {
14
- super();
15
- this.studio = getStudio();
16
- this.handleClick = this.handleClick.bind(this);
17
- this.handleLogout = this.handleLogout.bind(this);
18
- this.token = null;
19
- }
20
-
21
- static get styles() {
22
- return css`
23
- :host {
24
- display: block;
25
- }
26
- `;
27
- }
28
-
29
- handleClick() {
30
- console.log("Dummy tool is requesting a token");
31
- this.getToken().then((token) => {
32
- console.log("Dummy tool received token:", token);
33
- this.token = token;
34
- this.requestUpdate();
35
- }).catch((error) => {
36
- console.error("Dummy tool did not receive token:", error);
37
- });
38
- }
39
-
40
- handleLogout() {
41
- this.studio.pub('studio.wral::mod-auth', 'logout');
42
- this.token = null;
43
- this.requestUpdate();
44
- }
45
-
46
- getToken() {
47
- return getToken(this.studio);
48
- }
49
-
50
- render() {
51
- return html`
52
- <h1>Dummy Tool</h1>
53
- <p>This tool represents a studio mod that needs an auth token. The
54
- token will be requested when the button is clicked.</p>
55
- <button @click="${this.handleClick}">Click me</button>
56
- <div>${this.token ? 'You are authenticated' : 'Token not requested'}</div>
57
- <div>
58
- ${this.token && html`
59
- <button @click="${this.handleLogout}">Request Logout</button>
60
- `}
61
- </div>
62
- ${this.token ? html`<studio-profile-view token="${this.token}">
63
- </studio-profile-view>` : null}
64
- `;
65
- }
66
-
67
- }
68
-
69
- const dummyTool = defineTool({
70
- component: DummyTool,
71
- token: 'dummy-tool',
72
- label: 'Dummy Tool',
73
- description: 'Dummy Tool',
74
- icon: '🤖',
75
- toolHtml: html`<tool-dummy-tool exportparts="title,description"></tool-dummy-tool>`,
76
- buttonHtml: html`<tool-dummy-tool-button exportparts="button,icon">
77
- </tool-dummy-tool-button>`,
78
- });
79
- export default dummyTool;
80
-
81
- export function init(studio) {
82
- // Force this tool to activate on init
83
- studio.pub('system::workspace', 'present', dummyTool);
84
- }