@wral/studio.mods.auth 0.3.7
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/.eslintrc.mjs +67 -0
- package/.husky/pre-commit +1 -0
- package/README.md +56 -0
- package/bitbucket-pipelines.yml +107 -0
- package/components.html +43 -0
- package/development.md +41 -0
- package/dist/auth.cjs.js +1479 -0
- package/dist/auth.es.js +3379 -0
- package/dist/lib.cjs.js +1 -0
- package/dist/lib.es.js +12 -0
- package/eslint.config.mjs +39 -0
- package/index.html +20 -0
- package/package.json +44 -0
- package/src/auth.mjs +73 -0
- package/src/components/mod-auth-login-form.mjs +133 -0
- package/src/components/studio-change-password.mjs +84 -0
- package/src/components/studio-login.mjs +94 -0
- package/src/components/studio-profile-view.mjs +56 -0
- package/src/components/studio-reset-password.mjs +110 -0
- package/src/lib.mjs +16 -0
- package/src/tool-dummy.mjs +84 -0
- package/src/util.mjs +194 -0
- package/src/util.test.mjs +171 -0
- package/vite.config.js +12 -0
- package/web-test-runner.config.mjs +28 -0
package/dist/lib.cjs.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function i(t){return new Promise((o,n)=>{t.pub("studio.wral::mod-auth","token-request",{callback:(u,e)=>{e&&n(e),o(u)}})})}exports.getToken=i;
|
package/dist/lib.es.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
"ignores": ["coverage", "dist/**", "node_modules"],
|
|
4
|
+
"rules": {
|
|
5
|
+
"no-console": [
|
|
6
|
+
"error",
|
|
7
|
+
],
|
|
8
|
+
"no-unused-vars": [
|
|
9
|
+
"error",
|
|
10
|
+
],
|
|
11
|
+
"no-unused-private-class-members": [
|
|
12
|
+
"error",
|
|
13
|
+
],
|
|
14
|
+
"curly": [
|
|
15
|
+
"warn",
|
|
16
|
+
],
|
|
17
|
+
'max-len': [
|
|
18
|
+
'warn',
|
|
19
|
+
{
|
|
20
|
+
code: 90,
|
|
21
|
+
comments: 80,
|
|
22
|
+
ignoreUrls: true,
|
|
23
|
+
ignoreRegExpLiterals: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
'object-curly-spacing': ['off'],
|
|
27
|
+
'comma-dangle': [
|
|
28
|
+
'warn',
|
|
29
|
+
{
|
|
30
|
+
arrays: 'always-multiline',
|
|
31
|
+
objects: 'always-multiline',
|
|
32
|
+
imports: 'always-multiline',
|
|
33
|
+
exports: 'always-multiline',
|
|
34
|
+
functions: 'ignore',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
package/index.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>My Studio</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<meta name="robots" content="noindex, nofollow">
|
|
8
|
+
<!-- Load studio-app from CDN -->
|
|
9
|
+
<script src="https://cdn.wral.studio/release/v2.2.3/studio-app.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<studio-app>
|
|
13
|
+
<studio-mod src="http://localhost:5173/src/auth.mjs"
|
|
14
|
+
apiBaseUrl="https://api.wral.com/dev/auth"
|
|
15
|
+
></studio-mod>
|
|
16
|
+
<studio-mod src="http://localhost:5173/src/tool-dummy.mjs"
|
|
17
|
+
></studio-mod>
|
|
18
|
+
</studio-app>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wral/studio.mods.auth",
|
|
3
|
+
"version": "0.3.7",
|
|
4
|
+
"description": "Auth mod for Studio",
|
|
5
|
+
"main": "dist/lib.cjs.js",
|
|
6
|
+
"module": "dist/lib.es.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prepare": "husky",
|
|
9
|
+
"lint": "eslint . --config eslint.config.mjs",
|
|
10
|
+
"update:modules": "npx npm-check-updates --interactive",
|
|
11
|
+
"build": "vite build",
|
|
12
|
+
"dev": "vite",
|
|
13
|
+
"test": "web-test-runner --node-resolve --coverage --polyfills && npm run lint"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://bitbucket.org/cbcnm/studio.mods.auth.git"
|
|
18
|
+
},
|
|
19
|
+
"author": "Kenneth Barbour <kbarbour@wral.com>",
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://bitbucket.org/cbcnm/studio.mods.auth/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://bitbucket.org/cbcnm/studio.mods.auth#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@shoelace-style/shoelace": "^2.16.0",
|
|
27
|
+
"@wral/sdk-auth": "^0.2.1",
|
|
28
|
+
"@wral/studio-tools": "^0.9.2",
|
|
29
|
+
"@wral/studio-ui": "^2.4.8",
|
|
30
|
+
"lit": "^3.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@open-wc/testing": "^4.0.0",
|
|
34
|
+
"@web/test-runner": "^0.19.0",
|
|
35
|
+
"@web/test-runner-coverage-v8": "^0.8.0",
|
|
36
|
+
"@web/test-runner-playwright": "^0.11.0",
|
|
37
|
+
"chai": "^5.1.1",
|
|
38
|
+
"eslint": "^9.10.0",
|
|
39
|
+
"husky": "^9.1.5",
|
|
40
|
+
"playwright": "^1.47.0",
|
|
41
|
+
"sinon": "^18.0.1",
|
|
42
|
+
"vite": "^5.4.4"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getToken, logout } from './util.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initializes authentication-related subscriptions with the given studio
|
|
5
|
+
* instance and configuration. This function sets up listeners for token
|
|
6
|
+
* requests and logout actions.
|
|
7
|
+
*
|
|
8
|
+
* @module studio-mod_auth
|
|
9
|
+
* @param {Object} studio - The studio instance where the authentication
|
|
10
|
+
* module is registered.
|
|
11
|
+
* @param {Object} config - Configuration object for the initialization
|
|
12
|
+
* (currently not used in the function).
|
|
13
|
+
*/
|
|
14
|
+
export function init(studio, config) {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Event Subscription: 'token-request'
|
|
18
|
+
* Handles requests for authentication tokens.
|
|
19
|
+
*
|
|
20
|
+
* @event studio.wral::mod-auth::token-request
|
|
21
|
+
* @type {object}
|
|
22
|
+
* @property {Object} state - Contains the state information from the
|
|
23
|
+
* event trigger.
|
|
24
|
+
* @property {Function} state.value.callback - Callback function to handle
|
|
25
|
+
* the token response.
|
|
26
|
+
* @param {string|null} state.value.token - Expected to receive parameters
|
|
27
|
+
* `token` (string|null) if successful, or `error` (Error|null) if an
|
|
28
|
+
* error occurs.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* studio.pub('studio.wral::mod-auth', 'token-request', {
|
|
32
|
+
* state: {
|
|
33
|
+
* value: {
|
|
34
|
+
* callback: (token, error) => {
|
|
35
|
+
* if (error) {
|
|
36
|
+
* console.error('Error retrieving token:', error);
|
|
37
|
+
* } else {
|
|
38
|
+
* console.log('Received token:', token);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* });
|
|
44
|
+
*/
|
|
45
|
+
studio.sub('studio.wral::mod-auth', 'token-request', ({state}) => {
|
|
46
|
+
const { callback } = state.value;
|
|
47
|
+
if (callback) {
|
|
48
|
+
getToken({ studio, config })
|
|
49
|
+
.then(token => callback(token))
|
|
50
|
+
.catch(error => callback(null, error));
|
|
51
|
+
} else {
|
|
52
|
+
/** Warn consumers of this mod if a message has been sent without a
|
|
53
|
+
* callback. */
|
|
54
|
+
/* eslint-disable no-console */
|
|
55
|
+
console.warn("No token callback defined");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Event Subscription: 'logout'
|
|
61
|
+
* Initiates the logout process upon receiving the event.
|
|
62
|
+
*
|
|
63
|
+
* @event studio.wral::mod-auth::logout
|
|
64
|
+
*
|
|
65
|
+
* Usage:
|
|
66
|
+
* studio.pub('studio.wral::mod-auth', 'logout');
|
|
67
|
+
*/
|
|
68
|
+
studio.sub('studio.wral::mod-auth', 'logout', () => {
|
|
69
|
+
logout({ studio });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// studio.pub('system::workspace', 'present', studioProfileView);
|
|
73
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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);
|
|
@@ -0,0 +1,84 @@
|
|
|
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);
|
|
@@ -0,0 +1,94 @@
|
|
|
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);
|
|
@@ -0,0 +1,56 @@
|
|
|
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>`
|
|
@@ -0,0 +1,110 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|