Exposed deleting users, mostly for my own testing, and used it to make a primitive admin app. Add a handful of apps I've been kicking around without version control, while I'm at it.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3950 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
Cory McWilliams 2022-08-04 00:57:56 +00:00
parent 353f2ccc13
commit fbfbd6a6b4
13 changed files with 326 additions and 0 deletions

1
apps/cory/admin.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&xotWQ8M3xgnWAPM/1TdrLmkcCyxGPiXqg9CsBm2ngcc=.sha256","index.html":"&PrdNng+/SYCFSEbx+E7tMKxs4/ypPDxbRlak4tGN/SM=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&hW7AyNMgC+paQBFDcggxmhwNWmEY+5HofubRalcz6u8=.sha256"}}

13
apps/cory/admin/app.js Normal file
View File

@ -0,0 +1,13 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(function delete_user(user) {
return core.deleteUser(user);
});
async function main() {
let data = {
users: await core.users(),
};
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
}
main();

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<script>const g_data = $data;</script>
</head>
<body style="color: #fff">
<h1>Test</h1>
</body>
<script type="module" src="script.js"></script>
</html>

13
apps/cory/admin/lit.min.js vendored Normal file

File diff suppressed because one or more lines are too long

21
apps/cory/admin/script.js Normal file
View File

@ -0,0 +1,21 @@
import {html, render} from './lit.min.js';
import * as tfrpc from '/static/tfrpc.js';
function delete_user(user) {
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
tfrpc.rpc.delete_user(user).then(function() {
alert(`User "${user}" deleted successfully.`);
}).catch(function(error) {
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
});
}
}
window.addEventListener('load', function() {
const user_template = (user) => html`<li><button @click=${(e) => delete_user(user)}>Delete</button> ${user}</li>`;
const users_template = (users) =>
html`<ul>
${users.map(u => user_template(u))}
</ul>`;
render(users_template(g_data.users), document.body);
});

1
apps/cory/api.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}

11
apps/cory/api/app.js Normal file
View File

@ -0,0 +1,11 @@
var global = Function('return this')();
function treeify(o) {
if (typeof(o) == 'object') {
return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
} else if (typeof(o) == 'function') {
return 'function';
} else if (typeof(o) == 'string' || typeof(o) == 'number') {
return o;
}
}
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(global), null, 2)}</pre>`);

1
apps/cory/db.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}

70
apps/cory/db/app.js Normal file
View File

@ -0,0 +1,70 @@
async function database_list() {
var dbs = await databases();
var doc = `<!DOCTYPE html>
<html>
<body style="background: #888">
<h1>Databases</h1>
<ul id="dbs"></ul>
</body>
<script>
function populate_dbs(id, dbs) {
var list = document.getElementById(id);
for (let db of dbs) {
var li = list.appendChild(document.createElement('li'));
var a = document.createElement('a');
a.innerText = db;
a.href = './#' + db;
a.target = '_top';
li.appendChild(a);
}
}
populate_dbs('dbs', ${JSON.stringify(dbs)});
</script>
</html>`
app.setDocument(doc);
}
async function key_list(db) {
let keys = await db.getAll();
let object = {};
for (let key of keys) {
object[key] = await db.get(key);
}
let doc = `<!DOCTYPE html>
<html>
<body style="background: #888">
<a href="#" target="_top">back</a>
<h1>Keys</h1>
<ul id="keys"></ul>
</body>
<script>
function populate_dbs(id, keys) {
var list = document.getElementById(id);
for (let [key, value] of Object.entries(keys)) {
var li = list.appendChild(document.createElement('li'));
li.innerText = key + ' = ' + value;
}
}
populate_dbs('keys', ${JSON.stringify(object)});
</script>
</html>`
app.setDocument(doc);
}
core.register('message', async function(message) {
if (message.event == 'hashChange') {
let hash = message.hash.substring(1);
if (hash.startsWith(':shared:')) {
let parts = hash.split(':');
let packageName = parts[3];
let key = parts.slice(4).join(':');
key_list(await my_shared_database(packageName, key));
} else if (hash.length) {
key_list(await database(hash.split(':').slice(1).join(':')));
} else {
database_list();
}
}
});
database_list();

1
apps/cory/follow.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&+LbIl429+UZeS9Nh8zO6n7pzRfWOfFF2K/Hg7Kq2HQo=.sha256"}}

159
apps/cory/follow/app.js Normal file
View File

@ -0,0 +1,159 @@
"use strict";
var g_following_cache = {};
var g_following_deep_cache = {};
var g_about_cache = {};
async function following(db, id) {
if (g_following_cache[id]) {
return g_following_cache[id];
}
var o = await db.get(id + ":following");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], sequence: 0, version: k_version};
}
f.users = new Set(f.users);
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
" json_extract(content, '$.following') AS following "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'contact' "+
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
f.sequence = row.sequence;
});
var as_set = f.users;
f.users = Array.from(f.users).sort();
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":following", j);
}
f.users = as_set;
g_following_cache[id] = f.users;
return f.users;
}
async function followingDeep(db, seed_ids, depth) {
if (depth <= 0) {
return seed_ids;
}
var key = JSON.stringify([seed_ids, depth]);
if (g_following_deep_cache[key]) {
return g_following_deep_cache[key];
}
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
var ids = [].concat(...f);
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
x = [...new Set([].concat(...x, ...seed_ids))].sort();
g_following_deep_cache[key] = x;
return x;
}
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
var o = await db.get(id + ":about");
const k_version = 4;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" content "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'about' AND "+
" json_extract(content, '$.about') = ?1 "+
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
f.sequence = row.sequence;
if (row.content) {
var about = {};
try {
about = JSON.parse(row.content);
} catch {
}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
});
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":about", j);
}
g_about_cache[id] = f.about;
return f.about;
}
async function getSize(db, id) {
let size = 0;
await ssb.sqlStream(
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
[id],
function (row) {
size += row.size;
});
return size;
}
function niceSize(bytes) {
let value = bytes;
let unit = 'B';
const k_units = ['kB', 'MB', 'GB', 'TB'];
for (let u of k_units) {
if (value >= 1024) {
value /= 1024;
unit = u;
} else {
break;
}
}
return Math.round(value * 10) / 10 + ' ' + unit;
}
async function buildTree(db, root, indent, depth) {
var f = await following(db, root);
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
if (depth > 0) {
for (let next of f) {
result += await buildTree(db, next, indent + ' ', depth - 1);
}
}
return result;
}
async function main() {
await app.setDocument('<pre style="color: #fff">building...</pre>');
var db = await database('ssb');
var whoami = await ssb.getIdentities();
var tree = '';
for (let id of whoami) {
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
tree += await buildTree(db, id, '', 2);
}
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
}
main();

View File

@ -224,6 +224,27 @@ async function getProcessBlob(blobId, key, options) {
}, },
} }
}; };
if (process.credentials?.permissions?.administration) {
imports.core.deleteUser = function(user) {
return imports.core.permissionTest('delete_user').then(function() {
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {
}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
db.set('users', users);
}
});
}
}
if (options.api) { if (options.api) {
imports.app = {}; imports.app = {};
for (let i in options.api) { for (let i in options.api) {

View File

@ -287,8 +287,12 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
else else
{ {
const char* k_export[] = { const char* k_export[] = {
"/~cory/api",
"/~cory/admin",
"/~cory/apps", "/~cory/apps",
"/~cory/db",
"/~cory/docs", "/~cory/docs",
"/~cory/follow",
"/~cory/ssb", "/~cory/ssb",
}; };
for (int i = 0; i < (int)_countof(k_export); i++) for (int i = 0; i < (int)_countof(k_export); i++)