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
|
/target
|
||||||
*.swp
|
*.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"
|
resolver = "2"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"admin-cli",
|
"admin-cli"
|
||||||
"api"
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,5 +7,7 @@ edition = "2021"
|
|||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
postgres = "0.19.9"
|
postgres = "0.19.9"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
serde = "1.0.215"
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.133"
|
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::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use argon2::password_hash::Salt;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use postgres::{Client, NoTls};
|
use postgres::{Client, NoTls};
|
||||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||||
|
use base64::engine::general_purpose::STANDARD_NO_PAD;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use argon2::{Argon2, PasswordHasher};
|
||||||
|
|
||||||
|
|
||||||
|
const PASSWORD_LENGTH: usize = 64;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
@@ -12,48 +18,98 @@ struct Args {
|
|||||||
/// Setup everything from scratch
|
/// Setup everything from scratch
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
setup: bool,
|
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)]
|
#[derive(Serialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
postgres_user: String,
|
postgres: Admin,
|
||||||
postgres_pass: String,
|
bubble: Admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn random_string(size: usize) -> String {
|
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 buffer = vec![0; size];
|
||||||
let mut f = std::fs::File::open("/dev/urandom").unwrap();
|
let mut f = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
f.read_exact(&mut buffer).unwrap();
|
f.read_exact(&mut buffer).unwrap();
|
||||||
URL_SAFE_NO_PAD.encode(buffer)
|
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
|
// Check to make sure we have the DB url set to connect
|
||||||
const KEY: &'static str = "DB_CONNECTION_STRING" ;
|
const KEY: &'static str = "DB_CONNECTION_STRING" ;
|
||||||
let connection_string = env::var(KEY).expect(
|
let connection_string = env::var(KEY).expect(
|
||||||
&format!("The env var {} is not set!", KEY)
|
&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");
|
.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)?;
|
let mut client = Client::connect(&connection_string, NoTls)?;
|
||||||
// Preliminary bs
|
// Preliminary bs
|
||||||
client.execute("DROP DATABASE IF EXISTS bubble;", &[])?;
|
client.execute("DROP DATABASE IF EXISTS bubble;", &[])?;
|
||||||
client.execute("CREATE DATABASE bubble;", &[])?;
|
client.execute("CREATE DATABASE bubble;", &[])?;
|
||||||
client.execute("DROP USER IF EXISTS bubble_admin;", &[])?;
|
client.execute("DROP USER IF EXISTS bubble_admin;", &[])?;
|
||||||
client.execute(
|
let stmt = format!("CREATE ROLE bubble_admin WITH ENCRYPTED PASSWORD '{}'", postgres_admin.password);
|
||||||
&format!("CREATE USER bubble_admin WITH ENCRYPTED PASSWORD '{}';", bubble_admin_password),
|
client.execute(&stmt , &[])?;
|
||||||
&[]
|
|
||||||
)?;
|
|
||||||
// Ensure the admin has ownership of the db we created
|
// Ensure the admin has ownership of the db we created
|
||||||
client.execute("ALTER DATABASE bubble OWNER TO bubble_admin", &[])?;
|
client.execute("ALTER DATABASE bubble OWNER TO bubble_admin", &[])?;
|
||||||
|
|
||||||
// Service table creation
|
// Service table creation
|
||||||
client.batch_execute(&setup_tables_script)?;
|
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 {
|
Ok(Config {
|
||||||
postgres_user: "bubble_admin".into(),
|
postgres: postgres_admin,
|
||||||
postgres_pass: bubble_admin_password
|
bubble: bubble_admin
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +117,7 @@ fn main() {
|
|||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if args.setup {
|
if args.setup {
|
||||||
match full_setup() {
|
match full_setup(args) {
|
||||||
Ok(config) => println!("{}", serde_json::to_string(&config).unwrap()),
|
Ok(config) => println!("{}", serde_json::to_string(&config).unwrap()),
|
||||||
Err(e) => eprintln!("{:#?}", e)
|
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 (
|
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)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -10,3 +16,10 @@ CREATE TABLE IF NOT EXISTS invites (
|
|||||||
expires INTEGER,
|
expires INTEGER,
|
||||||
PRIMARY KEY (id)
|
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