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

View File

@@ -1,6 +1,4 @@
pub mod request;
pub mod schema;
pub mod util;
use actix_cors::Cors;
use actix_web::middleware::Logger;
@@ -8,31 +6,15 @@ use actix_web::HttpServer;
use actix_web::App;
use actix_web::http;
use actix_web::web;
use diesel::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use request::{login, register, gamenights, gamenight_post, gamenight_get};
use util::GetConnection;
use tracing_actix_web::TracingLogger;
pub(crate) type DbPool = Pool<ConnectionManager<PgConnection>>;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
fn run_migration(conn: &mut PgConnection) {
conn.run_pending_migrations(MIGRATIONS).unwrap();
}
use gamenight_database::*;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let url = "postgres://root:root@127.0.0.1/gamenight";
let manager = ConnectionManager::<PgConnection>::new(url);
// Refer to the `r2d2` documentation for more methods to use
// when building a connection pool
let pool = Pool::builder()
.test_on_check_out(true)
.build(manager)
.expect("Could not build connection pool");
let pool = get_connection_pool(url);
let mut conn = pool.get_conn();
run_migration(&mut conn);

View File

@@ -6,7 +6,7 @@ use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation}
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::{schema::user::{User, get_user}, DbPool};
use gamenight_database::{user::{get_user, Role, User}, DbPool};
use super::error::ApiError;
@@ -16,6 +16,24 @@ pub struct Claims {
uid: Uuid
}
pub struct AuthUser {
pub id: Uuid,
pub username: String,
pub email: String,
pub role: Role,
}
impl From<User> for AuthUser {
fn from(value: User) -> Self {
Self{
id: value.id,
username: value.username,
email: value.email,
role: value.role,
}
}
}
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
let token = req.headers()
.get(http::header::AUTHORIZATION)
@@ -43,18 +61,19 @@ pub fn get_token(user: &User) -> Result<String, ApiError> {
&EncodingKey::from_secret(secret.as_bytes()))?)
}
impl FromRequest for User {
impl FromRequest for AuthUser {
type Error = ApiError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready(
(|| -> Result<User, ApiError>{
(|| -> Result<AuthUser, ApiError>{
let pool = req.app_data::<Data<DbPool>>().expect("No database configured");
let mut conn = pool.get().expect("couldn't get db connection from pool");
let uid = get_claims(req)?.uid;
let user = get_user(&mut conn, uid)?;
Ok(get_user(&mut conn, uid)?)
Ok(user.into())
})()
)
}

View File

@@ -3,7 +3,7 @@ use actix_web::{ResponseError, error::BlockingError, HttpResponse, http::{header
use serde::{Serialize, Deserialize};
use validator::ValidationErrors;
use crate::schema::error::DatabaseError;
use gamenight_database::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug)]
pub struct ApiError {

View File

@@ -2,18 +2,18 @@ use actix_web::{get, web, Responder, http::header::ContentType, HttpResponse, po
use chrono::{DateTime, ParseError};
use uuid::Uuid;
use crate::schema::{self};
use crate::schema::user::User;
use gamenight_database::{gamenight::Gamenight, DbPool, GetConnection};
use crate::request::authorization::AuthUser;
use crate::request::requests::GamenightGet;
use crate::request::requests::GamenightPost;
use crate::request::responses::GameNightsResponse;
use crate::request::error::ApiError;
use crate::DbPool;
use crate::util::GetConnection;
impl GamenightPost {
pub fn into_with_user(&self, user: User) -> Result<schema::gamenight::Gamenight, ParseError> {
Ok(schema::gamenight::Gamenight {
pub fn into_with_user(&self, user: AuthUser) -> Result<Gamenight, ParseError> {
Ok(Gamenight {
datetime: DateTime::parse_from_rfc3339(&self.datetime)?.with_timezone(&chrono::Utc),
id: Uuid::new_v4(),
name: self.name.clone(),
@@ -29,9 +29,9 @@ impl From<GamenightGet> for Uuid {
}
#[get("/gamenights")]
pub async fn gamenights(pool: web::Data<DbPool>, _user: User) -> Result<impl Responder, ApiError> {
pub async fn gamenights(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenights: GameNightsResponse = schema::gamenights(&mut conn)?;
let gamenights: GameNightsResponse = gamenight_database::gamenights(&mut conn)?;
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
@@ -40,19 +40,19 @@ pub async fn gamenights(pool: web::Data<DbPool>, _user: User) -> Result<impl Res
}
#[post("/gamenight")]
pub async fn gamenight_post(pool: web::Data<DbPool>, user: User, gamenight_data: web::Json<GamenightPost>) -> Result<impl Responder, ApiError> {
pub async fn gamenight_post(pool: web::Data<DbPool>, user: AuthUser, gamenight_data: web::Json<GamenightPost>) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
schema::gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
gamenight_database::gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
Ok(HttpResponse::Ok())
}
#[get("/gamenight")]
pub async fn gamenight_get(pool: web::Data<DbPool>, _user: User, gamenight_data: web::Json<GamenightGet>) -> Result<impl Responder, ApiError> {
pub async fn gamenight_get(pool: web::Data<DbPool>, _user: AuthUser, gamenight_data: web::Json<GamenightGet>) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenight = schema::gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
let gamenight = gamenight_database::gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
//let participants = schema::user::get_participants(&mut conn, gamenight_id);
Ok(HttpResponse::Ok()

View File

@@ -1,10 +1,6 @@
use gamenight_database::{DbPool, GetConnection, user::count_users_with_username, user::count_users_with_email};
use serde::{Serialize, Deserialize};
use validator::Validate;
use diesel::PgConnection;
use diesel::r2d2::ConnectionManager;
use diesel::r2d2::Pool;
use crate::schema::user::{unique_email, unique_username};
use validator::{Validate, ValidationError};
#[derive(Serialize, Deserialize, Clone)]
pub struct Login {
@@ -13,7 +9,29 @@ pub struct Login {
}
pub struct RegisterContext<'v_a> {
pub pool: &'v_a Pool<ConnectionManager<PgConnection>>
pub pool: &'v_a DbPool
}
pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_username(&mut conn, username)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("User already exists")),
Err(_) => Err(ValidationError::new("Database error while validating user")),
}
}
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_email(&mut conn, email)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("email already exists")),
Err(_) => Err(ValidationError::new("Database error while validating email"))
}
}
#[derive(Serialize, Deserialize, Clone, Validate)]

View File

@@ -1,7 +1,7 @@
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::schema::gamenight::Gamenight;
use gamenight_database::gamenight::Gamenight;
#[derive(Serialize, Deserialize)]
pub struct LoginResponse {

View File

@@ -2,27 +2,25 @@
use actix_web::http::header::ContentType;
use actix_web::{web, get, post, HttpResponse, Responder};
use validator::ValidateArgs;
use crate::DbPool;
use crate::request::requests::{Login, Register, RegisterContext};
use crate::request::error::ApiError;
use crate::request::responses::LoginResponse;
use crate::request::authorization::get_token;
use crate::util::GetConnection;
use crate::schema::{self};
use serde_json;
use gamenight_database::{DbPool, GetConnection};
impl From<Login> for schema::user::LoginUser {
impl From<Login> for gamenight_database::user::LoginUser {
fn from(val: Login) -> Self {
schema::user::LoginUser {
gamenight_database::user::LoginUser {
username: val.username,
password: val.password
}
}
}
impl From<Register> for schema::user::Register {
impl From<Register> for gamenight_database::user::Register {
fn from(val: Register) -> Self {
schema::user::Register {
gamenight_database::user::Register {
email: val.email,
username: val.username,
password: val.password
@@ -36,7 +34,7 @@ pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Res
if let Ok(Some(user)) = web::block(move || {
let mut conn = pool.get_conn();
schema::login(&mut conn, data.into())
gamenight_database::login(&mut conn, data.into())
})
.await?
{
@@ -58,7 +56,7 @@ pub async fn register(pool: web::Data<DbPool>, register_data: web::Json<Register
register_data.validate_with_args(&RegisterContext{pool: &pool})?;
let register_request = register_data.into_inner().into();
let mut conn = pool.get_conn();
schema::register(&mut conn, register_request)?;
gamenight_database::register(&mut conn, register_request)?;
Ok(())
}).await??;

View File

@@ -1,13 +0,0 @@
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

@@ -1,28 +0,0 @@
use chrono::{DateTime, Utc};
use diesel::{Insertable, Queryable, PgConnection, RunQueryDsl, insert_into, QueryDsl};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::schema::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(crate) fn get_gamenight(conn: &mut PgConnection, id: Uuid) -> Result<Gamenight, DatabaseError> {
Ok(gamenight::table.find(id).first(conn)?)
}

View File

@@ -1,21 +0,0 @@
use diesel::{ExpressionMethods, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::schema::schema::gamenight_participants;
use super::error::DatabaseError;
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
#[diesel(belongs_to(Gamenight))]
#[diesel(belongs_to(User))]
#[diesel(table_name = gamenight_participants)]
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_participants::table
.filter(gamenight_participants::gamenight_id.eq(id))
.get_results(conn)?)
}

View File

@@ -1,10 +0,0 @@
pub mod user;
pub mod error;
pub mod schema;
pub mod gamenight;
pub mod gamenight_participants;
pub use user::login;
pub use user::register;
pub use gamenight::gamenights;
pub use gamenight_participants::gamenight_participants;

View File

@@ -1,83 +0,0 @@
// @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_participants (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_participants -> gamenight (gamenight_id));
diesel::joinable!(gamenight_participants -> users (user_id));
diesel::joinable!(pwd -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
gamenight,
gamenight_gamelist,
gamenight_participants,
known_games,
pwd,
registration_tokens,
users,
);

View File

@@ -1,154 +0,0 @@
use argon2::password_hash::Salt;
use diesel::Connection;
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use diesel::{PgConnection, 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 validator::ValidationError;
use crate::util::GetConnection;
use super::schema::{pwd, users};
pub use super::error::DatabaseError;
use ::rand_core::{OsRng,TryRngCore};
use crate::request::requests::RegisterContext;
#[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::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 PgConnection, 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 PgConnection, id: Uuid) -> Result<User, DatabaseError> {
Ok(users::table.find(id).first(conn)?)
}
pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get().expect("Couldn't get db connection from pool");
match users::table
.count()
.filter(users::username.eq(username))
.get_result(&mut conn)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("User already exists")),
Err(_) => Err(ValidationError::new("Database error while validating user")),
}
}
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match users::table
.count()
.filter(users::email.eq(email))
.get_result(&mut conn)
{
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("email already exists")),
Err(_) => Err(ValidationError::new(
"Database error while validating email",
)),
}
}
pub fn register(conn: &mut PgConnection, 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(())
})
}

View File

@@ -1,11 +0,0 @@
use diesel::{r2d2::{PooledConnection, ManageConnection, 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")
}
}