permissions (basic)

main
june moretz 2023-05-28 17:56:08 -04:00
parent 85d5297c94
commit 89f8032034
13 changed files with 529 additions and 248 deletions

View File

@ -90,7 +90,12 @@ Message format:
```json
{
"name": "thread name"
"name": "thread name",
"permissions": {
"view_limited": true,
"post_limited": true
},
"members": ["username1", "username2"]
}
```

View File

@ -1,161 +1,218 @@
function rand() {
let str = "";
const lookups =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
while (str.length < 16) {
const n = Math.random() * lookups.length;
str += lookups[Math.floor(n)];
}
return str;
let str = "";
const lookups =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
while (str.length < 16) {
const n = Math.random() * lookups.length;
str += lookups[Math.floor(n)];
}
return str;
}
async function auth() {
let session = rand();
const sig = await openpgp.sign({
message: new openpgp.CleartextMessage("vybe_auth " + session, ""),
signingKeys: window.keys.priv,
});
window.session = session;
window.socket.emit("authenticate", { name: window.name, message: sig });
let session = rand();
const sig = await openpgp.sign({
message: new openpgp.CleartextMessage("vybe_auth " + session, ""),
signingKeys: window.keys.priv,
});
window.session = session;
window.socket.emit("authenticate", { name: window.name, message: sig });
}
async function loadKeys(keys) {
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub };
await auth();
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub };
await auth();
}
function chooseThread(thread) {
window.currentThreadId = thread.id;
window.earliestMessage = null;
document.getElementById("messages").innerHTML = "";
document.getElementById("threadname").innerHTML = thread.name;
window.currentThreadId = thread.id;
window.earliestMessage = null;
document.getElementById("messages").innerHTML = "";
document.getElementById("threadname").innerHTML = thread.name;
}
function loadMessages() {
window.socket.emit(
"get_history",
{ before: window.earliestMessage, thread: window.currentThreadId }
);
window.socket.emit("get_history", {
before: window.earliestMessage,
thread: window.currentThreadId,
});
}
function addThread(thread) {
const el = document.createElement("div");
el.classList.add("thread");
el.innerHTML = thread.name;
const btn = document.createElement("button");
btn.innerHTML = "choose";
btn.onclick = () => {
chooseThread(thread);
loadMessages();
document.getElementById("loadmore").classList.remove("hidden");
};
el.appendChild(btn);
document.getElementById("threadlist").appendChild(el);
const el = document.createElement("div");
el.classList.add("thread");
el.innerHTML = thread.name;
const btn = document.createElement("button");
btn.innerHTML = "choose";
btn.onclick = () => {
chooseThread(thread);
loadMessages();
document.getElementById("loadmore").classList.remove("hidden");
if (!thread.permissions.post) {
document.getElementById("msginput").classList.add("hidden");
} else {
document.getElementById("msginput").classList.remove("hidden");
}
};
el.appendChild(btn);
document.getElementById("threadlist").appendChild(el);
}
function addMember() {
const name = document.getElementById("membername").value;
if (!window.threadmembers) {
window.threadmembers = [window.name, name];
} else {
window.threadmembers.push(name);
}
const member = document.createElement("p");
member.textContent = name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
document.getElementById("membername").value = "";
}
window.onload = () => {
window.currentThreadId = 1;
window.socket = io();
window.socket.on("create_user", auth);
window.socket.on("new_message", (msg) => {
if (msg.thread !== window.currentThreadId) return;
const el = document.createElement("div");
el.classList.add("message");
const strong = document.createElement('strong');
strong.textContent = msg.name + ': ';
el.append(strong, msg.message);
document.getElementById("messages").appendChild(el);
if (!window.earliestMessage)
window.earliestMessage = msg.id;
});
window.socket.on("get_history", (msg) => {
if (msg.messages.length > 0) {
window.earliestMessage = msg.messages[msg.messages.length - 1].id;
for (let message of msg.messages) {
const el = document.createElement("div");
el.classList.add("message");
const strong = document.createElement('strong');
strong.textContent = message.name + ': ';
el.append(strong, message.message);
document.getElementById("messages").prepend(el);
}
}
if (!msg.more)
document.getElementById("loadmore").classList.add("hidden");
});
window.socket.on("authenticate", (msg) => {
if (msg.success) {
document.getElementById("register").classList.add("hidden");
document.getElementById("threads").classList.remove("hidden");
document.getElementById("chat").classList.remove("hidden");
window.socket.emit("list_threads");
}
let emitter = window.socket.emit;
window.socket.emit = (type, data) => {
if (data)
return emitter.call(window.socket, type, {
...data,
__session: window.session,
});
else return emitter.call(window.socket, type);
};
});
window.socket.on("list_threads", (msg) => {
document.getElementById("threadlist").innerHTML = "";
for (let thread of msg.threads)
addThread(thread);
});
window.socket.on('new_thread', addThread);
window.socket.on("create_thread", (msg) => {
chooseThread({
name: document.getElementById("newthreadname").value,
id: msg.id,
});
document.getElementById("newthreadname").value = "";
});
window.currentThreadId = 1;
window.socket = io();
window.socket.on("create_user", auth);
window.socket.on("new_message", (msg) => {
if (msg.thread !== window.currentThreadId) return;
const el = document.createElement("div");
el.classList.add("message");
const strong = document.createElement("strong");
strong.textContent = msg.name + ": ";
el.append(strong, msg.message);
document.getElementById("messages").appendChild(el);
if (!window.earliestMessage) window.earliestMessage = msg.id;
});
window.socket.on("get_history", (msg) => {
if (msg.messages.length > 0) {
window.earliestMessage = msg.messages[msg.messages.length - 1].id;
for (let message of msg.messages) {
const el = document.createElement("div");
el.classList.add("message");
const strong = document.createElement("strong");
strong.textContent = message.name + ": ";
el.append(strong, message.message);
document.getElementById("messages").prepend(el);
}
}
if (!msg.more) document.getElementById("loadmore").classList.add("hidden");
});
window.socket.on("authenticate", (msg) => {
if (msg.success) {
document.getElementById("register").classList.add("hidden");
document.getElementById("threads").classList.remove("hidden");
document.getElementById("chat").classList.remove("hidden");
const member = document.createElement("p");
member.textContent = window.name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
let emitter = window.socket.emit;
window.socket.emit = (type, data) => {
if (data)
return emitter.call(window.socket, type, {
...data,
__session: window.session,
});
else return emitter.call(window.socket, type);
};
window.socket.emit("list_threads", {});
} else {
document.getElementById("register").classList.remove("hidden");
}
});
window.socket.on("list_threads", (msg) => {
document.getElementById("threadlist").innerHTML = "";
for (let thread of msg.threads) addThread(thread);
});
window.socket.on("new_thread", addThread);
window.socket.on("create_thread", (msg) => {
chooseThread({
name: document.getElementById("newthreadname").value,
id: msg.id,
});
document.getElementById("newthreadname").value = "";
document.getElementById("loadmore").classList.add("hidden");
document.getElementById("msginput").classList.remove("hidden");
});
document.getElementById("registerform").onsubmit = async e => {
e.preventDefault();
const name = document.getElementById("name").value;
if (!name) return;
const keys = await openpgp.generateKey({
userIDs: [{ name }],
});
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub };
localStorage.setItem("keys", JSON.stringify(keys));
localStorage.setItem("name", name);
window.name = name;
window.socket.emit("create_user", { name, pubkey: keys.publicKey });
};
document.getElementById("msginput").onsubmit = e => {
e.preventDefault();
const msg = document.getElementById("msg").value;
if (!msg) return;
window.socket.emit("send_message", {
message: msg,
thread: window.currentThreadId,
});
document.getElementById("msg").value = "";
};
document.getElementById("loadmore").onclick = e => {
loadMessages();
};
document.getElementById("createthread").onsubmit = e => {
e.preventDefault();
window.socket.emit("create_thread", {
name: document.getElementById("newthreadname").value,
});
};
document.getElementById("registerform").onsubmit = async (e) => {
e.preventDefault();
const name = document.getElementById("name").value;
if (!name) return;
const keys = await openpgp.generateKey({
userIDs: [{ name }],
});
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub };
localStorage.setItem("keys", JSON.stringify(keys));
localStorage.setItem("name", name);
window.name = name;
window.socket.emit("create_user", { name, pubkey: keys.publicKey });
};
document.getElementById("msginput").onsubmit = (e) => {
e.preventDefault();
const msg = document.getElementById("msg").value;
if (!msg) return;
window.socket.emit("send_message", {
message: msg,
thread: window.currentThreadId,
});
document.getElementById("msg").value = "";
};
document.getElementById("loadmore").onclick = (e) => {
loadMessages();
};
document.getElementById("createthread").onsubmit = (e) => {
e.preventDefault();
const perms = document.querySelector(
'input[name="permissions"]:checked'
).value;
let permissions;
if (perms === "public") {
permissions = {
view_limited: false,
post_limited: false,
};
} else if (perms === "private_post") {
permissions = {
view_limited: false,
post_limited: true,
};
} else if (perms === "private_view") {
permissions = {
view_limited: true,
post_limited: true,
};
}
window.socket.emit("create_thread", {
name: document.getElementById("newthreadname").value,
permissions,
members: window.threadmembers || [window.name],
});
document.getElementById(perms).checked = false;
window.threadmembers = null;
document.getElementById("memberlist").innerHTML = "";
const member = document.createElement("p");
member.textContent = window.name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
};
document.getElementById("membername").onkeydown = (e) => {
if (e.key == "Enter") {
addMember();
}
};
document.getElementById("addmember").onclick = addMember;
const keys = localStorage.getItem("keys");
if (keys) {
window.name = localStorage.getItem("name");
loadKeys(JSON.parse(keys)).then(() => {});
}
else
document.getElementById("register").classList.remove("hidden");
const keys = localStorage.getItem("keys");
if (keys) {
window.name = localStorage.getItem("name");
loadKeys(JSON.parse(keys)).then(() => {});
} else document.getElementById("register").classList.remove("hidden");
};

View File

@ -1,89 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/openpgp.min.js"></script>
<script src="/socket.io.min.v4.6.1.js"></script>
<script src="/chat.js"></script>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vybe</title>
<style>
* {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif;
}
body, button, input {
background: #020202;
color: #eaeaea;
}
body {
display: flex;
align-items: stretch;
margin: 0;
min-width: min-content;
}
.column {
flex-grow: 1;
}
button {
border-color: #767676;
}
.hidden {
display: none;
}
#msginput {
margin-top: 15px;
}
.message {
margin-bottom: 5px;
}
#loadmore {
margin-bottom: 10px;
}
.channel {
font-weight: normal;
}
</style>
</head>
<body>
<div id="register" class="hidden">
<h1>welcome to vybe</h1>
<h3>a communication network (beta)</h3>
<p>
to get started, you'll need an account. we use public key
cryptography for security, rather than passwords. for now
your keys are stored in your browser storage only.
</p>
<form id="registerform">
<label for="name">name/username: </label>
<input type="text" id="name" />
<button id="submit" type="submit">generate keys & register</button>
</form>
</div>
<div id="threads" class="column hidden">
<h1>vybe</h1>
<h3>threads</h3>
<div id="threadlist">loading...</div>
<h3>create thread</h3>
<form id="createthread">
<label for="newthreadname">thread name</label>
<input type="text" id="newthreadname" />
<button id="submitthread" type="submit">create</button>
</form>
</div>
<div id="chat" class="column hidden">
<h3 class="thread">
current thread: <strong id="threadname">meow</strong>
</h3>
<h3>messages will appear below as they are sent</h3>
<button id="loadmore">load more messages</button>
<div id="messages"></div>
<form id="msginput">
<input type="text" placeholder="write a message..." id="msg" />
<button type="submit" class="hidden" id="sendmsg"></button>
</form>
</div>
</body>
<head>
<script src="/openpgp.min.js"></script>
<script src="/socket.io.min.v4.6.1.js"></script>
<script src="/chat.js"></script>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vybe</title>
<style>
* {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif;
}
body,
button,
input {
background: #020202;
color: #eaeaea;
}
body {
display: flex;
align-items: stretch;
margin: 0;
min-width: min-content;
padding: 0 20px;
}
.column {
flex: 1;
max-width: 50vw;
overflow: hidden;
}
button {
border-color: #767676;
}
.hidden {
display: none;
}
#msginput {
margin-top: 15px;
}
.message {
margin-bottom: 5px;
overflow-wrap: break-word;
}
#loadmore {
margin-bottom: 10px;
}
.channel {
font-weight: normal;
}
.member {
margin: 5px 0;
}
</style>
</head>
<body>
<div id="register" class="hidden">
<h1>welcome to vybe</h1>
<h3>a communication network (beta)</h3>
<p>
to get started, you'll need an account. we use public key cryptography
for security, rather than passwords. for now your keys are stored in
your browser storage only.
</p>
<form id="registerform">
<label for="name">name/username: </label>
<input type="text" id="name" />
<button id="submit" type="submit">generate keys & register</button>
</form>
</div>
<div id="threads" class="column hidden">
<h1>vybe</h1>
<h3>threads</h3>
<div id="threadlist">loading...</div>
<h3>create thread</h3>
<form id="createthread">
<label for="newthreadname">thread name</label>
<input type="text" id="newthreadname" /><br /><br />
<span>thread permissions</span><br />
<input type="radio" id="public" name="permissions" value="public" />
<label for="public">anyone can view and post</label><br />
<input
type="radio"
id="private_post"
name="permissions"
value="private_post"
/>
<label for="private_post">only members can post, anyone can view</label
><br />
<input
type="radio"
id="private_view"
name="permissions"
value="private_view"
/>
<label for="private_view">only members can view and post</label
><br /><br />
<span>members</span><br />
<input type="text" id="membername" placeholder="username" /><button
id="addmember"
>
add
</button>
<div id="memberlist"></div>
<br />
<button id="submitthread" type="submit">create</button>
</form>
</div>
<div id="chat" class="column hidden">
<h3 class="thread">
current thread: <strong id="threadname">meow</strong>
</h3>
<h3>messages will appear below as they are sent</h3>
<button id="loadmore" class="hidden">load more messages</button>
<div id="messages"></div>
<form id="msginput">
<input type="text" placeholder="write a message..." id="msg" />
<button type="submit" class="hidden" id="sendmsg"></button>
</form>
</div>
</body>
</html>

View File

@ -28,14 +28,14 @@ CREATE TABLE permissions (
value text,
foreign key(user) references users(id),
foreign key(thread) references threads(id)
)
);
CREATE TABLE members (
thread integer,
user integer,
foreign key(user) references users(id),
foreign key(thread) references threads(id)
)
);
CREATE TABLE posts (
id integer primary key asc,

View File

@ -9,8 +9,8 @@
## permissions -> permission
- manage_permissions
- add_users
- remove_users
- add_members
- remove_members
- view
- post

View File

@ -14,10 +14,17 @@ const PORT = process.env.PORT || 3435;
const actions = require("./src/actions");
io.cache = {};
io.on("connection", (socket) => {
for (let action in actions) {
socket.on(action, (msg) =>
actions[action](msg, (response) => socket.emit(action, response), socket, io)
actions[action](
msg,
(response) => socket.emit(action, response),
socket,
io
)
);
}
});

View File

@ -1,7 +1,7 @@
const db = require("../db");
const openpgp = require("openpgp");
const authenticate = async (msg, respond, socket) => {
const authenticate = async (msg, respond, socket, io) => {
if (!msg.name || !msg.message) {
return respond({
success: false,
@ -43,6 +43,11 @@ const authenticate = async (msg, respond, socket) => {
data[1],
]);
socket.userid = result.rows[0].id;
if (io.cache[msg.name]) {
io.cache[msg.name].push(socket.id);
} else {
io.cache[msg.name] = [socket.id];
}
return respond({
success: true,
});

View File

@ -1,7 +1,7 @@
const db = require("../db");
const authwrap = (fn) => async (msg, respond, socket, io) => {
if (!msg.__session) {
if (!msg || !msg.__session) {
return respond({
success: false,
message: "Not authenticated",

View File

@ -20,10 +20,82 @@ const create_thread = async (msg, respond, socket, io) => {
"insert into threads (name, creator) values (?, ?) returning id",
[msg.name, msg.auth_user.id]
);
io.emit('new_thread', {
name: msg.name,
id: insert.rows[0].id
});
const thread_id = insert.rows[0].id;
// set up permissions
if (!msg.permissions || !msg.permissions.view_limited) {
await db.query(
`insert into permissions (thread, type, flexible, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, "everyone", false, "view", "true"]
);
if (!msg.permissions || !msg.permissions.post_limited) {
await db.query(
`insert into permissions (thread, type, flexible, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, "everyone", false, "post", "true"]
);
} else {
await db.query(
`insert into permissions (thread, type, flexible, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, "members", false, "post", "true"]
);
}
} else {
await db.query(
`insert into permissions (thread, type, flexible, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, "members", false, "view", "true"]
);
await db.query(
`insert into permissions (thread, type, flexible, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, "members", false, "post", "true"]
);
}
// add members
for (let user of msg.members) {
// get user id
const id = await db.query("select id from users where name = ?", [user]);
if (id.rows.length > 0) {
const user_id = id.rows[0].id;
await db.query("insert into members (thread, user) values (?, ?)", [
thread_id,
user_id,
]);
}
}
const member_perms = {
is_member: true,
view: true,
post: true,
};
const general_perms = {
is_member: false,
view: !msg.permissions || !msg.permissions.view_limited,
post: !msg.permissions || !msg.permissions.post_limited,
};
for (let username in io.cache) {
if (msg.members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_thread", {
name: msg.name,
id: insert.rows[0].id,
permissions: member_perms,
});
}
} else if (general_perms.view) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_thread", {
name: msg.name,
id: insert.rows[0].id,
permissions: general_perms,
});
}
}
}
// respond
return respond({
success: true,

View File

@ -1,4 +1,6 @@
const db = require("../db");
const authwrap = require("./authwrap");
const check_permissions = require("./helpers/check_permissions");
const get_history = async (msg, respond) => {
if (msg.before && isNaN(Number(msg.before))) {
@ -13,6 +15,12 @@ const get_history = async (msg, respond) => {
message: "thread ID required",
});
}
if (!(await check_permissions(msg.auth_user.id, msg.thread)).view) {
return respond({
success: false,
message: "you can't view this thread",
});
}
const messages = await db.query(
`select users.name, posts.id, content from posts
join users on posts.user = users.id
@ -31,4 +39,4 @@ const get_history = async (msg, respond) => {
});
};
module.exports = get_history;
module.exports = authwrap(get_history);

View File

@ -0,0 +1,37 @@
const db = require("../../db");
const check_permissions = async (user_id, thread_id) => {
// get all the permissions for the thread
const permissions = await db.query(
"select * from permissions where thread = ?",
[thread_id]
);
// check if the user is a member
const is_member =
(
await db.query("select * from members where thread = ? and user = ?", [
thread_id,
user_id,
])
).rows.length > 0;
const get_permission = (permission) => {
const relevant = permissions.rows.filter(
(i) => i.permission === permission
);
for (let i of relevant) {
if (i.type === "everyone" && i.value === "true") {
return true;
}
if (i.type === "members" && i.value === "true" && is_member) {
return true;
}
}
};
return {
is_member,
view: get_permission("view"),
post: get_permission("post"),
};
};
module.exports = check_permissions;

View File

@ -1,14 +1,32 @@
const db = require("../db");
const authwrap = require("./authwrap");
const check_permissions = require("./helpers/check_permissions");
const list_threads = async (msg, respond) => {
const threads = await db.query(
"select name, id from threads order by created desc"
`select name, id from threads
join permissions on threads.id = permissions.thread
join members on threads.id = members.thread
where permissions.permission = 'view'
and permissions.value = 'true'
and ((permissions.type = 'everyone') or
permissions.type = 'members' and members.user = ?)
group by threads.id
order by created desc`,
[msg.auth_user.id]
);
// respond
const rows = [];
for (let i of threads.rows) {
rows.push({
...i,
permissions: await check_permissions(msg.auth_user.id, i.id),
});
}
return respond({
success: true,
threads: threads.rows,
threads: rows,
});
};
module.exports = list_threads;
module.exports = authwrap(list_threads);

View File

@ -1,24 +1,60 @@
const db = require("../db");
const authwrap = require("./authwrap");
const check_permissions = require("./helpers/check_permissions");
const send_message = async (msg, respond, socket) => {
const send_message = async (msg, respond, socket, io) => {
if (!msg.thread) {
return respond({
success: false,
message: "thread ID required",
});
}
if (!(await check_permissions(msg.auth_user.id, msg.thread)).post) {
return respond({
success: false,
message: "you can't post to this thread",
});
}
// add message and send it to everyone
const id = await db.query(
"insert into posts (user, thread, content) values (?, ?, ?) returning id",
[msg.auth_user.id, msg.thread, msg.message]
);
socket.broadcast.emit("new_message", {
id: id.rows[0].id,
name: msg.auth_user.name,
message: msg.message,
thread: msg.thread,
});
// get thread members
const members = (
await db.query(
"select name from users join members on members.user = users.id where members.thread = ?",
[msg.thread]
)
).rows.map((i) => i.name);
// get perms
const permissions = await db.query(
"select * from permissions where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
[msg.thread]
);
for (let username in io.cache) {
if (members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_message", {
id: id.rows[0].id,
name: msg.auth_user.name,
message: msg.message,
thread: msg.thread,
});
}
} else if (permissions.rows.length > 0) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_message", {
id: id.rows[0].id,
name: msg.auth_user.name,
message: msg.message,
thread: msg.thread,
});
}
}
}
return respond({
success: true,
id: id.rows[0].id,