diff --git a/crates/database/migrations/20230305135630_users.down.sql b/crates/database/migrations/20230305135630_users.down.sql new file mode 100644 index 0000000..2482754 --- /dev/null +++ b/crates/database/migrations/20230305135630_users.down.sql @@ -0,0 +1 @@ +drop table if exists users; diff --git a/crates/database/migrations/20230305135630_users.up.sql b/crates/database/migrations/20230305135630_users.up.sql new file mode 100644 index 0000000..6f08d06 --- /dev/null +++ b/crates/database/migrations/20230305135630_users.up.sql @@ -0,0 +1,25 @@ +create table if not exists users +( + id TEXT not null primary key, + created_at TEXT not null default CURRENT_TIMESTAMP, + updated_at TEXT not null default CURRENT_TIMESTAMP, + is_admin INTEGER not null, + username TEXT not null unique, + name TEXT, + email TEXT unique, + password TEXT, + password_recover TEXT, + paper_key TEXT, + is_archived INTEGER not null default 0 +); + +-- update "updated_at" +create trigger if not exists users_updated_at + after update + on users + for each row +begin + update users + set updated_at = CURRENT_TIMESTAMP + where id is NEW.id; +end; diff --git a/crates/database/queries/settings/set_first_admin.sql b/crates/database/queries/settings/set_first_admin.sql new file mode 100644 index 0000000..5adbd6a --- /dev/null +++ b/crates/database/queries/settings/set_first_admin.sql @@ -0,0 +1,5 @@ +update settings + +set first_admin = ? + +where id is 0 diff --git a/crates/database/queries/users/get_initial_admin.sql b/crates/database/queries/users/get_initial_admin.sql new file mode 100644 index 0000000..29875d1 --- /dev/null +++ b/crates/database/queries/users/get_initial_admin.sql @@ -0,0 +1,20 @@ +select u.id, + u.created_at as "created_at: DateTime", + u.updated_at as "updated_at: DateTime", + u.is_admin as "is_admin: bool", + u.username, + u.name, + u.email, + u.password, + u.password_recover, + u.paper_key, + u.is_archived as "is_archived: bool" +from users u + + inner join settings s on u.id = s.first_admin + +where u.is_admin is 1 + and u.is_archived is 0 + and u.id is s.first_admin + +limit 1 \ No newline at end of file diff --git a/crates/database/queries/users/insert.sql b/crates/database/queries/users/insert.sql new file mode 100644 index 0000000..ac93064 --- /dev/null +++ b/crates/database/queries/users/insert.sql @@ -0,0 +1,2 @@ +insert into users (id, is_admin, username, password) +values (?, ?, ?, ?) diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index 859442a..e47ef3f 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -20,6 +20,16 @@ }, "query": "update settings\n\nset business_name = ?\n\nwhere id is 0\n" }, + "520fe30e21f6b6c4d9a47c457675eebd144cf020e9230d154e9e4d0c8d6e01ca": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "insert into users (id, is_admin, username, password)\nvalues (?, ?, ?, ?)\n" + }, "62c75412f673f6a293b0d188d79c50676ec21cf94e2e50e18f9279c91e6b85c8": { "describe": { "columns": [], @@ -30,6 +40,94 @@ }, "query": "insert or ignore into settings(id)\nvalues (0);" }, + "aae93a39c5a9f46235b5ef871b45ba76d7efa1677bfe8291a62b8cbf9cd9e0d5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "update settings\n\nset first_admin = ?\n\nwhere id is 0\n" + }, + "c5a57c971d07532ec0cc897b5ac06e0814e506f9c24647d1eaf44174dc0a5954": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "updated_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "is_admin: bool", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "username", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "email", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "password", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "password_recover", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "paper_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "is_archived: bool", + "ordinal": 10, + "type_info": "Int64" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select u.id,\n u.created_at as \"created_at: DateTime\",\n u.updated_at as \"updated_at: DateTime\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join settings s on u.id = s.first_admin\n\nwhere u.is_admin is 1\n and u.is_archived is 0\n and u.id is s.first_admin\n\nlimit 1" + }, "cc69514c4d9457462e634eb58cbfc82b454197c5cb7f4a451954eb5a421afc3b": { "describe": { "columns": [ diff --git a/crates/database/src/tables.rs b/crates/database/src/tables.rs index a6fdf4c..8458c40 100644 --- a/crates/database/src/tables.rs +++ b/crates/database/src/tables.rs @@ -1,3 +1,5 @@ mod settings; +mod users; pub use settings::Settings; +pub use users::Users; diff --git a/crates/database/src/tables/users.rs b/crates/database/src/tables/users.rs new file mode 100644 index 0000000..6283664 --- /dev/null +++ b/crates/database/src/tables/users.rs @@ -0,0 +1,44 @@ +use crate::error::{handle_error, Error}; +use sqlx::sqlite::SqliteQueryResult; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{FromRow, SqliteExecutor}; + +#[derive(FromRow)] +pub struct Users { + pub id: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_admin: bool, + pub username: String, + pub name: Option, + pub email: Option, + pub password: Option, + pub password_recover: Option, + pub paper_key: Option, + pub is_archived: bool, +} + +impl Users { + pub async fn get_initial_admin(conn: impl SqliteExecutor<'_>) -> Result, Error> { + sqlx::query_file_as!(Self, "queries/users/get_initial_admin.sql") + .fetch_optional(conn) + .await + .map_err(handle_error) + } + + pub async fn insert( + conn: impl SqliteExecutor<'_>, + id: &str, + is_admin: bool, + username: &str, + password: Option<&str>, + ) -> Result, Error> { + let query: SqliteQueryResult = + sqlx::query_file!("queries/users/insert.sql", id, is_admin, username, password) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } +} diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 33d96c2..ffb88e2 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -7,7 +7,6 @@ use chrono::{DateTime, Utc}; pub use crate::database::DEFAULT_BUSINESS_LOGO; pub use crate::error::Error; -// the rest #[derive(Debug)] pub struct Settings { created_at: DateTime, diff --git a/crates/users/Cargo.toml b/crates/users/Cargo.toml new file mode 100644 index 0000000..ba87b9f --- /dev/null +++ b/crates/users/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "users" +version = "0.0.0" +edition = "2021" + +[dependencies] +database = { path = "../database" } +hash = { path = "../hash" } +id = { path = "../id" } +thiserror = { workspace = true } +chrono = { workspace = true } \ No newline at end of file diff --git a/crates/users/src/database.rs b/crates/users/src/database.rs new file mode 100644 index 0000000..b30c964 --- /dev/null +++ b/crates/users/src/database.rs @@ -0,0 +1,45 @@ +use crate::error::Error; +use crate::User; +use database::sqlx::SqliteExecutor; +use database::Users as DatabaseUsers; +use hash::Password; +use id::UserID; + +impl From for User { + fn from(db: DatabaseUsers) -> Self { + Self { + id: UserID(db.id), + created_at: db.created_at, + updated_at: db.updated_at, + is_admin: db.is_admin, + username: db.username, + name: db.name, + email: db.email, + password: db.password, + password_recover: db.password_recover, + paper_key: db.paper_key, + is_archived: db.is_archived, + } + } +} + +impl User { + pub async fn get_initial_admin(conn: impl SqliteExecutor<'_>) -> Result, Error> { + Ok(DatabaseUsers::get_initial_admin(conn) + .await? + .map(Self::from)) + } + + pub async fn insert( + conn: impl SqliteExecutor<'_>, + id: &UserID, + is_admin: bool, + username: &str, + password: Option<&Password>, + ) -> Result, Error> { + Ok( + DatabaseUsers::insert(conn, &id.0, is_admin, username, password.map(|p| p.hash())) + .await?, + ) + } +} diff --git a/crates/users/src/error.rs b/crates/users/src/error.rs new file mode 100644 index 0000000..47def78 --- /dev/null +++ b/crates/users/src/error.rs @@ -0,0 +1,8 @@ +// error +#[derive(thiserror::Error)] +// the rest +#[derive(Debug)] +pub enum Error { + #[error("Database: {0}")] + Database(#[from] database::Error), +} diff --git a/crates/users/src/lib.rs b/crates/users/src/lib.rs new file mode 100644 index 0000000..369ad98 --- /dev/null +++ b/crates/users/src/lib.rs @@ -0,0 +1,22 @@ +mod database; +mod error; + +use chrono::{DateTime, Utc}; +use id::UserID; + +pub use crate::error::Error; + +#[derive(Debug)] +pub struct User { + id: UserID, + created_at: DateTime, + updated_at: DateTime, + is_admin: bool, + username: String, + name: Option, + email: Option, + password: Option, + password_recover: Option, + paper_key: Option, + is_archived: bool, +}