Splits database into a separate crate

This commit is contained in:
2025-05-30 14:31:00 +02:00
parent 3f7ed03973
commit 597a960bf1
34 changed files with 368 additions and 402 deletions

1
gamenight-database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

View File

@@ -0,0 +1,15 @@
[package]
name = "gamenight-database"
version = "0.1.0"
edition = "2024"
[dependencies]
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] }
diesel-derive-enum = { version = "2.0", features = ["postgres"] }
diesel_migrations = "2.0"
argon2 = { version = "0.5", features = ["std"] }
uuid = { version = "1.17.0", features = ["serde", "v4"] }
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
rand_core = "0.9"

View File

@@ -0,0 +1,2 @@
[print_schema]
file = "src/schema/schema.rs"

View File

@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
drop table gamenight;
drop table known_games;

View File

@@ -0,0 +1,12 @@
-- Your SQL goes here
CREATE TABLE gamenight (
id UUID NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL,
datetime VARCHAR NOT NULL
);
CREATE TABLE known_games (
id UUID NOT NULL PRIMARY KEY,
name VARCHAR UNIQUE NOT NULL
);

View File

@@ -0,0 +1,6 @@
-- This file should undo anything in `up.sql`
drop table pwd;
drop table users;
drop type Role;

View File

@@ -0,0 +1,28 @@
CREATE TYPE Role AS ENUM ('user', 'admin');
CREATE TABLE users (
id UUID NOT NULL PRIMARY KEY,
username VARCHAR UNIQUE NOT NULL,
email VARCHAR UNIQUE NOT NULL,
role Role NOT NULL
);
CREATE TABLE pwd (
user_id UUID NOT NULL PRIMARY KEY,
password VARCHAR NOT NULL,
CONSTRAINT FK_UserId FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
--Initialize default admin user, with password "gamenight!"
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DO $$
DECLARE
admin_uuid uuid = uuid_generate_v4();
BEGIN
INSERT INTO users (id, username, email, role)
values(admin_uuid, 'admin', '', 'admin');
insert INTO pwd (user_id, password)
values(admin_uuid, '$argon2id$v=19$m=4096,t=3,p=1$zEdUjCAnZqd8DziYWzlFHw$YBLQhKvYIZBY43B8zM6hyBvLKuqTeh0EM5pKOfbWQSI');
END $$;

View File

@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE gamenight
DROP COLUMN owner_id;

View File

@@ -0,0 +1,19 @@
ALTER TABLE gamenight RENAME TO _gamenight_old;
CREATE TABLE gamenight (
id UUID NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL,
datetime VARCHAR NOT NULL,
owner_id UUID NOT NULL,
CONSTRAINT FK_OwnerId FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
);
SET session_replication_role = 'replica';
INSERT INTO gamenight (id, name, datetime, owner_id)
select id, name, datetime, '00000000-0000-0000-0000-000000000000'
FROM _gamenight_old;
drop table _gamenight_old;
SET session_replication_role = 'origin';

View File

@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
drop table gamenight_gamelist;

View File

@@ -0,0 +1,9 @@
-- Your SQL goes here
create table gamenight_gamelist (
gamenight_id UUID NOT NULL,
game_id UUID NOT NULL,
CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE,
CONSTRAINT FK_game_id FOREIGN KEY (game_id) REFERENCES known_games(id) ON DELETE CASCADE,
PRIMARY KEY(gamenight_id, game_id)
);

View File

@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
drop table gamenight_participant;

View File

@@ -0,0 +1,9 @@
-- Your SQL goes here
create table gamenight_participant (
gamenight_id UUID NOT NULL,
user_id UUID NOT NULL,
CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE,
CONSTRAINT FK_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY(gamenight_id, user_id)
)

View File

@@ -0,0 +1,6 @@
-- This file should undo anything in `up.sql`
drop table registration_tokens;
ALTER TABLE gamenight
ALTER datetime TYPE VARCHAR;

View File

@@ -0,0 +1,11 @@
-- Your SQL goes here
create table registration_tokens (
id UUID PRIMARY KEY,
token CHARACTER(32) NOT NULL,
single_use BOOLEAN NOT NULL,
expires TIMESTAMPTZ
);
ALTER TABLE gamenight
ALTER datetime TYPE TIMESTAMPTZ using datetime::timestamp;

View File

@@ -0,0 +1,13 @@
pub struct DatabaseError(pub String);
impl From<diesel::result::Error> for DatabaseError {
fn from(value: diesel::result::Error) -> Self {
DatabaseError(value.to_string())
}
}
impl From<argon2::password_hash::Error> for DatabaseError {
fn from(value: argon2::password_hash::Error) -> Self {
DatabaseError(value.to_string())
}
}

View File

@@ -0,0 +1,28 @@
use chrono::{DateTime, Utc};
use diesel::{Insertable, Queryable, PgConnection, RunQueryDsl, insert_into, QueryDsl};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::schema::gamenight;
use super::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[diesel(table_name = gamenight)]
pub struct Gamenight {
pub id: Uuid,
pub name: String,
pub datetime: DateTime<Utc>,
pub owner_id: Uuid,
}
pub fn gamenights(conn: &mut PgConnection) -> Result<Vec::<Gamenight>, DatabaseError> {
Ok(gamenight::table.load::<Gamenight>(conn)?)
}
pub fn add_gamenight(conn: &mut PgConnection, gamenight: Gamenight) -> Result<usize, DatabaseError> {
Ok(insert_into(gamenight::table).values(&gamenight).execute(conn)?)
}
pub fn get_gamenight(conn: &mut PgConnection, id: Uuid) -> Result<Gamenight, DatabaseError> {
Ok(gamenight::table.find(id).first(conn)?)
}

View File

@@ -0,0 +1,22 @@
use diesel::{ExpressionMethods, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::schema::gamenight_participant;
use super::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[diesel(belongs_to(Gamenight))]
#[diesel(belongs_to(User))]
#[diesel(table_name = gamenight_participant)]
pub struct GamenightParticipants {
pub gamenight_id: Uuid,
pub user_id: Uuid,
}
pub fn gamenight_participants(conn: &mut PgConnection, id: Uuid) -> Result<Vec<GamenightParticipants>, DatabaseError> {
Ok(gamenight_participant::table
.filter(gamenight_participant::gamenight_id.eq(id))
.get_results(conn)?)
}

View File

@@ -0,0 +1,46 @@
pub mod user;
pub mod error;
pub mod schema;
pub mod gamenight;
pub mod gamenight_participants;
use diesel::r2d2::ConnectionManager;
use diesel::r2d2::ManageConnection;
use diesel::r2d2::Pool;
use diesel::r2d2::PooledConnection;
use diesel::PgConnection;
use diesel_migrations::embed_migrations;
use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
pub use user::login;
pub use user::register;
pub use gamenight::gamenights;
pub use gamenight_participants::gamenight_participants;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub type DbConnection = PgConnection;
pub type DbPool = Pool<ConnectionManager<DbConnection>>;
pub fn run_migration(conn: &mut DbConnection) {
conn.run_pending_migrations(MIGRATIONS).unwrap();
}
pub fn get_connection_pool(url: &str) -> DbPool {
let manager = ConnectionManager::<PgConnection>::new(url);
// Refer to the `r2d2` documentation for more methods to use
// when building a connection pool
Pool::builder()
.test_on_check_out(true)
.build(manager)
.expect("Could not build connection pool")
}
pub trait GetConnection<T> where T: ManageConnection {
fn get_conn(&self) -> PooledConnection<T>;
}
impl<T: ManageConnection> GetConnection<T> for Pool<T> {
fn get_conn(&self) -> PooledConnection<T> {
self.get().expect("Couldn't get db connection from pool")
}
}

View File

@@ -0,0 +1,83 @@
// @generated automatically by Diesel CLI.
pub mod sql_types {
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "role"))]
pub struct Role;
}
diesel::table! {
gamenight (id) {
id -> Uuid,
name -> Varchar,
datetime -> Timestamptz,
owner_id -> Uuid,
}
}
diesel::table! {
gamenight_gamelist (gamenight_id, game_id) {
gamenight_id -> Uuid,
game_id -> Uuid,
}
}
diesel::table! {
gamenight_participant (gamenight_id, user_id) {
gamenight_id -> Uuid,
user_id -> Uuid,
}
}
diesel::table! {
known_games (id) {
id -> Uuid,
name -> Varchar,
}
}
diesel::table! {
pwd (user_id) {
user_id -> Uuid,
password -> Varchar,
}
}
diesel::table! {
registration_tokens (id) {
id -> Uuid,
#[max_length = 32]
token -> Bpchar,
single_use -> Bool,
expires -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::Role;
users (id) {
id -> Uuid,
username -> Varchar,
email -> Varchar,
role -> Role,
}
}
diesel::joinable!(gamenight -> users (owner_id));
diesel::joinable!(gamenight_gamelist -> gamenight (gamenight_id));
diesel::joinable!(gamenight_gamelist -> known_games (game_id));
diesel::joinable!(gamenight_participant -> gamenight (gamenight_id));
diesel::joinable!(gamenight_participant -> users (user_id));
diesel::joinable!(pwd -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
gamenight,
gamenight_gamelist,
gamenight_participant,
known_games,
pwd,
registration_tokens,
users,
);

View File

@@ -0,0 +1,140 @@
use argon2::password_hash::Salt;
use diesel::Connection;
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, Insertable, Queryable};
use diesel_derive_enum::DbEnum;
use argon2::password_hash::SaltString;
use argon2::PasswordHash;
use argon2::PasswordVerifier;
use argon2::Argon2;
use argon2::password_hash::PasswordHasher;
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::rand_core::RngCore;
use crate::DbConnection;
use super::schema::{pwd, users};
pub use super::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[diesel(table_name = pwd)]
struct Pwd {
user_id: Uuid,
password: String,
}
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[diesel(table_name = users)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: String,
pub role: Role,
}
#[derive(DbEnum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[ExistingTypePath = "crate::schema::sql_types::Role"]
pub enum Role {
Admin,
User
}
pub struct LoginUser {
pub username: String,
pub password: String
}
#[derive(Serialize, Deserialize)]
pub struct LoginResult {
pub result: bool,
pub user: Option<User>,
}
#[derive(Serialize, Deserialize)]
pub struct RegisterResult {
pub result: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Register {
pub username: String,
pub email: String,
pub password: String
}
pub fn login(conn: &mut DbConnection, user: LoginUser) -> Result<Option<User>, DatabaseError> {
let id: Uuid = users::table
.filter(users::username.eq(&user.username))
.or_filter(users::email.eq(&user.username))
.select(users::id)
.first(conn)?;
let pwd: String = pwd::table
.filter(pwd::user_id.eq(id))
.select(pwd::password)
.first(conn)?;
let parsed_hash = PasswordHash::new(&pwd)?;
if Argon2::default()
.verify_password(user.password.as_bytes(), &parsed_hash)
.is_ok()
{
Ok(Some(users::table.find(id).first(conn)?))
} else {
Ok(None)
}
}
pub fn get_user(conn: &mut DbConnection, id: Uuid) -> Result<User, DatabaseError> {
Ok(users::table.find(id).first(conn)?)
}
pub fn register(conn: &mut DbConnection, register: Register) -> Result<(), DatabaseError> {
let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
OsRng.try_fill_bytes(&mut bytes).unwrap();
let salt = SaltString::encode_b64(&bytes).unwrap();
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(register.password.as_bytes(), &salt)?
.to_string();
conn.transaction(|c| {
let id = Uuid::new_v4();
diesel::insert_into(users::table)
.values(User {
id,
username: register.username,
email: register.email,
role: Role::User,
})
.execute(c)?;
diesel::insert_into(pwd::table)
.values(Pwd {
user_id: id,
password: password_hash,
})
.execute(c)?;
Ok(())
})
}
pub fn count_users_with_username(conn: &mut DbConnection, username: &String) -> Result<i64, DatabaseError> {
Ok(users::table
.count()
.filter(users::username.eq(username))
.get_result::<i64>(conn)?)
}
pub fn count_users_with_email(conn: &mut DbConnection, email: &String) -> Result<i64, DatabaseError> {
Ok(users::table
.count()
.filter(users::email.eq(email))
.get_result::<i64>(conn)?)
}