Compare commits
14 Commits
57c2241e12
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea65a665d | |||
| 836beda422 | |||
| 99a3b57cf6 | |||
| 6cf8a02100 | |||
| 44530ff327 | |||
| 1c9d0a6207 | |||
| 503ba812f2 | |||
| a7bae463a2 | |||
| 219ec4df9a | |||
| d83bd0a7bf | |||
| 8f67333050 | |||
| b4aa323577 | |||
| a679f49b18 | |||
| d55714a6f0 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
*.swp
|
||||
api/dist/
|
||||
api/node_modules/
|
||||
|
||||
1344
Cargo.lock
generated
1344
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,5 @@
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"admin-cli",
|
||||
"api"
|
||||
"admin-cli"
|
||||
]
|
||||
|
||||
@@ -7,5 +7,7 @@ edition = "2021"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
postgres = "0.19.9"
|
||||
base64 = "0.22.1"
|
||||
serde = "1.0.215"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
argon2 = "0.5.3"
|
||||
rand_core = { version = "0.9.3", features = [ "os_rng" ] }
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use argon2::password_hash::Salt;
|
||||
use clap::Parser;
|
||||
use postgres::{Client, NoTls};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use base64::engine::general_purpose::STANDARD_NO_PAD;
|
||||
use serde::Serialize;
|
||||
use argon2::{Argon2, PasswordHasher};
|
||||
|
||||
|
||||
const PASSWORD_LENGTH: usize = 64;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
@@ -12,48 +18,98 @@ struct Args {
|
||||
/// Setup everything from scratch
|
||||
#[arg(short, long)]
|
||||
setup: bool,
|
||||
/// Specify the directory where all the sql files are located
|
||||
#[arg(short, long)]
|
||||
psql_dir: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Admin {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Config {
|
||||
postgres_user: String,
|
||||
postgres_pass: String,
|
||||
postgres: Admin,
|
||||
bubble: Admin
|
||||
}
|
||||
|
||||
|
||||
fn random_string(size: usize) -> String {
|
||||
// Next we'll generate a bunch of random numbers
|
||||
// Generates a URL safe string of random text of a given size
|
||||
// Internally uses /dev/urandom to generate that string
|
||||
let mut buffer = vec![0; size];
|
||||
let mut f = std::fs::File::open("/dev/urandom").unwrap();
|
||||
f.read_exact(&mut buffer).unwrap();
|
||||
URL_SAFE_NO_PAD.encode(buffer)
|
||||
}
|
||||
|
||||
fn full_setup() -> Result<Config, postgres::Error> {
|
||||
fn random_b64_std(size: usize) -> String {
|
||||
let mut buffer = vec![0; size];
|
||||
let mut f = std::fs::File::open("/dev/urandom").unwrap();
|
||||
f.read_exact(&mut buffer).unwrap();
|
||||
STANDARD_NO_PAD.encode(buffer)
|
||||
}
|
||||
|
||||
|
||||
fn salt_and_hash(password: &str) -> String {
|
||||
// Generates a salted and hashed variation of the given password
|
||||
let salt_str = random_b64_std(8);
|
||||
let salt = Salt::from_b64(&salt_str).unwrap();
|
||||
let a2 = Argon2::default();
|
||||
let hash = a2.hash_password(password.as_bytes(), salt).unwrap();
|
||||
hash.to_string()
|
||||
}
|
||||
|
||||
fn admin(username: &str, password_size: usize) -> Admin {
|
||||
Admin {
|
||||
username: username.to_string(),
|
||||
password: random_string(password_size)
|
||||
}
|
||||
}
|
||||
|
||||
fn full_setup(args: Args) -> Result<Config, postgres::Error> {
|
||||
// Check to make sure we have the DB url set to connect
|
||||
const KEY: &'static str = "DB_CONNECTION_STRING" ;
|
||||
let connection_string = env::var(KEY).expect(
|
||||
&format!("The env var {} is not set!", KEY)
|
||||
);
|
||||
let setup_tables_script = fs::read_to_string("db/setup-tables.sql")
|
||||
let db_folder = match args.psql_dir {
|
||||
Some(val) => format!("{}/setup-tables.sql", val),
|
||||
None => "db/setup-tables.sql".to_string()
|
||||
};
|
||||
let setup_tables_script = fs::read_to_string(db_folder)
|
||||
.expect("Failed to load file: db/setup-tables.sql");
|
||||
let bubble_admin_password = random_string(32);
|
||||
let postgres_admin = admin(&format!("bubble_admin-{}", random_string(8)), PASSWORD_LENGTH);
|
||||
let bubble_admin = admin(
|
||||
&format!("admin-{}", random_string(8)),
|
||||
PASSWORD_LENGTH
|
||||
);
|
||||
|
||||
let mut client = Client::connect(&connection_string, NoTls)?;
|
||||
// Preliminary bs
|
||||
client.execute("DROP DATABASE IF EXISTS bubble;", &[])?;
|
||||
client.execute("CREATE DATABASE bubble;", &[])?;
|
||||
client.execute("DROP USER IF EXISTS bubble_admin;", &[])?;
|
||||
client.execute(
|
||||
&format!("CREATE USER bubble_admin WITH ENCRYPTED PASSWORD '{}';", bubble_admin_password),
|
||||
&[]
|
||||
)?;
|
||||
let stmt = format!("CREATE ROLE bubble_admin WITH ENCRYPTED PASSWORD '{}'", postgres_admin.password);
|
||||
client.execute(&stmt , &[])?;
|
||||
|
||||
// Ensure the admin has ownership of the db we created
|
||||
client.execute("ALTER DATABASE bubble OWNER TO bubble_admin", &[])?;
|
||||
|
||||
// Service table creation
|
||||
client.batch_execute(&setup_tables_script)?;
|
||||
|
||||
// Populate the user table with the first user ( owner )
|
||||
let salted = salt_and_hash(&bubble_admin.password);
|
||||
client.execute(
|
||||
"INSERT INTO users (id, username, password) VALUES (gen_random_uuid(), $1, $2)",
|
||||
&[&bubble_admin.username, &salted]
|
||||
)?;
|
||||
Ok(Config {
|
||||
postgres_user: "bubble_admin".into(),
|
||||
postgres_pass: bubble_admin_password
|
||||
postgres: postgres_admin,
|
||||
bubble: bubble_admin
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +117,7 @@ fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.setup {
|
||||
match full_setup() {
|
||||
match full_setup(args) {
|
||||
Ok(config) => println!("{}", serde_json::to_string(&config).unwrap()),
|
||||
Err(e) => eprintln!("{:#?}", e)
|
||||
}
|
||||
|
||||
1602
api/Cargo.lock
generated
1602
api/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
serde = "1.0.213"
|
||||
rocket = { version = "0.5.1", features = [ "json" ] }
|
||||
3729
api/package-lock.json
generated
Normal file
3729
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
api/package.json
Normal file
30
api/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.10",
|
||||
"express": "^4.21.2",
|
||||
"node": "^23.9.0",
|
||||
"npm": "^11.2.0",
|
||||
"pg-promise": "^11.13.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"description": "HTTP API component for bubble",
|
||||
"main": "dist/main.js",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"debug": "node dist/main.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.shockrah.xyz:2222/shockrah/bubble.git"
|
||||
},
|
||||
"author": "shockrah",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum Type {
|
||||
Voice,
|
||||
Text,
|
||||
Video
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Channel {
|
||||
id: i64,
|
||||
name: String,
|
||||
desc: Option<String>,
|
||||
r#type: Type
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
pub async fn list() -> Json<Vec<Channel>> {
|
||||
// TODO
|
||||
Json(vec![])
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
pub async fn create() -> Json<Option<Channel>> {
|
||||
// TODO
|
||||
Json(None)
|
||||
}
|
||||
|
||||
#[delete("/delete")]
|
||||
pub async fn delete() -> Json<Option<i64>> {
|
||||
// TODO
|
||||
Json(None)
|
||||
}
|
||||
|
||||
#[post("/update")]
|
||||
pub async fn update() -> Json<Option<Channel>> {
|
||||
// TODO
|
||||
Json(None)
|
||||
}
|
||||
|
||||
30
api/src/channels.ts
Normal file
30
api/src/channels.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Request, Response } from 'express'
|
||||
import {values_present} from './common'
|
||||
|
||||
export enum Type {
|
||||
Text = 'text',
|
||||
Voice = 'voice',
|
||||
}
|
||||
|
||||
function valid_channel(ch: string) : boolean {
|
||||
return ch === Type.Text || ch === Type.Voice
|
||||
}
|
||||
|
||||
export async function list(req: Request, res: Response, db) : Promise<void> {
|
||||
const channels = await db.one('SELECT * from channels')
|
||||
res.send({path: req.path, data: channels.value})
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response) : Promise<void> {
|
||||
const { name, description, type } = req.query
|
||||
if (values_present([name, description, type])|| !valid_channel(type.toString()) ) {
|
||||
res.status(400).send({ error: 'Invalid query string params' })
|
||||
return
|
||||
}
|
||||
res.send({
|
||||
id: 123,
|
||||
name: name,
|
||||
description: description,
|
||||
type: type
|
||||
})
|
||||
}
|
||||
6
api/src/common.ts
Normal file
6
api/src/common.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function values_present(values: any[]) : boolean {
|
||||
/**
|
||||
* Function determines if there are any undefined or null values present
|
||||
*/
|
||||
return values.filter((val) => val === undefined || val === null).length > 0
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct User {
|
||||
id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/<id>")]
|
||||
pub async fn invite_user(id: i64) -> Json<Option<User>> {
|
||||
// First we check to see if the id is present in the invites table
|
||||
Json(None)
|
||||
}
|
||||
5
api/src/invites.ts
Normal file
5
api/src/invites.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export async function code(req: Request, res: Response) : Promise<void> {
|
||||
res.send({path: req.path})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#[macro_use] extern crate rocket;
|
||||
mod invites;
|
||||
mod channels;
|
||||
|
||||
#[rocket::main]
|
||||
async fn main() -> Result<(), rocket::Error> {
|
||||
let _ = rocket::build()
|
||||
.mount("/channels", routes![
|
||||
channels::list,
|
||||
channels::create,
|
||||
channels::delete
|
||||
])
|
||||
.mount("/invite", routes![invites::invite_user])
|
||||
.ignite().await?
|
||||
.launch().await?;
|
||||
Ok(())
|
||||
}
|
||||
22
api/src/main.ts
Normal file
22
api/src/main.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import process from 'process'
|
||||
import express from 'express'
|
||||
import * as channels from './channels'
|
||||
import * as invites from './invites'
|
||||
const pgPromise = require('pg-promise')()
|
||||
|
||||
|
||||
const DB_URL = process.env['DB_CONNECTION_STRING']
|
||||
const PORT = process.env['PORT'] || 8000
|
||||
|
||||
console.log('db-url', DB_URL)
|
||||
console.log('port', PORT)
|
||||
|
||||
const app = express()
|
||||
const db = pgPromise(DB_URL)
|
||||
|
||||
|
||||
app.get('/invite/<code>', async (req, res) => await invites.code(req, res))
|
||||
app.get('/channels/list', async (req, res) => await channels.list(req, res, db))
|
||||
app.post('/channels/create', async (req, res) => await channels.create(req, res))
|
||||
|
||||
app.listen(PORT)
|
||||
20
api/tsconfig.json
Normal file
20
api/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./dist/",
|
||||
"importHelpers": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER,
|
||||
name VARCHAR(256),
|
||||
/* */
|
||||
id UUID,
|
||||
/* Acts as a kind of nick name per instance as it assumes no uniqueness */
|
||||
username VARCHAR(256),
|
||||
/* Salt to be generated everytime password is (re)created */
|
||||
salt VARCHAR(256),
|
||||
/* Basic hashed password */
|
||||
password VARCHAR(256),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
@@ -10,3 +16,10 @@ CREATE TABLE IF NOT EXISTS invites (
|
||||
expires INTEGER,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id BIGINT,
|
||||
name varchar(256),
|
||||
description varchar(256),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
49
dev.py
Executable file
49
dev.py
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from subprocess import run
|
||||
from argparse import ArgumentParser
|
||||
import os
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--init-db',
|
||||
help='Run the admin cli to setup the db backend',
|
||||
action='store_true'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--db-url',
|
||||
help='Sets the database URL to use for connecting to postgres',
|
||||
default='postgres://psql:example@localhost:5432'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c',
|
||||
'--check-container',
|
||||
help='Execs into the given container with bash for debugging'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s',
|
||||
'--server',
|
||||
help='Run a debug server (assumes db is ready)',
|
||||
action='store_true'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
os.environ['DB_CONNECTION_STRING'] = args.db_url
|
||||
if args.init_db:
|
||||
run(
|
||||
'cargo run --bin admin-cli -- --setup',
|
||||
env=os.environ,
|
||||
shell=True
|
||||
)
|
||||
if args.check_container:
|
||||
run(f'docker exec -it {args.check_container} bash'.split())
|
||||
if args.server:
|
||||
run('npm run build', env=os.environ, cwd='api', shell=True)
|
||||
run(
|
||||
f'npm run debug',
|
||||
env=os.environ,
|
||||
cwd='api',
|
||||
shell=True
|
||||
)
|
||||
Reference in New Issue
Block a user