(feat) Switched to sqlx.

Signed-off-by: Louis Hollingworth <louis@hollingworth.nl>
This commit is contained in:
Louis Hollingworth 2023-10-26 18:14:26 +01:00
parent f267d6b285
commit ba4216c21e
Signed by: lucxjo
GPG key ID: A11415CB3DC7809B
22 changed files with 957 additions and 579 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
/target /target
.DS_Store .DS_Store
.env .env
/.sqlx

1040
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -19,11 +19,15 @@ argon2 = { version = "0.5.2", features = ["password-hash", "std"] }
axum = {version = "0.6.20", features = ["macros"]} axum = {version = "0.6.20", features = ["macros"]}
chrono = {version = "0.4.31", features = ["serde"]} chrono = {version = "0.4.31", features = ["serde"]}
crypto = { version = "0.5.1", features = ["password-hash"] } crypto = { version = "0.5.1", features = ["password-hash"] }
diesel = { version = "2.1.3", features = ["postgres", "extras", "uuid", "r2d2"] }
diesel_migrations = { version = "2.1.0", features = ["postgres"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1.0.189", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"
tokio = { version = "1.33.0", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }
uuid = { version = "1.0.0", features = ["serde", "v4", "v7"] } uuid = { version = "1.0.0", features = ["serde", "v4", "v7"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "macros", "migrate", "uuid", "chrono", "json"] }
anyhow = "1.0.75"
thiserror = "1.0.50"
[profile.dev.package.sqlx-macros]
opt-level = 3

View file

@ -1,13 +0,0 @@
# SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

View file

View file

@ -1,42 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View file

@ -1,6 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file should undo anything in `up.sql`
DROP TABLE posts;

View file

@ -1,17 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Your SQL goes here
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
slug VARCHAR NOT NULL,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
user_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
SELECT diesel_manage_updated_at('posts');

View file

@ -1,6 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file should undo anything in `up.sql`
DROP TABLE blogs;

View file

@ -1,18 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Your SQL goes here
CREATE TABLE blogs (
id SERIAL PRIMARY KEY,
slug VARCHAR NOT NULL,
name VARCHAR NOT NULL,
description TEXT NOT NULL,
url TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
SELECT diesel_manage_updated_at('blogs');

View file

@ -1,8 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file should undo anything in `up.sql`
ALTER TABLE posts
DROP COLUMN blog_id;

View file

@ -1,8 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Your SQL goes here
ALTER TABLE posts
ADD COLUMN blog_id INTEGER NOT NULL REFERENCES blogs(id);

View file

@ -2,9 +2,6 @@
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file was automatically created by Diesel to setup helper functions -- Add down migration script here
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at(); DROP FUNCTION IF EXISTS diesel_set_updated_at();

View file

@ -0,0 +1,24 @@
-- SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Add up migration script here
CREATE OR REPLACE FUNCTION manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql

View file

@ -2,5 +2,5 @@
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
-- This file should undo anything in `up.sql` -- Add down migration script here
DROP TABLE users; DROP TABLE users;

View file

@ -2,17 +2,16 @@
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
-- Your SQL goes here -- Add up migration script here
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users ( CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR NOT NULL, username VARCHAR UNIQUE NOT NULL,
epost VARCHAR NOT NULL, epost VARCHAR UNIQUE NOT NULL,
pass VARCHAR NOT NULL, pass VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
main_fedi TEXT, main_fedi TEXT
UNIQUE(username, epost)
); );
SELECT diesel_manage_updated_at('users'); SELECT manage_updated_at('users');

View file

@ -2,27 +2,52 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
pub mod models; pub mod models;
pub mod schema; use thiserror::Error;
use diesel::prelude::*;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; #[derive(Error, Debug)]
pub enum FbError {
#[error("Database Error: {0}")]
DbError(sqlx::Error),
pub fn establish_connection() -> diesel::pg::PgConnection { #[error("Internal Error: {0}")]
IError(String)
}
pub async fn connection_pool() -> Result<sqlx::Pool<sqlx::Postgres>, sqlx::Error> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
diesel::pg::PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) Ok(sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
.connect(&database_url).await?)
} }
pub fn connection_pool() -> DbPool { pub async fn get_user_by_name_for_auth(username: String, pool: sqlx::Pool<sqlx::Postgres>) -> Result<models::user::AuthUser, sqlx::Error> {
dotenvy::dotenv().ok(); let user = sqlx::query_as!(models::user::AuthUser, "SELECT id, username, pass FROM users WHERE username = $1", username).fetch_one(&pool).await.unwrap();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); Ok(user)
let manager = diesel::r2d2::ConnectionManager::<diesel::PgConnection>::new(database_url); }
diesel::r2d2::Pool::builder()
.test_on_check_out(true) pub async fn get_all_users(pool: sqlx::Pool<sqlx::Postgres>) -> Result<Vec<models::user::User>, sqlx::Error> {
.build(manager) let users = sqlx::query_as!(models::user::User, "SELECT * FROM users").fetch_all(&pool).await.unwrap();
.expect("Could not build connection pool")
Ok(users)
}
/// Creates user in database. No need to salt and hash the password, the function does it for you.
pub async fn create_user(user: models::user::NewUser, pool: sqlx::Pool<sqlx::Postgres>) -> Result<(), FbError> {
use argon2::password_hash::{PasswordHasher, SaltString};
let argon = argon2::Argon2::default();
let salt = SaltString::generate(&mut rand::thread_rng());
let hashed: String = match argon.hash_password(&user.pass.into_bytes(), &salt) {
Err(_) => return Err(FbError::IError("Error while hashing password".to_string())),
Ok(pswd) => pswd.to_string()
};
let _ = sqlx::query_as!(models::user::NewUser, "
INSERT INTO users (username, epost, pass)
VALUES ($1, $2, $3)", user.username, user.epost, hashed).execute(&pool).await.unwrap();
Ok(())
} }

View file

@ -3,18 +3,16 @@
* *
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
use argon2::password_hash::{PasswordHasher, SaltString};
use axum::{routing::{get, post}, Router, response::Json, extract::State}; use axum::{routing::{get, post}, Router, response::Json, extract::State};
use diesel::prelude::*;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let pool: fediblog::DbPool = fediblog::connection_pool(); let pool = fediblog::connection_pool().await.unwrap();
let app = Router::new() let app = Router::new()
.route("/health", get(health_check)) .route("/health", get(health_check))
.route("/api/v0/user", post(create_user)) .route("/api/v0/user", post(create_user))
.route("/api/v0/user", get(get_user)) // .route("/api/v0/user", get(get_user))
.with_state(pool); .with_state(pool);
axum::Server::bind(&"0.0.0.0:7654".parse().unwrap()) axum::Server::bind(&"0.0.0.0:7654".parse().unwrap())
@ -23,115 +21,17 @@ async fn main() {
.unwrap(); .unwrap();
} }
#[axum::debug_handler]
async fn create_user( async fn create_user(
State(pool): State<fediblog::DbPool>, State(pool): State<sqlx::Pool<sqlx::Postgres>>,
Json(new_user): Json<NewUser>, Json(u): Json<fediblog::models::user::NewUser>
) -> Result<Json<UserResponse>, (axum::http::StatusCode, String)> { ) -> &'static str {
use fediblog::schema::users::dsl::*; let _ = fediblog::create_user(u, pool).await;
let argon = argon2::Argon2::default();
let salt = SaltString::generate(&mut rand::thread_rng());
let hashed_pass: String = match argon.hash_password(&new_user.pass.into_bytes(), &salt) {
Err(_) => {
return Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"Error hashing password".to_string(),
))
}
Ok(pswd) => pswd.to_string(),
};
diesel::insert_into(users)
.values((
username.eq(new_user.username.clone()),
epost.eq(new_user.epost.clone()),
pass.eq(hashed_pass),
))
.execute(&mut pool.get().unwrap())
.expect("Error saving new user");
let uid = get_userid_from_name(new_user.username.clone(), &mut pool.get().unwrap()); "All done :)"
match uid {
None => return Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, "There was an error creating this account".to_string())),
Some(user_id) => return Ok(Json(
UserResponse {
id: user_id,
username: new_user.username,
epost: Some(new_user.epost.to_string()),
main_fedi: None
}
))
}
}
fn get_userid_from_name(username: String, pool: &mut diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>) -> Option<uuid::Uuid> {
let uv: Result<Vec<fediblog::models::BaseUser>, diesel::result::Error> = pool.build_transaction().read_only().run(|conn| {
let userv = fediblog::schema::users::dsl::users
.select((fediblog::schema::users::dsl::id, fediblog::schema::users::dsl::username))
.load::<fediblog::models::BaseUser>(conn);
Ok(userv.unwrap())
});
find_userid_from_base_user(uv.unwrap(), username)
}
fn find_userid_from_base_user(usrs: Vec<fediblog::models::BaseUser>, usrn: String) -> Option<uuid::Uuid> {
for u in usrs {
if u.username == usrn {
return Some(u.id);
}
}
None
}
async fn get_user(
State(pool): State<fediblog::DbPool>,
Json(auth_user): Json<AuthUser>
) ->Result<Json<UserResponse>, (axum::http::StatusCode, String)> {
let conn = &mut pool.get().unwrap();
let uid = get_userid_from_name(auth_user.username, conn).unwrap();
let userv = diesel::sql_query(format!("SELECT * FROM users WHERE id = {}", uid.to_string())).load::<fediblog::models::User>(conn);
match userv {
Ok(usrs) => return Ok(Json(UserResponse{
id: usrs[0].id,
}))
}
Ok(Json(UserResponse{
id: user.id,
username: user.username.clone(),
epost: None,
main_fedi: None
}))
} }
async fn health_check() -> &'static str { async fn health_check() -> &'static str {
"Saluton, amiko! Looks like we are running!" "Saluton, amiko! Looks like we are running!"
} }
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct NewUser {
username: String,
epost: String,
pass: String,
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct AuthUser {
username: String,
pass: String,
}
#[derive(serde::Deserialize, serde::Serialize)]
struct UserResponse {
id: uuid::Uuid,
username: String,
epost: Option<String>,
main_fedi: Option<String>,
}

View file

@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(
Selectable, Debug, Queryable, QueryableByName
)]
#[diesel(table_name = crate::schema::users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct User {
pub id: Uuid,
pub username: String,
pub epost: String,
pub pass: String,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
pub main_fedi: Option<String>,
}
#[derive(Queryable, PartialEq, Debug)]
#[diesel(table_name = crate::schema::users)]
pub struct BaseUser {
pub id: Uuid,
pub username: String,
}
#[derive(Insertable, Debug, Selectable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser {
pub username: String,
pub epost: String,
pub pass: String
}
#[derive(
Queryable, Selectable, Identifiable, Associations, Debug, PartialEq, Serialize, Deserialize,
)]
#[diesel(belongs_to(User))]
#[diesel(table_name = crate::schema::blogs)]
pub struct Blog {
pub id: isize,
pub slug: String,
pub name: String,
pub description: String,
pub url: String,
pub user_id: Uuid,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}
#[derive(
Queryable, Selectable, Identifiable, Associations, Debug, PartialEq, Serialize, Deserialize,
)]
#[diesel(belongs_to(User))]
#[diesel(belongs_to(Blog))]
#[diesel(table_name = crate::schema::posts)]
pub struct Post {
pub id: isize,
pub slug: String,
pub title: String,
pub body: String,
pub published: bool,
pub user_id: Uuid,
pub blog_id: isize,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

5
src/models/mod.rs Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod user;

28
src/models/user.rs Normal file
View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
#[derive(serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct AuthUser {
pub id: uuid::Uuid,
pub username: String,
pub pass: String,
}
#[derive(serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct NewUser {
pub username: String,
pub pass: String,
pub epost: String
}
#[derive(serde::Deserialize, serde::Serialize, sqlx::FromRow)]
pub struct User {
pub id: uuid::Uuid,
pub username: String,
pub epost: String,
pub pass: String,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
pub main_fedi: Option<String>
}

View file

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2023 Louis Hollingworth <louis@hollingworth.nl>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// @generated automatically by Diesel CLI.
diesel::table! {
blogs (id) {
id -> Int4,
slug -> Varchar,
name -> Varchar,
description -> Text,
url -> Text,
user_id -> Uuid,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
posts (id) {
id -> Int4,
slug -> Varchar,
title -> Varchar,
body -> Text,
published -> Bool,
user_id -> Uuid,
created_at -> Timestamp,
updated_at -> Timestamp,
blog_id -> Int4,
}
}
diesel::table! {
users (id) {
id -> Uuid,
username -> Varchar,
epost -> Varchar,
pass -> Varchar,
created_at -> Timestamp,
updated_at -> Timestamp,
main_fedi -> Nullable<Text>,
}
}
diesel::joinable!(blogs -> users (user_id));
diesel::joinable!(posts -> blogs (blog_id));
diesel::joinable!(posts -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
blogs,
posts,
users,
);