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 ```json
{ {
"name": "thread name" "name": "thread name",
"permissions": {
"view_limited": true,
"post_limited": true
},
"members": ["username1", "username2"]
} }
``` ```

View File

@ -34,10 +34,10 @@ function chooseThread(thread) {
} }
function loadMessages() { function loadMessages() {
window.socket.emit( window.socket.emit("get_history", {
"get_history", before: window.earliestMessage,
{ before: window.earliestMessage, thread: window.currentThreadId } thread: window.currentThreadId,
); });
} }
function addThread(thread) { function addThread(thread) {
@ -50,11 +50,30 @@ function addThread(thread) {
chooseThread(thread); chooseThread(thread);
loadMessages(); loadMessages();
document.getElementById("loadmore").classList.remove("hidden"); 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); el.appendChild(btn);
document.getElementById("threadlist").appendChild(el); 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.onload = () => {
window.currentThreadId = 1; window.currentThreadId = 1;
window.socket = io(); window.socket = io();
@ -63,12 +82,11 @@ window.onload = () => {
if (msg.thread !== window.currentThreadId) return; if (msg.thread !== window.currentThreadId) return;
const el = document.createElement("div"); const el = document.createElement("div");
el.classList.add("message"); el.classList.add("message");
const strong = document.createElement('strong'); const strong = document.createElement("strong");
strong.textContent = msg.name + ': '; strong.textContent = msg.name + ": ";
el.append(strong, msg.message); el.append(strong, msg.message);
document.getElementById("messages").appendChild(el); document.getElementById("messages").appendChild(el);
if (!window.earliestMessage) if (!window.earliestMessage) window.earliestMessage = msg.id;
window.earliestMessage = msg.id;
}); });
window.socket.on("get_history", (msg) => { window.socket.on("get_history", (msg) => {
if (msg.messages.length > 0) { if (msg.messages.length > 0) {
@ -76,22 +94,23 @@ window.onload = () => {
for (let message of msg.messages) { for (let message of msg.messages) {
const el = document.createElement("div"); const el = document.createElement("div");
el.classList.add("message"); el.classList.add("message");
const strong = document.createElement('strong'); const strong = document.createElement("strong");
strong.textContent = message.name + ': '; strong.textContent = message.name + ": ";
el.append(strong, message.message); el.append(strong, message.message);
document.getElementById("messages").prepend(el); document.getElementById("messages").prepend(el);
} }
} }
if (!msg.more) if (!msg.more) document.getElementById("loadmore").classList.add("hidden");
document.getElementById("loadmore").classList.add("hidden");
}); });
window.socket.on("authenticate", (msg) => { window.socket.on("authenticate", (msg) => {
if (msg.success) { if (msg.success) {
document.getElementById("register").classList.add("hidden"); document.getElementById("register").classList.add("hidden");
document.getElementById("threads").classList.remove("hidden"); document.getElementById("threads").classList.remove("hidden");
document.getElementById("chat").classList.remove("hidden"); document.getElementById("chat").classList.remove("hidden");
window.socket.emit("list_threads"); const member = document.createElement("p");
} member.textContent = window.name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
let emitter = window.socket.emit; let emitter = window.socket.emit;
window.socket.emit = (type, data) => { window.socket.emit = (type, data) => {
if (data) if (data)
@ -101,22 +120,27 @@ window.onload = () => {
}); });
else return emitter.call(window.socket, type); 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) => { window.socket.on("list_threads", (msg) => {
document.getElementById("threadlist").innerHTML = ""; document.getElementById("threadlist").innerHTML = "";
for (let thread of msg.threads) for (let thread of msg.threads) addThread(thread);
addThread(thread);
}); });
window.socket.on('new_thread', addThread); window.socket.on("new_thread", addThread);
window.socket.on("create_thread", (msg) => { window.socket.on("create_thread", (msg) => {
chooseThread({ chooseThread({
name: document.getElementById("newthreadname").value, name: document.getElementById("newthreadname").value,
id: msg.id, id: msg.id,
}); });
document.getElementById("newthreadname").value = ""; document.getElementById("newthreadname").value = "";
document.getElementById("loadmore").classList.add("hidden");
document.getElementById("msginput").classList.remove("hidden");
}); });
document.getElementById("registerform").onsubmit = async e => { document.getElementById("registerform").onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const name = document.getElementById("name").value; const name = document.getElementById("name").value;
if (!name) return; if (!name) return;
@ -131,7 +155,7 @@ window.onload = () => {
window.name = name; window.name = name;
window.socket.emit("create_user", { name, pubkey: keys.publicKey }); window.socket.emit("create_user", { name, pubkey: keys.publicKey });
}; };
document.getElementById("msginput").onsubmit = e => { document.getElementById("msginput").onsubmit = (e) => {
e.preventDefault(); e.preventDefault();
const msg = document.getElementById("msg").value; const msg = document.getElementById("msg").value;
if (!msg) return; if (!msg) return;
@ -141,21 +165,54 @@ window.onload = () => {
}); });
document.getElementById("msg").value = ""; document.getElementById("msg").value = "";
}; };
document.getElementById("loadmore").onclick = e => { document.getElementById("loadmore").onclick = (e) => {
loadMessages(); loadMessages();
}; };
document.getElementById("createthread").onsubmit = e => { document.getElementById("createthread").onsubmit = (e) => {
e.preventDefault(); 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", { window.socket.emit("create_thread", {
name: document.getElementById("newthreadname").value, 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"); const keys = localStorage.getItem("keys");
if (keys) { if (keys) {
window.name = localStorage.getItem("name"); window.name = localStorage.getItem("name");
loadKeys(JSON.parse(keys)).then(() => {}); loadKeys(JSON.parse(keys)).then(() => {});
} } else document.getElementById("register").classList.remove("hidden");
else
document.getElementById("register").classList.remove("hidden");
}; };

View File

@ -14,7 +14,9 @@
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif; sans-serif;
} }
body, button, input { body,
button,
input {
background: #020202; background: #020202;
color: #eaeaea; color: #eaeaea;
} }
@ -23,9 +25,12 @@
align-items: stretch; align-items: stretch;
margin: 0; margin: 0;
min-width: min-content; min-width: min-content;
padding: 0 20px;
} }
.column { .column {
flex-grow: 1; flex: 1;
max-width: 50vw;
overflow: hidden;
} }
button { button {
border-color: #767676; border-color: #767676;
@ -38,6 +43,7 @@
} }
.message { .message {
margin-bottom: 5px; margin-bottom: 5px;
overflow-wrap: break-word;
} }
#loadmore { #loadmore {
margin-bottom: 10px; margin-bottom: 10px;
@ -45,6 +51,9 @@
.channel { .channel {
font-weight: normal; font-weight: normal;
} }
.member {
margin: 5px 0;
}
</style> </style>
</head> </head>
<body> <body>
@ -52,9 +61,9 @@
<h1>welcome to vybe</h1> <h1>welcome to vybe</h1>
<h3>a communication network (beta)</h3> <h3>a communication network (beta)</h3>
<p> <p>
to get started, you'll need an account. we use public key to get started, you'll need an account. we use public key cryptography
cryptography for security, rather than passwords. for now for security, rather than passwords. for now your keys are stored in
your keys are stored in your browser storage only. your browser storage only.
</p> </p>
<form id="registerform"> <form id="registerform">
<label for="name">name/username: </label> <label for="name">name/username: </label>
@ -69,7 +78,34 @@
<h3>create thread</h3> <h3>create thread</h3>
<form id="createthread"> <form id="createthread">
<label for="newthreadname">thread name</label> <label for="newthreadname">thread name</label>
<input type="text" id="newthreadname" /> <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> <button id="submitthread" type="submit">create</button>
</form> </form>
</div> </div>
@ -78,7 +114,7 @@
current thread: <strong id="threadname">meow</strong> current thread: <strong id="threadname">meow</strong>
</h3> </h3>
<h3>messages will appear below as they are sent</h3> <h3>messages will appear below as they are sent</h3>
<button id="loadmore">load more messages</button> <button id="loadmore" class="hidden">load more messages</button>
<div id="messages"></div> <div id="messages"></div>
<form id="msginput"> <form id="msginput">
<input type="text" placeholder="write a message..." id="msg" /> <input type="text" placeholder="write a message..." id="msg" />

View File

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

View File

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

View File

@ -14,10 +14,17 @@ const PORT = process.env.PORT || 3435;
const actions = require("./src/actions"); const actions = require("./src/actions");
io.cache = {};
io.on("connection", (socket) => { io.on("connection", (socket) => {
for (let action in actions) { for (let action in actions) {
socket.on(action, (msg) => 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 db = require("../db");
const openpgp = require("openpgp"); const openpgp = require("openpgp");
const authenticate = async (msg, respond, socket) => { const authenticate = async (msg, respond, socket, io) => {
if (!msg.name || !msg.message) { if (!msg.name || !msg.message) {
return respond({ return respond({
success: false, success: false,
@ -43,6 +43,11 @@ const authenticate = async (msg, respond, socket) => {
data[1], data[1],
]); ]);
socket.userid = result.rows[0].id; 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({ return respond({
success: true, success: true,
}); });

View File

@ -1,7 +1,7 @@
const db = require("../db"); const db = require("../db");
const authwrap = (fn) => async (msg, respond, socket, io) => { const authwrap = (fn) => async (msg, respond, socket, io) => {
if (!msg.__session) { if (!msg || !msg.__session) {
return respond({ return respond({
success: false, success: false,
message: "Not authenticated", 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", "insert into threads (name, creator) values (?, ?) returning id",
[msg.name, msg.auth_user.id] [msg.name, msg.auth_user.id]
); );
io.emit('new_thread', { 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, name: msg.name,
id: insert.rows[0].id 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 // respond
return respond({ return respond({
success: true, success: true,

View File

@ -1,4 +1,6 @@
const db = require("../db"); const db = require("../db");
const authwrap = require("./authwrap");
const check_permissions = require("./helpers/check_permissions");
const get_history = async (msg, respond) => { const get_history = async (msg, respond) => {
if (msg.before && isNaN(Number(msg.before))) { if (msg.before && isNaN(Number(msg.before))) {
@ -13,6 +15,12 @@ const get_history = async (msg, respond) => {
message: "thread ID required", 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( const messages = await db.query(
`select users.name, posts.id, content from posts `select users.name, posts.id, content from posts
join users on posts.user = users.id 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 db = require("../db");
const authwrap = require("./authwrap");
const check_permissions = require("./helpers/check_permissions");
const list_threads = async (msg, respond) => { const list_threads = async (msg, respond) => {
const threads = await db.query( 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 // respond
const rows = [];
for (let i of threads.rows) {
rows.push({
...i,
permissions: await check_permissions(msg.auth_user.id, i.id),
});
}
return respond({ return respond({
success: true, 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 db = require("../db");
const authwrap = require("./authwrap"); 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) { if (!msg.thread) {
return respond({ return respond({
success: false, success: false,
message: "thread ID required", 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 // add message and send it to everyone
const id = await db.query( const id = await db.query(
"insert into posts (user, thread, content) values (?, ?, ?) returning id", "insert into posts (user, thread, content) values (?, ?, ?) returning id",
[msg.auth_user.id, msg.thread, msg.message] [msg.auth_user.id, msg.thread, msg.message]
); );
socket.broadcast.emit("new_message", { // 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, id: id.rows[0].id,
name: msg.auth_user.name, name: msg.auth_user.name,
message: msg.message, message: msg.message,
thread: msg.thread, 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({ return respond({
success: true, success: true,
id: id.rows[0].id, id: id.rows[0].id,