WIP: user_settings app #34
@ -1,5 +0,0 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🪪",
|
||||
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(async function get_private_key(id) {
|
||||
return bip39Words(await ssb.getPrivateKey(id));
|
||||
});
|
||||
tfrpc.register(async function create_id(id) {
|
||||
return await ssb.createIdentity();
|
||||
});
|
||||
tfrpc.register(async function add_id(id) {
|
||||
return await ssb.addIdentity(bip39Bytes(id));
|
||||
});
|
||||
tfrpc.register(async function delete_id(id) {
|
||||
return await ssb.deleteIdentity(id);
|
||||
});
|
||||
tfrpc.register(async function reload() {
|
||||
await main();
|
||||
});
|
||||
|
||||
async function main() {
|
||||
let ids = await ssb.getIdentities();
|
||||
await app.setDocument(
|
||||
`<body style="color: #fff">
|
||||
<script>const handler = {};</script>
|
||||
<script type="module">
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
handler.export_id = async function export_id(event) {
|
||||
let id = event.srcElement.dataset.id;
|
||||
let element = document.createElement('textarea');
|
||||
element.value = await tfrpc.rpc.get_private_key(id);
|
||||
element.style = 'width: 100%; read-only: true';
|
||||
element.readOnly = true;
|
||||
event.srcElement.parentElement.appendChild(element);
|
||||
event.srcElement.onclick = event => handler.hide_id(event, element);
|
||||
}
|
||||
handler.add_id = async function add_id(event) {
|
||||
let id = document.getElementById('add_id').value;
|
||||
try {
|
||||
let new_id = await tfrpc.rpc.add_id(id);
|
||||
alert('Successfully imported: ' + new_id);
|
||||
await tfrpc.rpc.reload();
|
||||
} catch (e) {
|
||||
alert('Error importing identity: ' + e);
|
||||
}
|
||||
}
|
||||
handler.create_id = async function create_id(event) {
|
||||
try {
|
||||
let id = await tfrpc.rpc.create_id();
|
||||
alert('Successfully created: ' + id);
|
||||
await tfrpc.rpc.reload();
|
||||
} catch (e) {
|
||||
alert('Error creating identity: ' + e);
|
||||
}
|
||||
}
|
||||
handler.hide_id = function hide_id(event, element) {
|
||||
element.parentNode.removeChild(element);
|
||||
event.srcElement.onclick = handler.export_id;
|
||||
}
|
||||
handler.delete_id = async function delete_id(event) {
|
||||
let id = event.srcElement.dataset.id;
|
||||
try {
|
||||
if (prompt('Are you sure you want to delete "' + id + '"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
|
||||
if (await tfrpc.rpc.delete_id(id)) {
|
||||
alert('Successfully deleted ID: ' + id);
|
||||
}
|
||||
await tfrpc.rpc.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error deleting ID: ' + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1>SSB Identity Management</h1>
|
||||
<h2>Create a new identity</h2>
|
||||
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
||||
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
||||
<h2>Identities</h2>
|
||||
<ul>` +
|
||||
ids
|
||||
.map(
|
||||
(id) => `<li>
|
||||
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
||||
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
||||
${id}
|
||||
</li>`
|
||||
)
|
||||
.join('\n') +
|
||||
` </ul>
|
||||
</body>`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
1
apps/user_settings.json
Normal file
1
apps/user_settings.json
Normal file
@ -0,0 +1 @@
|
||||
{"type": "tildefriends-app", "emoji": "⚙️"}
|
60
apps/user_settings/app.js
Normal file
60
apps/user_settings/app.js
Normal file
@ -0,0 +1,60 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(async function getIdentities() {
|
||||
return ssb.getIdentities();
|
||||
});
|
||||
tfrpc.register(async function createID(id) {
|
||||
return await ssb.createIdentity();
|
||||
});
|
||||
tfrpc.register(async function getPrivateKey(id) {
|
||||
return bip39Words(await ssb.getPrivateKey(id));
|
||||
});
|
||||
tfrpc.register(async function addID(id) {
|
||||
return await ssb.addIdentity(bip39Bytes(id));
|
||||
});
|
||||
tfrpc.register(async function deleteID(id) {
|
||||
return await ssb.deleteIdentity(id);
|
||||
});
|
||||
tfrpc.register(async function getThemes() {
|
||||
// TODO
|
||||
return ['solarized', 'gruvbox', 'light'];
|
||||
});
|
||||
tfrpc.register(async function getTheme() {
|
||||
// TODO
|
||||
return 'gruvbox';
|
||||
});
|
||||
tfrpc.register(async function setTheme() {
|
||||
// TODO
|
||||
console.warn('setTheme called - not implemented');
|
||||
return null;
|
||||
});
|
||||
tfrpc.register(async function reload() {
|
||||
await main();
|
||||
});
|
||||
|
||||
async function main() {
|
||||
// Get body.html
|
||||
const body = utf8Decode(await getFile('body.html'));
|
||||
|
||||
// Build the document
|
||||
const document = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/static/tildefriends-v1.css"/>
|
||||
<script src="tf-theme-picker.js" type="module"></script>
|
||||
<script src="tf-password-form.js" type="module"></script>
|
||||
<script src="tf-delete-account-btn.js" type="module"></script>
|
||||
<script src="tf-identity-manager.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body class="flex-column">
|
||||
${body}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Send it to the browser
|
||||
app.setDocument(document);
|
||||
}
|
||||
|
||||
main();
|
20
apps/user_settings/body.html
Normal file
20
apps/user_settings/body.html
Normal file
@ -0,0 +1,20 @@
|
||||
<h1>Your settings</h1>
|
||||
|
||||
<div class="box flex-column">
|
||||
<h2>Appearance</h2>
|
||||
|
||||
<tf-theme-picker></tf-theme-picker>
|
||||
</div>
|
||||
|
||||
<div class="box flex-column">
|
||||
<h2>Danger Zone</h2>
|
||||
|
||||
<h3>Manage your identities</h3>
|
||||
<tf-identity-manager></tf-identity-manager>
|
||||
|
||||
<h3>Change my password</h3>
|
||||
<tf-password-form></tf-password-form>
|
||||
|
||||
<h3>Delete your account</h3>
|
||||
<tf-delete-account-btn></tf-delete-account-btn>
|
||||
</div>
|
120
apps/user_settings/lit-all.min.js
vendored
Normal file
120
apps/user_settings/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/user_settings/lit-all.min.js.map
Normal file
1
apps/user_settings/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
36
apps/user_settings/tf-delete-account-btn.js
Normal file
36
apps/user_settings/tf-delete-account-btn.js
Normal file
@ -0,0 +1,36 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfDeleteAccountButtonElement extends LitElement {
|
||||
static get properties() {
|
||||
return {};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
deleteAccount() {
|
||||
const res = confirm(
|
||||
'Are you really sure you want to delete your account ?'
|
||||
);
|
||||
|
||||
if (!res) return;
|
||||
|
||||
console.warn('TODO');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
|
||||
|
||||
<span>This action is irreversible !</span>
|
||||
|
||||
<button class="red" @click=${this.deleteAccount}>
|
||||
[Not implemented] Delete my Tilde Friends account
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-delete-account-btn', TfDeleteAccountButtonElement);
|
118
apps/user_settings/tf-identity-manager.js
Normal file
118
apps/user_settings/tf-identity-manager.js
Normal file
@ -0,0 +1,118 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfIdentityManagerElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
ids: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ids = [];
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.ids = await tfrpc.rpc.getIdentities();
|
||||
}
|
||||
|
||||
async createIdentity() {
|
||||
try {
|
||||
const id = await tfrpc.rpc.createID();
|
||||
alert('Successfully created: ' + id);
|
||||
await tfrpc.rpc.reload();
|
||||
} catch (err) {
|
||||
alert('Error creating identity: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
async importIdentity() {
|
||||
const words = this.renderRoot?.querySelector('#import-id-textarea').value;
|
||||
if (!words) return;
|
||||
|
||||
try {
|
||||
const newID = await tfrpc.rpc.addID(words);
|
||||
|
||||
if (newID) alert('Successfully imported a new identity.');
|
||||
else alert('This identity already exists or is invalid.');
|
||||
await tfrpc.rpc.reload();
|
||||
} catch (err) {
|
||||
alert('Error importing identity: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
async exportIdentity(id) {
|
||||
alert(
|
||||
'Your private key is:\n' +
|
||||
(await tfrpc.rpc.getPrivateKey(id)) +
|
||||
'\nDo not share it with anyone!'
|
||||
);
|
||||
}
|
||||
|
||||
async deleteIdentity(id) {
|
||||
try {
|
||||
if (
|
||||
prompt(
|
||||
'Are you sure you want to delete "' +
|
||||
id +
|
||||
'"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.'
|
||||
) === 'DELETE'
|
||||
) {
|
||||
if (await tfrpc.rpc.deleteID(id)) {
|
||||
alert('Successfully deleted ID: ' + id);
|
||||
}
|
||||
await tfrpc.rpc.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error deleting ID: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <link rel="stylesheet" href="/static/tildefriends-v1.css" />
|
||||
<style>
|
||||
.id-span {
|
||||
font-family: monospace;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h4>Create a new identity</h4>
|
||||
<button id="create-id" class="green" @click=${this.createIdentity}>
|
||||
Create Identity
|
||||
</button>
|
||||
|
||||
<h4>Import an SSB Identity from 12 BIP39 English Words</h4>
|
||||
<textarea id="import-id-textarea" style="width: 100%" rows="4"></textarea>
|
||||
<button class="green" @click=${this.importIdentity}>
|
||||
Import Identity
|
||||
</button>
|
||||
|
||||
<h4>Warning !</h4>
|
||||
<strong
|
||||
>Anybody that knows your private key can gain total access over your
|
||||
account.</strong
|
||||
>
|
||||
<br /><br />
|
||||
Tilde Friends' contributors will never ask you for your private key !
|
||||
|
||||
<ul>
|
||||
${this.ids.map(
|
||||
(id) =>
|
||||
html` <li>
|
||||
<button class="blue" @click=${() => this.exportIdentity(id)}>
|
||||
Export Identity
|
||||
</button>
|
||||
<button class="red" @click=${() => this.deleteIdentity(id)}>
|
||||
Delete Identity
|
||||
</button>
|
||||
<span class="id-span">${id}</span>
|
||||
</li>`
|
||||
)}
|
||||
</ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-identity-manager', TfIdentityManagerElement);
|
82
apps/user_settings/tf-password-form.js
Normal file
82
apps/user_settings/tf-password-form.js
Normal file
@ -0,0 +1,82 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfPasswordFormElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
//selected: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a password against different requirements
|
||||
* @param {string} password the password to validate
|
||||
* @returns
|
||||
*/
|
||||
validatePassword(password) {
|
||||
// TODO(tasiaiso)
|
||||
return true;
|
||||
}
|
||||
|
||||
submitPassword() {
|
||||
const currentPwd = this.shadowRoot.getElementById('current').value;
|
||||
const newPwd = this.shadowRoot.getElementById('new').value;
|
||||
const repeatPwd = this.shadowRoot.getElementById('Repeat').value;
|
||||
|
||||
if (!(newPwd === repeatPwd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// tfrpc.changePassword()
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="grid">
|
||||
<label for="current">Current password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="current"
|
||||
name="current"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<label for="new">Enter new password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="new"
|
||||
name="new"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<label for="repeat">Repeat new password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="repeat"
|
||||
name="repeat"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button @click=${this.submitPassword} class="red">
|
||||
[Not implemented] Change my password
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-password-form', TfPasswordFormElement);
|
51
apps/user_settings/tf-theme-picker.js
Normal file
51
apps/user_settings/tf-theme-picker.js
Normal file
@ -0,0 +1,51 @@
|
||||
import {LitElement, html, nothing} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfThemePickerElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
selected: {type: String},
|
||||
themes: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.themes = await tfrpc.rpc.getThemes();
|
||||
this.selected = await tfrpc.rpc.getTheme();
|
||||
|
||||
let select = this.renderRoot?.querySelector('#theme-select');
|
||||
select.value = this.selected;
|
||||
}
|
||||
|
||||
changed(event) {
|
||||
this.selected = event.srcElement.value;
|
||||
console.log('selected theme', this.selected);
|
||||
// TODO
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
|
||||
|
||||
<label for="theme">[Not implemented] Choose your theme:</label>
|
||||
|
||||
<select
|
||||
name="theme"
|
||||
id="theme-select"
|
||||
?hidden=${!this.themes?.length}
|
||||
@change=${this.changed}
|
||||
>
|
||||
${(this.themes ?? []).map(
|
||||
(name) => html`<option value=${name}>${name}</option>`
|
||||
)}
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-theme-picker', TfThemePickerElement);
|
114
core/tildefriends-v1.css
Normal file
114
core/tildefriends-v1.css
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Tilde Friends core stylesheet
|
||||
*
|
||||
* This Software is an external library that is part of
|
||||
* Tilde Friends and is shared under the MIT license.
|
||||
*
|
||||
* Inject this file in your app at tildefriends.css
|
||||
* and use this tag to import it:
|
||||
* <link rel="stylesheet" href="/static/tildefriends-v1.css"/>
|
||||
*
|
||||
* v1.0 / 2024 M03 21
|
||||
*/
|
||||
|
||||
body {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
.button,
|
||||
input[type='button'],
|
||||
input[type='submit'],
|
||||
select {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
|
||||
&.red {
|
||||
background-color: #bd1e24;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background-color: #18922d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background-color: #0067a7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.yellow {
|
||||
background-color: #ee9600;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #6c71c4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #2aa198;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #ffffff40;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.inline-flex-row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: #00000020;
|
||||
border: 1px solid grey;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 4px;
|
||||
}
|
@ -644,6 +644,7 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
|
||||
"style.css",
|
||||
"tfrpc.js",
|
||||
"w3.css",
|
||||
"tildefriends-v1.css"
|
||||
};
|
||||
|
||||
const char* k_map[][2] = {
|
||||
|
Loading…
Reference in New Issue
Block a user