space ui and updates
							parent
							
								
									f347e659e9
								
							
						
					
					
						commit
						26530690b9
					
				|  | @ -122,6 +122,11 @@ async function createThread(e) { | |||
| 			chooseThread({ | ||||
| 				name: name.value, | ||||
| 				id: msg.id, | ||||
| 				permissions: { | ||||
| 					is_member: true, | ||||
| 					view: true, | ||||
| 					post: true | ||||
| 				} | ||||
| 			}); | ||||
| 			// since the form exists, this will perform cleanup
 | ||||
| 			newThread(); | ||||
|  | @ -215,9 +220,9 @@ render(document.body, html` | |||
| 			</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button> | ||||
| 		</div> | ||||
| 		<div id='message' class='tabcontent'> | ||||
| 		<button id="loadmore" class="hidden" onclick=${loadMessages}> | ||||
| 		load more messages | ||||
| 		</button> | ||||
| 			<button id="loadmore" class="hidden" onclick=${loadMessages}> | ||||
| 			load more messages | ||||
| 			</button> | ||||
| 			<div id='messages'></div> | ||||
| 			<form id="msginput" onsubmit=${sendMessage}> | ||||
| 				<input type="text" placeholder="write a message..." id="msg" /> | ||||
|  | @ -228,7 +233,7 @@ render(document.body, html` | |||
| 	</div> | ||||
| `);
 | ||||
| 
 | ||||
| window.socket.on("new_message", (msg) => { | ||||
| window.socket.on("new_message", msg => { | ||||
| 	if (msg.thread !== window.currentThreadId) | ||||
| 		return; | ||||
| 	document.getElementById("messages").appendChild(html.node` | ||||
|  | @ -247,3 +252,5 @@ window.emit("list_threads", {}, msg => { | |||
| 		addThread(thread); | ||||
| 	chooseThread(msg.threads[0]); | ||||
| }); | ||||
| 
 | ||||
| import('/space.js'); | ||||
|  |  | |||
|  | @ -20,69 +20,64 @@ async function auth() { | |||
| 		"authenticate", | ||||
| 		{ name: window.name, message: sig }, | ||||
| 		msg => { | ||||
| 			let register = document.getElementById('register'); | ||||
| 			if (!msg.success) { | ||||
| 				console.log('authenticate failed', msg); | ||||
| 				register.classList.remove('hidden'); | ||||
| 				return; | ||||
| 			} | ||||
| 			if (document.getElementById("register")) { | ||||
| 				document.getElementById("register").remove(); | ||||
| 			if (register) { | ||||
| 				register.remove(); | ||||
| 				import('/app.js'); | ||||
| 			} | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| render( | ||||
| 	document.body, | ||||
| 	html` | ||||
| 		<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. your keys are stored in your | ||||
| 				browser storage only, so do this on a browser you can access again. | ||||
| 			</p> | ||||
| 			<form | ||||
| 				onsubmit=${async e => { | ||||
| 					e.preventDefault(); | ||||
| 					const name = document.getElementById("name").value; | ||||
| 					if (!name) | ||||
| render(document.body, html` | ||||
| 	<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. your keys are stored in your | ||||
| 			browser storage only, so do this on a browser you can access again. | ||||
| 		</p> | ||||
| 		<form 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.emit( | ||||
| 				"create_user", | ||||
| 				{ name, pubkey: keys.publicKey }, | ||||
| 				(msg) => { | ||||
| 					if (!msg.success) { | ||||
| 						document.querySelector('#registerform').insertAdjacentHTML( | ||||
| 							'afterend', | ||||
| 							`<p>${msg.message}</p>` | ||||
| 						); | ||||
| 						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.emit( | ||||
| 						"create_user", | ||||
| 						{ name, pubkey: keys.publicKey }, | ||||
| 						(msg) => { | ||||
| 							if (!msg.success) { | ||||
| 								document.querySelector('#registerform').insertAdjacentHTML( | ||||
| 									'afterend', | ||||
| 									` | ||||
| 						<p>${msg.message}</p>` | ||||
| 								); | ||||
| 								return; | ||||
| 							} | ||||
| 							window.keys = { priv, pub }; | ||||
| 							localStorage.setItem("keys", JSON.stringify(keys)); | ||||
| 							localStorage.setItem("name", name); | ||||
| 							window.name = name; | ||||
| 							auth(); | ||||
| 						} | ||||
| 					); | ||||
| 				}} | ||||
| 				id="registerform" | ||||
| 			> | ||||
| 				<label for="name">username: </label> | ||||
| 				<input id="name" type="text" /> | ||||
| 				<input id="submit" type="submit" value='generate keys & register' /> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	` | ||||
| ); | ||||
| 					} | ||||
| 					window.keys = { priv, pub }; | ||||
| 					localStorage.setItem("keys", JSON.stringify(keys)); | ||||
| 					localStorage.setItem("name", name); | ||||
| 					window.name = name; | ||||
| 					auth(); | ||||
| 				} | ||||
| 			); | ||||
| 		}} id="registerform"> | ||||
| 			<label for="name">username: </label> | ||||
| 			<input id="name" type="text" /> | ||||
| 			<button id="submit" type="submit">generate keys & register</button> | ||||
| 		</form> | ||||
| 	</div> | ||||
| `);
 | ||||
| 
 | ||||
| const gensession = async () => { | ||||
| 	window.session = rand(); | ||||
|  |  | |||
|  | @ -14,6 +14,16 @@ | |||
| 				font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | ||||
| 				Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | ||||
| 				sans-serif; | ||||
| 				scrollbar-color: #505050 #111; | ||||
| 			} | ||||
| 			::-webkit-scrollbar-thumb { | ||||
| 				background: #505050; | ||||
| 			} | ||||
| 			::-webkit-scrollbar { | ||||
| 				background-color: #111; | ||||
| 			} | ||||
| 			::-webkit-scrollbar-corner { | ||||
| 				background-color: #111; | ||||
| 			} | ||||
| 			body, | ||||
| 			button, | ||||
|  | @ -33,6 +43,9 @@ | |||
| 				margin: 0; | ||||
| 				min-width: min-content; | ||||
| 			} | ||||
| 			#register { | ||||
| 				margin-inline: 14px; | ||||
| 			} | ||||
| 			button, | ||||
| 			input, | ||||
| 			.tab { | ||||
|  | @ -42,9 +55,9 @@ | |||
| 				background: #303030; | ||||
| 			} | ||||
| 			input { | ||||
| 				background: #171717; | ||||
| 				border-bottom: 2px solid transparent; | ||||
| 				background: #1b1b1b; | ||||
| 				padding-bottom: 3px; | ||||
| 				border-bottom: 2px solid transparent; | ||||
| 			} | ||||
| 			input:focus { | ||||
| 				border-bottom: 2px solid #4f4f4f; | ||||
|  | @ -75,16 +88,14 @@ | |||
| 			} | ||||
| 			.column { | ||||
| 				flex: 1; | ||||
| 				margin: 5px; | ||||
| 				overflow: hidden; | ||||
| 				margin: 5px; | ||||
| 			} | ||||
| 			#threads { | ||||
| 				max-width: 250px; | ||||
| 			} | ||||
| 			.thread { | ||||
| 				display: block; | ||||
| 				width: 100%; | ||||
| 				text-align: left; | ||||
| 				padding: 2px; | ||||
| 			} | ||||
| 			#newthread { | ||||
| 				margin-top: 5px; | ||||
|  | @ -98,30 +109,31 @@ | |||
| 			#thread { | ||||
| 				display: flex; | ||||
| 				flex-direction: column; | ||||
| 				margin: 0; | ||||
| 			} | ||||
| 			#title { | ||||
| 				margin: 4px 2px; | ||||
| 				margin: 4px; | ||||
| 			} | ||||
| 			#tabs { | ||||
| 				margin-block: 2px; | ||||
| 				margin: 4px 2px; | ||||
| 			} | ||||
| 			.tab { | ||||
| 				padding: 5px 7px; | ||||
| 				background-color: #1f1f1f; | ||||
| 				border: 0; | ||||
| 				margin-top: 2px; | ||||
| 				color: #ddd; | ||||
| 				font-weight: 500; | ||||
| 			} | ||||
| 			.tabcontent { | ||||
| 				flex-grow: 1; | ||||
| 				margin: 2px; | ||||
| 			} | ||||
| 			#message { | ||||
| 				display: flex; | ||||
| 				flex-direction: column; | ||||
| 			} | ||||
| 			#messages { | ||||
| 				margin: 4px 2px; | ||||
| 				margin: 2px; | ||||
| 				flex-grow: 1; | ||||
| 			} | ||||
| 			#msginput { | ||||
|  | @ -149,6 +161,14 @@ | |||
| 			.member { | ||||
| 				margin: 5px 0; | ||||
| 			} | ||||
| 			#space { | ||||
| 				margin: 0; | ||||
| 				position: relative; | ||||
| 				overflow: auto; | ||||
| 			} | ||||
| 			.span { | ||||
| 				position: absolute; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body></body> | ||||
|  |  | |||
|  | @ -0,0 +1,93 @@ | |||
| let space = document.getElementById('space'); | ||||
| 
 | ||||
| let scale = 1; | ||||
| 
 | ||||
| let editing; | ||||
| let dragging; | ||||
| let moved; | ||||
| let offset; | ||||
| 
 | ||||
| function mousemove(event) { | ||||
| 	let left = (event.clientX - space.offsetLeft) * scale - offset.x; | ||||
| 	let top = (event.clientY - space.offsetTop) * scale - offset.y; | ||||
| 	dragging.style.left = `${left < 0 ? 0 : left}px`; | ||||
| 	dragging.style.top = `${top < 0 ? 0 : top}px`; | ||||
| 	moved = true; | ||||
| 	//save
 | ||||
| } | ||||
| 
 | ||||
| function remove(span) { | ||||
| 	//remove
 | ||||
| 	span.remove(); | ||||
| } | ||||
| 
 | ||||
| function add(x, y) { | ||||
| 	let span = document.createElement('span'); | ||||
| 	span.classList.add('span'); | ||||
| 	span.contentEditable = true; | ||||
| 	span.spellcheck = false; | ||||
| 	span.scale = 1; | ||||
| 	span.style.left = `${x}px`; | ||||
| 	span.style.top = `${y}px`; | ||||
| 	span.style.transform = 'translate(-50%, -50%)'; | ||||
| 	span.onkeydown = function(event) { | ||||
| 		if (event.key === 'Enter' && !event.getModifierState('Shift')) { | ||||
| 			event.preventDefault(); | ||||
| 			editing = null; | ||||
| 			span.blur(); | ||||
| 		} | ||||
| 	}; | ||||
| 	span.oninput = function(event) { | ||||
| 		this.time = Date.now(); | ||||
| 		//save
 | ||||
| 	}; | ||||
| 	span.onblur = function(event) { | ||||
| 		if (!this.innerText) | ||||
| 			remove(this); | ||||
| 	}; | ||||
| 	span.onwheel = function(event) { | ||||
| 		event.preventDefault(); | ||||
| 		this.scale *= 1 - event.deltaY * .001; | ||||
| 		this.style.transform = `translate(-50%, -50%) scale(${this.scale})`; | ||||
| 		//save
 | ||||
| 	}; | ||||
| 	span.onmousedown = function(event) { | ||||
| 		if (dragging || editing === this) | ||||
| 			return; | ||||
| 		dragging = this; | ||||
| 		event.preventDefault(); | ||||
| 		offset = { | ||||
| 			x: event.clientX - (space.offsetLeft + this.offsetLeft), | ||||
| 			y: event.clientY - (space.offsetTop + this.offsetTop) | ||||
| 		}; | ||||
| 		moved = false; | ||||
| 		document.addEventListener('mousemove', mousemove); | ||||
| 	}; | ||||
| 	span.onmouseup = function(event) { | ||||
| 		event.stopPropagation(); | ||||
| 		document.removeEventListener('mousemove', mousemove); | ||||
| 		dragging = null; | ||||
| 		if (moved) | ||||
| 			return; | ||||
| 		this.focus(); | ||||
| 		editing = this; | ||||
| 	}; | ||||
| 	space.append(span); | ||||
| 	return span; | ||||
| } | ||||
| 
 | ||||
| space.onmouseup = event => { | ||||
| 	if (dragging) { | ||||
| 		dragging.onmouseup(event); | ||||
| 		return; | ||||
| 	} | ||||
| 	if (editing) { | ||||
| 		if (!editing.innerText) | ||||
| 			remove(editing); | ||||
| 		if (event.target !== editing) | ||||
| 			editing = null; | ||||
| 		return; | ||||
| 	} | ||||
| 	editing = add(event.offsetX + space.scrollLeft, event.offsetY + space.scrollTop); | ||||
| 	editing.focus(); | ||||
| }; | ||||
							
								
								
									
										109
									
								
								db/1-init.sql
								
								
								
								
							
							
						
						
									
										109
									
								
								db/1-init.sql
								
								
								
								
							|  | @ -1,60 +1,73 @@ | |||
| CREATE TABLE users ( | ||||
|   id integer primary key asc, | ||||
|   name text, | ||||
|   pubkey text, | ||||
|   created timestamp default current_timestamp | ||||
| create table user ( | ||||
| 	id integer primary key asc, | ||||
| 	name text, | ||||
| 	pubkey text, | ||||
| 	created timestamp default current_timestamp | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE authentications ( | ||||
|   user integer, | ||||
|   salt text, | ||||
|   created timestamp default current_timestamp, | ||||
|   foreign key(user) references users(id) | ||||
| create table authentication ( | ||||
| 	user integer, | ||||
| 	salt text, | ||||
| 	created timestamp default current_timestamp, | ||||
| 	foreign key(user) references user(id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE threads ( | ||||
|   id integer primary key asc, | ||||
|   name text, | ||||
|   creator integer, | ||||
|   created timestamp default current_timestamp, | ||||
|   foreign key(creator) references users(id) | ||||
| create table thread ( | ||||
| 	id integer primary key asc, | ||||
| 	name text, | ||||
| 	creator integer, | ||||
| 	created timestamp default current_timestamp, | ||||
| 	foreign key(creator) references user(id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE permissions ( | ||||
|   thread integer, | ||||
|   user integer, | ||||
|   type text, | ||||
|   mutable boolean, | ||||
|   permission text, | ||||
|   value text, | ||||
|   foreign key(user) references users(id), | ||||
|   foreign key(thread) references threads(id) | ||||
| create table permission ( | ||||
| 	thread integer, | ||||
| 	user integer, | ||||
| 	type text, | ||||
| 	mutable boolean, | ||||
| 	permission text, | ||||
| 	value text, | ||||
| 	foreign key(user) references user(id), | ||||
| 	foreign key(thread) references thread(id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE members ( | ||||
|   thread integer, | ||||
|   user integer, | ||||
|   key_delivery text, | ||||
|   created timestamp default current_timestamp, | ||||
|   foreign key(user) references users(id), | ||||
|   foreign key(thread) references threads(id) | ||||
| create table member ( | ||||
| 	thread integer, | ||||
| 	user integer, | ||||
| 	key_delivery text, | ||||
| 	created timestamp default current_timestamp, | ||||
| 	foreign key(user) references user(id), | ||||
| 	foreign key(thread) references thread(id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE posts ( | ||||
|   id integer primary key asc, | ||||
|   user integer, | ||||
|   thread integer, | ||||
|   content text, | ||||
|   encrypted bool, | ||||
|   created timestamp default current_timestamp, | ||||
|   foreign key(user) references users(id), | ||||
|   foreign key(thread) references threads(id) | ||||
| create table post ( | ||||
| 	id integer primary key asc, | ||||
| 	user integer, | ||||
| 	thread integer, | ||||
| 	content text, | ||||
| 	encrypted bool, | ||||
| 	created timestamp default current_timestamp, | ||||
| 	foreign key(user) references user(id), | ||||
| 	foreign key(thread) references thread(id) | ||||
| ); | ||||
| 
 | ||||
| INSERT INTO threads (name) values ("meow"); | ||||
| INSERT INTO permissions  | ||||
|   (thread, type, permission, value) values  | ||||
|   (1, "everyone", "view", "true"); | ||||
| INSERT INTO permissions  | ||||
|   (thread, type, permission, value) values  | ||||
|   (1, "everyone", "post", "true"); | ||||
| create table span ( | ||||
| 	id integer primary key asc, | ||||
| 	thread integer, | ||||
| 	creator integer, | ||||
| 	created timestamp default current_timestamp, | ||||
| 	content text, | ||||
| 	x integer, | ||||
| 	y integer, | ||||
| 	scale decimal, | ||||
| 	foreign key(creator) references user(id), | ||||
| 	foreign key(thread) references thread(id) | ||||
| ); | ||||
| 
 | ||||
| insert into thread (name) values ("meow"); | ||||
| insert into permission  | ||||
| 	(thread, type, permission, value) values | ||||
| 	(1, "everyone", "view", "true"); | ||||
| insert into permission  | ||||
| 	(thread, type, permission, value) values | ||||
| 	(1, "everyone", "post", "true"); | ||||
|  | @ -2,68 +2,68 @@ const db = require("../db"); | |||
| const openpgp = require("openpgp"); | ||||
| 
 | ||||
| const authenticate = async (msg, respond, socket, io) => { | ||||
|   if (!msg.name || !msg.message) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "Invalid message", | ||||
|     }); | ||||
|   } | ||||
|   const result = await db.query("select * from users where name = ?", [ | ||||
|     msg.name, | ||||
|   ]); | ||||
|   if (result.rows.length === 0) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "User not found", | ||||
|     }); | ||||
|   } | ||||
|   try { | ||||
|     const key = await openpgp.readKey({ armoredKey: result.rows[0].pubkey }); | ||||
|     const verification = await openpgp.verify({ | ||||
|       message: await openpgp.readCleartextMessage({ | ||||
|         cleartextMessage: msg.message, | ||||
|       }), | ||||
|       verificationKeys: key, | ||||
|       expectSigned: true, | ||||
|     }); | ||||
|     const data = verification.data.split(" "); | ||||
|     if (data[0] !== "vybe_auth") { | ||||
|       return respond({ | ||||
|         success: false, | ||||
|         message: "Bad auth message", | ||||
|       }); | ||||
|     } | ||||
|     const auths = await db.query( | ||||
|       "select * from authentications where user = ? and salt = ?", | ||||
|       [result.rows[0].id, data[1]] | ||||
|     ); | ||||
|     if (auths.rows.length === 0) { | ||||
|       await db.query("insert into authentications (user, salt) values (?, ?)", [ | ||||
|         result.rows[0].id, | ||||
|         data[1], | ||||
|       ]); | ||||
|       socket.username = msg.name; | ||||
|       if (io.cache[msg.name]) { | ||||
|         io.cache[msg.name].push(socket.id); | ||||
|       } else { | ||||
|         io.cache[msg.name] = [socket.id]; | ||||
|       } | ||||
|       return respond({ | ||||
|         success: true, | ||||
|       }); | ||||
|     } else { | ||||
|       return respond({ | ||||
|         success: false, | ||||
|         message: "Already authenticated with this message", | ||||
|       }); | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.error("error in authentication: " + err); | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "Message signature verification failed", | ||||
|     }); | ||||
|   } | ||||
| 	if (!msg.name || !msg.message) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "Invalid message", | ||||
| 		}); | ||||
| 	} | ||||
| 	const result = await db.query("select * from user where name = ?", [ | ||||
| 		msg.name, | ||||
| 	]); | ||||
| 	if (result.rows.length === 0) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "User not found", | ||||
| 		}); | ||||
| 	} | ||||
| 	try { | ||||
| 		const key = await openpgp.readKey({ armoredKey: result.rows[0].pubkey }); | ||||
| 		const verification = await openpgp.verify({ | ||||
| 			message: await openpgp.readCleartextMessage({ | ||||
| 				cleartextMessage: msg.message, | ||||
| 			}), | ||||
| 			verificationKeys: key, | ||||
| 			expectSigned: true, | ||||
| 		}); | ||||
| 		const data = verification.data.split(" "); | ||||
| 		if (data[0] !== "vybe_auth") { | ||||
| 			return respond({ | ||||
| 				success: false, | ||||
| 				message: "Bad auth message", | ||||
| 			}); | ||||
| 		} | ||||
| 		const auths = await db.query( | ||||
| 			"select * from authentication where user = ? and salt = ?", | ||||
| 			[result.rows[0].id, data[1]] | ||||
| 		); | ||||
| 		if (auths.rows.length === 0) { | ||||
| 			await db.query("insert into authentication (user, salt) values (?, ?)", [ | ||||
| 				result.rows[0].id, | ||||
| 				data[1], | ||||
| 			]); | ||||
| 			socket.username = msg.name; | ||||
| 			if (io.cache[msg.name]) { | ||||
| 				io.cache[msg.name].push(socket.id); | ||||
| 			} else { | ||||
| 				io.cache[msg.name] = [socket.id]; | ||||
| 			} | ||||
| 			return respond({ | ||||
| 				success: true, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return respond({ | ||||
| 				success: false, | ||||
| 				message: "Already authenticated with this message", | ||||
| 			}); | ||||
| 		} | ||||
| 	} catch (err) { | ||||
| 		console.error("error in authentication: " + err); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "Message signature verification failed", | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| module.exports = authenticate; | ||||
|  |  | |||
|  | @ -1,27 +1,27 @@ | |||
| const db = require("../db"); | ||||
| 
 | ||||
| const authwrap = (fn) => async (msg, respond, socket, io) => { | ||||
|   if (!respond) | ||||
|     respond = () => {}; | ||||
|   if (!msg || !msg.__session) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "Not authenticated", | ||||
|     }); | ||||
|   } | ||||
|   const result = await db.query( | ||||
|     `select users.* from users join authentications
 | ||||
|     on authentications.user = users.id | ||||
|     where authentications.salt = ?`,
 | ||||
|     [msg.__session] | ||||
|   ); | ||||
|   if (result.rows.length === 0) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "User not found", | ||||
|     }); | ||||
|   } | ||||
|   return await fn({ ...msg, auth_user: result.rows[0] }, respond, socket, io); | ||||
| 	if (!respond) | ||||
| 		respond = () => {}; | ||||
| 	if (!msg || !msg.__session) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "Not authenticated", | ||||
| 		}); | ||||
| 	} | ||||
| 	const result = await db.query( | ||||
| 		`select user.* from user join authentication
 | ||||
| 		on authentication.user = user.id | ||||
| 		where authentication.salt = ?`,
 | ||||
| 		[msg.__session] | ||||
| 	); | ||||
| 	if (result.rows.length === 0) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "User not found", | ||||
| 		}); | ||||
| 	} | ||||
| 	return await fn({ ...msg, auth_user: result.rows[0] }, respond, socket, io); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap; | ||||
|  |  | |||
|  | @ -17,38 +17,38 @@ const create_thread = async (msg, respond, socket, io) => { | |||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		"insert into threads (name, creator) values (?, ?) returning id", | ||||
| 		"insert into thread (name, creator) values (?, ?) returning id", | ||||
| 		[msg.name, msg.auth_user.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, mutable, permission, value) 
 | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "everyone", false, "view", "true"] | ||||
| 		); | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			await db.query( | ||||
| 				`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, "everyone", false, "post", "true"] | ||||
| 			); | ||||
| 		} else { | ||||
| 			await db.query( | ||||
| 				`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, "members", false, "post", "true"] | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		await db.query( | ||||
| 			`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "members", false, "view", "true"] | ||||
| 		); | ||||
| 		await db.query( | ||||
| 			`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "members", false, "post", "true"] | ||||
| 		); | ||||
|  | @ -57,13 +57,13 @@ const create_thread = async (msg, respond, socket, io) => { | |||
| 	for (let user of msg.members) { | ||||
| 		if (!user) continue; | ||||
| 		// get user id
 | ||||
| 		const id = await db.query("select id from users where name = ?", [ | ||||
| 		const id = await db.query("select id from user where name = ?", [ | ||||
| 			user.name, | ||||
| 		]); | ||||
| 		if (id.rows.length > 0) { | ||||
| 			const user_id = id.rows[0].id; | ||||
| 			await db.query( | ||||
| 				"insert into members (thread, user, key_delivery) values (?, ?, ?)", | ||||
| 				"insert into member (thread, user, key_delivery) values (?, ?, ?)", | ||||
| 				[thread_id, user_id, user.key] | ||||
| 			); | ||||
| 		} | ||||
|  |  | |||
|  | @ -2,50 +2,50 @@ const db = require("../db"); | |||
| const openpgp = require("openpgp"); | ||||
| 
 | ||||
| const create_user = async (msg, respond) => { | ||||
|   // validate inputs
 | ||||
|   if (!msg.name) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "username required", | ||||
|     }); | ||||
|   } | ||||
|   // ensure username is not taken
 | ||||
|   const result = await db.query("select * from users where name = ?", [ | ||||
|     msg.name, | ||||
|   ]); | ||||
|   if (result.rows.length > 0) { | ||||
|     console.log(`username already exists: ${result}`); | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "a user with this name already exists on this server", | ||||
|     }); | ||||
|   } | ||||
|   // validate public key
 | ||||
|   if (!msg.pubkey) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "public key required", | ||||
|     }); | ||||
|   } | ||||
|   try { | ||||
|     await openpgp.readKey({ armoredKey: msg.pubkey }); | ||||
|   } catch (err) { | ||||
|     console.err("error in create_user readkey: " + err); | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "public key invalid", | ||||
|     }); | ||||
|   } | ||||
|   // add to db
 | ||||
|   const insert = await db.query( | ||||
|     "insert into users (name, pubkey) values (?, ?) returning id", | ||||
|     [msg.name, msg.pubkey] | ||||
|   ); | ||||
|   // respond
 | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     id: insert.rows[0].id, | ||||
|   }); | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.name) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "username required", | ||||
| 		}); | ||||
| 	} | ||||
| 	// ensure username is not taken
 | ||||
| 	const result = await db.query("select * from user where name = ?", [ | ||||
| 		msg.name, | ||||
| 	]); | ||||
| 	if (result.rows.length > 0) { | ||||
| 		console.log(`username already exists: ${result}`); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "a user with this name already exists on this server", | ||||
| 		}); | ||||
| 	} | ||||
| 	// validate public key
 | ||||
| 	if (!msg.pubkey) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "public key required", | ||||
| 		}); | ||||
| 	} | ||||
| 	try { | ||||
| 		await openpgp.readKey({ armoredKey: msg.pubkey }); | ||||
| 	} catch (err) { | ||||
| 		console.err("error in create_user readkey: " + err); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "public key invalid", | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		"insert into user (name, pubkey) values (?, ?) returning id", | ||||
| 		[msg.name, msg.pubkey] | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: insert.rows[0].id, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = create_user; | ||||
|  |  | |||
|  | @ -1,42 +1,42 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| const check_permission = require("./helpers/check_permission"); | ||||
| 
 | ||||
| const get_history = async (msg, respond) => { | ||||
|   if (msg.before && isNaN(Number(msg.before))) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "before must be a number", | ||||
|     }); | ||||
|   } | ||||
|   if (!msg.thread) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       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 | ||||
|     ${msg.before ? "where posts.id < ? and" : "where"} | ||||
|     thread = ? | ||||
|     order by posts.created desc | ||||
|     limit 101`,
 | ||||
|     msg.before ? [msg.before, msg.thread] : [msg.thread] | ||||
|   ); | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     messages: messages.rows | ||||
|       .slice(0, 100) | ||||
|       .map((i) => ({ id: i.id, name: i.name, message: i.content })), | ||||
|     more: messages.rows.length > 100, | ||||
|   }); | ||||
| 	if (msg.before && isNaN(Number(msg.before))) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "before must be a number", | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "thread ID required", | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread", | ||||
| 		}); | ||||
| 	} | ||||
| 	const messages = await db.query( | ||||
| 		`select user.name, post.id, content from post
 | ||||
| 		join user on post.user = user.id | ||||
| 		${msg.before ? "where post.id < ? and" : "where"} | ||||
| 		thread = ? | ||||
| 		order by post.created desc | ||||
| 		limit 101`,
 | ||||
| 		msg.before ? [msg.before, msg.thread] : [msg.thread] | ||||
| 	); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		messages: messages.rows | ||||
| 			.slice(0, 100) | ||||
| 			.map((i) => ({ id: i.id, name: i.name, message: i.content })), | ||||
| 		more: messages.rows.length > 100, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_history); | ||||
|  |  | |||
|  | @ -2,30 +2,30 @@ const db = require("../db"); | |||
| const authwrap = require("./authwrap"); | ||||
| 
 | ||||
| const get_keys = async (msg, respond, socket, io) => { | ||||
|   // validate inputs
 | ||||
|   if (!msg.names) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "user names required", | ||||
|     }); | ||||
|   } | ||||
|   if (typeof msg.names !== "object") { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "can't iterate user names", | ||||
|     }); | ||||
|   } | ||||
|   const keys = await db.query( | ||||
|     `select name, pubkey from users where name in (${msg.names | ||||
|       .map((i) => "?") | ||||
|       .join(",")})`,
 | ||||
|     msg.names | ||||
|   ); | ||||
|   // respond
 | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     keys: keys.rows, | ||||
|   }); | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.names) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "user names required", | ||||
| 		}); | ||||
| 	} | ||||
| 	if (typeof msg.names !== "object") { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "can't iterate user names", | ||||
| 		}); | ||||
| 	} | ||||
| 	const keys = await db.query( | ||||
| 		`select name, pubkey from user where name in (${msg.names | ||||
| 			.map((i) => "?") | ||||
| 			.join(",")})`,
 | ||||
| 		msg.names | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		keys: keys.rows, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_keys); | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| const db = require("../../db"); | ||||
| 
 | ||||
| const check_permission = async (user_id, thread_id) => { | ||||
| 	// get all the permissions for the thread
 | ||||
| 	const permissions = await db.query( | ||||
| 		"select * from permission where thread = ?", | ||||
| 		[thread_id] | ||||
| 	); | ||||
| 	// check if the user is a member
 | ||||
| 	const is_member = | ||||
| 		( | ||||
| 			await db.query("select * from member 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_permission; | ||||
|  | @ -1,37 +0,0 @@ | |||
| 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; | ||||
|  | @ -1,32 +1,32 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| const check_permission = require("./helpers/check_permission"); | ||||
| 
 | ||||
| const list_threads = async (msg, respond) => { | ||||
|   const threads = await db.query( | ||||
|     `select name, id, members.key_delivery as key from threads
 | ||||
|     join permissions on threads.id = permissions.thread | ||||
|     left 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 threads.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: rows, | ||||
|   }); | ||||
| 	const threads = await db.query( | ||||
| 		`select name, id, member.key_delivery as key from thread
 | ||||
| 		join permission on thread.id = permission.thread | ||||
| 		left join member on thread.id = member.thread | ||||
| 		where permission.permission = 'view' | ||||
| 		and permission.value = 'true' | ||||
| 		and ((permission.type = 'everyone') or | ||||
| 		permission.type = 'members' and member.user = ?) | ||||
| 		group by thread.id | ||||
| 		order by thread.created desc`,
 | ||||
| 		[msg.auth_user.id] | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	const rows = []; | ||||
| 	for (let thread of threads.rows) { | ||||
| 		rows.push({ | ||||
| 			...thread, | ||||
| 			permissions: await check_permission(msg.auth_user.id, thread.id), | ||||
| 		}); | ||||
| 	} | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		threads: rows, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(list_threads); | ||||
|  |  | |||
|  | @ -1,64 +1,64 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| const check_permission = require("./helpers/check_permission"); | ||||
| 
 | ||||
| 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, encrypted) values (?, ?, ?, ?) returning id", | ||||
|     [msg.auth_user.id, msg.thread, msg.message, msg.encrypted] | ||||
|   ); | ||||
|   // 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, | ||||
|   }); | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "thread ID required", | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(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 post (user, thread, content, encrypted) values (?, ?, ?, ?) returning id", | ||||
| 		[msg.auth_user.id, msg.thread, msg.message, msg.encrypted] | ||||
| 	); | ||||
| 	// get thread members
 | ||||
| 	const members = ( | ||||
| 		await db.query( | ||||
| 			"select name from user join member on member.user = user.id where member.thread = ?", | ||||
| 			[msg.thread] | ||||
| 		) | ||||
| 	).rows.map((i) => i.name); | ||||
| 	// get perms
 | ||||
| 	const permissions = await db.query( | ||||
| 		"select * from permission 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, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(send_message); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue