summaryrefslogtreecommitdiff
path: root/server/src/db.rs
blob: 2bc3f9b282e07ab49aee9af650617566ced53e2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
use tokio;
use tokio::sync::Mutex;
use std::{
    sync::Arc,
    ops::DerefMut,
    time::SystemTime,
    fs,
    process,
};
use sqlx::{
    ConnectOptions,
    SqliteConnection,
    Row,
};
use sqlx::sqlite::{
    SqliteConnectOptions,
    SqliteJournalMode,
};
use rand::RngCore;
use argon2::password_hash::rand_core::OsRng;

use crate::constants::*;
use crate::util::base64_encode;

async fn initialise_schema(conn: &mut SqliteConnection) {
    match sqlx::query(&fs::read_to_string("schema.sql").unwrap()).execute(conn).await {
        Ok(_) => {},
        Err(err) => {
            eprintln!("Error: Could not initialise database schema: {err}");
            process::exit(1);
        },
    }
}

pub async fn open() -> SqliteConnection {
    let opts = SqliteConnectOptions::new()
                    .filename(DB_FILE_NAME)
                    .create_if_missing(true)
                    .journal_mode(SqliteJournalMode::Wal)
                    .pragma("foreign_keys", "on");

    let mut conn = match opts.connect().await {
        Ok(conn) => conn,
        Err(err) => {
            eprintln!("Error: Could not open database file '{DB_FILE_NAME}': {err}");
            process::exit(1);
        }
    };

    match sqlx::query("select version from meta").fetch_one(&mut conn).await {
        Ok(row) => {
            let ver: i64 = row.get(0);
            if ver == DB_SCHEMA_VERSION {
                conn
            } else {
                eprintln!("Error: Database schema version {ver} but application schema version {DB_SCHEMA_VERSION}");
                process::exit(1);
            }
        },
        Err(err) => {
            eprintln!("Error: Failed querying database meta version: {err}");
            eprintln!("Initialising schema.");
            initialise_schema(&mut conn).await;
            conn
        },
    }
}

pub type DB = Arc<Mutex<SqliteConnection>>;

pub async fn register_account(db: DB, username: &str, passhash: &str) -> Result<(), String> {
    let mut conn = db.lock().await;
    match sqlx::query("insert into Users (username, passhash) values ($1, $2)")
            .bind(username)
            .bind(passhash)
            .execute(conn.deref_mut())
            .await {
        Ok(_) => Ok(()),
        Err(_) => Err("User already exists".to_string()),
    }
}

pub async fn get_passhash(db: DB, username: &str) -> Result<String, ()> {
    let mut conn = db.lock().await;
    match sqlx::query("select passhash from Users where username = $1")
            .bind(username)
            .fetch_optional(conn.deref_mut())
            .await {
        Ok(Some(row)) => Ok(row.get(0)),
        Ok(None) => Err(()),
        Err(err) => {
            eprintln!("db_get_passhash: sqlx error: {err}");
            Err(())
        }
    }
}

fn generate_login_token() -> String {
    let mut bytes = [0u8; 30];
    OsRng.fill_bytes(&mut bytes);
    base64_encode(&bytes)
}

pub async fn create_login_token(db: DB, username: &str) -> Result<String, ()> {
    let mut conn = db.lock().await;
    let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
    let token = generate_login_token();
    match sqlx::query("insert into Logins (user, token, expire) values ($1, $2, $3)")
            .bind(username)
            .bind(&token)
            .bind(now as i64 + LOGIN_TOKEN_EXPIRE_SECS)
            .execute(conn.deref_mut()).await {
        Ok(_) => Ok(token),
        Err(_) => Err(()),
    }
}