diff --git a/migrations/20231110161954_base.sql b/migrations/20231110161954_base.sql index c3e2cec..a47eda3 100644 --- a/migrations/20231110161954_base.sql +++ b/migrations/20231110161954_base.sql @@ -1,17 +1,8 @@ -- Add migration script here -CREATE TABLE IF NOT EXISTS users( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT, - pass TEXT, - UNIQUE(username) -); - CREATE TABLE IF NOT EXISTS accounts( account_id INTEGER PRIMARY KEY AUTOINCREMENT, - user INTEGER, - account_name TEXT, - FOREIGN KEY (user) REFERENCES users(user_id) + account_name TEXT ); CREATE TABLE IF NOT EXISTS categories ( @@ -22,10 +13,8 @@ CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS rules( rule_id INTEGER PRIMARY KEY AUTOINCREMENT, - user INTEGER, regex TEXT, category INTEGER, - FOREIGN KEY (user) REFERENCES users(user_id) FOREIGN KEY (category) REFERENCES categories(category_id) ); diff --git a/src/models/account.rs b/src/models/account.rs index c219a22..1eacbfb 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -1,4 +1,3 @@ -use chrono::prelude::*; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Result, SqlitePool}; @@ -7,7 +6,6 @@ use super::{rules::Rule, transaction::Transaction}; #[derive(FromRow, Serialize, Deserialize, Debug)] pub struct Account { account_id: i32, - user: i32, account_name: String, } @@ -16,10 +14,6 @@ impl Account { self.account_id } - pub fn get_user(&self) -> i32 { - self.user - } - pub fn get_account_name(&self) -> &str { self.account_name.as_str() } @@ -42,18 +36,16 @@ impl Account { .and_then(|r| Account::from_row(&r)) } - pub async fn new(pool: &SqlitePool, user: i32, name: &str) -> Result { - let row = sqlx::query("INSERT INTO accounts(user, account_name) VALUES (?,?) RETURNING *") - .bind(user) + pub async fn new(pool: &SqlitePool, name: &str) -> Result { + let row = sqlx::query("INSERT INTO accounts(account_name) VALUES (?) RETURNING *") .bind(name) .fetch_one(pool) .await?; Self::from_row(&row) } - pub async fn list(pool: &SqlitePool, user: i32) -> Result> { - let rows = sqlx::query("SELECT * FROM accounts WHERE user=?") - .bind(user) + pub async fn list(pool: &SqlitePool) -> Result> { + let rows = sqlx::query("SELECT * FROM accounts") .fetch_all(pool) .await?; let mut res = Vec::new(); @@ -64,7 +56,7 @@ impl Account { } pub async fn recategorize_transactions(&self, pool: &SqlitePool) -> Result<()> { - let rules = Rule::list_by_user(pool, self.user).await?; + let rules = Rule::list(pool).await?; let mut tx_list = Transaction::list_uncategorized(pool, self.account_id).await?; for tx in tx_list.iter_mut() { println!("Checking {}", tx.get_description()); @@ -84,7 +76,6 @@ impl Account { #[cfg(test)] mod tests { use super::Account; - use crate::models::users::User; use sqlx::SqlitePool; async fn get_db() -> SqlitePool { @@ -96,19 +87,10 @@ mod tests { std::fs::remove_file("account_test.db").unwrap(); } - async fn new_user(pool: &SqlitePool) -> User { - User::create_user(pool, "account_test", "pass") - .await - .unwrap() - } - #[tokio::test] async fn create_test() { let pool = get_db().await; - let user = new_user(&pool).await; - Account::new(&pool, user.get_id(), "account_test") - .await - .unwrap(); + Account::new(&pool, "account_test").await.unwrap(); remove_db(pool).await; } } diff --git a/src/models/rules.rs b/src/models/rules.rs index f9485da..b1c7d8e 100644 --- a/src/models/rules.rs +++ b/src/models/rules.rs @@ -5,7 +5,6 @@ use sqlx::{FromRow, SqlitePool}; #[derive(FromRow, Serialize)] pub struct Rule { pub rule_id: i32, - pub user: i32, pub regex: String, pub category: i32, } @@ -19,27 +18,8 @@ impl Rule { .and_then(|r| Rule::from_row(&r)) } - pub async fn list_by_user(pool: &SqlitePool, user: i32) -> sqlx::Result> { - let mut res = Vec::new(); - for r in sqlx::query("SELECT * FROM rules WHERE user=?") - .bind(user) - .fetch_all(pool) - .await? - .iter() - { - res.push(Rule::from_row(r)?); - } - Ok(res) - } - - pub async fn new( - pool: &SqlitePool, - user: i32, - regex: String, - category: i32, - ) -> sqlx::Result { - sqlx::query("INSERT INTO rules(user, regex, category) VALUES (?,?,?) RETURNING *") - .bind(user) + pub async fn new(pool: &SqlitePool, regex: String, category: i32) -> sqlx::Result { + sqlx::query("INSERT INTO rules(regex, category) VALUES (?,?) RETURNING *") .bind(regex) .bind(category) .fetch_one(pool) @@ -47,6 +27,26 @@ impl Rule { .and_then(|r| Rule::from_row(&r)) } + pub async fn list(pool: &SqlitePool) -> sqlx::Result> { + let mut res = Vec::new(); + for r in sqlx::query("SELECT * FROM rules") + .fetch_all(pool) + .await? + .iter() + { + res.push(Rule::from_row(r)?) + } + Ok(res) + } + + pub async fn delete(&self, pool: &SqlitePool) -> sqlx::Result<()> { + sqlx::query("DELETE FROM rules WHERE rule_id=?") + .bind(self.rule_id) + .execute(pool) + .await + .map(|_| ()) + } + pub fn matches(&self, description: &str) -> Result { let re = Regex::new(&self.regex)?; Ok(re.is_match(description)) diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 78da2c8..6fd354d 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -53,7 +53,25 @@ impl Transaction { .and_then(|x| Transaction::from_row(&x)) } - pub async fn list( + pub async fn list(pool: &SqlitePool, limit: i32, offset: i32, asc: bool) -> Result> { + let rows = sqlx::query(if asc { + "SELECT * FROM transactions ORDER BY tx_date ASC LIMIT ? OFFSET ?" + } else { + "SELECT * FROM transactions ORDER BY tx_date DESC LIMIT ? OFFSET ?" + }) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await?; + + let mut res = Vec::new(); + for r in &rows { + res.push(Transaction::from_row(r)?); + } + Ok(res) + } + + pub async fn list_by_account( pool: &SqlitePool, account: i32, limit: i32, @@ -78,41 +96,19 @@ impl Transaction { Ok(res) } - pub async fn list_by_user( - pool: &SqlitePool, - user: i32, - limit: i32, - offset: i32, - asc: bool, - ) -> Result> { - let rows = sqlx::query( - if asc { - "SELECT t.* FROM transactions t JOIN accounts a ON a.account_id=t.account WHERE a.user=? ORDER BY tx_date ASC LIMIT ? OFFSET ?" - } else { - "SELECT t.* FROM transactions t JOIN accounts a ON a.account_id=t.account WHERE a.user=? ORDER BY tx_date DESC LIMIT ? OFFSET ?" - } - ).bind(user) - .bind(limit) - .bind(offset) - .fetch_all(pool) - .await?; - - let mut res = Vec::new(); - for r in &rows { - res.push(Transaction::from_row(r)?); - } - Ok(res) - } - pub fn query_by_date<'a>( - account: i32, + account: Option, after: Option>, before: Option>, limit: Option, asc: bool, ) -> sqlx::QueryBuilder<'a, Sqlite> { - let mut query = sqlx::QueryBuilder::new("SELECT * FROM TRANSACTIONS WHERE account="); - query.push_bind(account); + let mut query = sqlx::QueryBuilder::new("SELECT * FROM transactions WHERE TRUE "); + + if let Some(acc) = account { + query.push(" AND account="); + query.push_bind(acc); + } if let Some(after) = after { query.push(" AND tx_date >= "); @@ -140,7 +136,7 @@ impl Transaction { pub async fn list_by_date( pool: &SqlitePool, - account: i32, + account: Option, after: Option>, before: Option>, limit: Option, @@ -284,7 +280,7 @@ impl Transaction { #[cfg(test)] mod tests { use super::Transaction; - use crate::models::{account::Account, users::User}; + use crate::models::account::Account; use sqlx::SqlitePool; async fn get_db() -> SqlitePool { @@ -296,15 +292,10 @@ mod tests { std::fs::remove_file("tx_test.db").unwrap(); } - async fn new_user(pool: &SqlitePool) -> User { - User::create_user(pool, "testuser", "pass").await.unwrap() - } - #[tokio::test] async fn create_test() { let pool = get_db().await; - let user = new_user(&pool).await; - let acc = Account::new(&pool, user.get_id(), "tx_test").await.unwrap(); + let acc = Account::new(&pool, "tx_test").await.unwrap(); let tx = Transaction::new( &pool, acc.get_id(), diff --git a/static/styles.css b/static/styles.css deleted file mode 100644 index b072356..0000000 --- a/static/styles.css +++ /dev/null @@ -1,586 +0,0 @@ -/* -! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e5e7eb; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -6. Use the user's configured `sans` font-variation-settings by default. -7. Disable tap highlights on iOS -*/ - -html, -:host { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - /* 4 */ - font-feature-settings: normal; - /* 5 */ - font-variation-settings: normal; - /* 6 */ - -webkit-tap-highlight-color: transparent; - /* 7 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font-family by default. -2. Use the user's configured `mono` font-feature-settings by default. -3. Use the user's configured `mono` font-variation-settings by default. -4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-feature-settings: normal; - /* 2 */ - font-variation-settings: normal; - /* 3 */ - font-size: 1em; - /* 4 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-feature-settings: inherit; - /* 1 */ - font-variation-settings: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - letter-spacing: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Reset default styling for dialogs. -*/ - -dialog { - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ - -[hidden] { - display: none; -} - -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -.ars-input { - margin: 0.25rem; - margin-bottom: 0.75rem; -} - -.ars-input input { - width: 100%; - padding: 0.25rem 0.75rem; - border: solid 1px #57534e; - border-radius: 0.5rem; -} - -.ars-input select { - width: 100%; - padding: 0.25rem 0.75rem; - border: solid 1px #57534e; - border-radius: 0.5rem; - background-color: transparent; -} - -.ars-button { - padding: 0.25rem 0.75rem; - border: solid 1px #57534e; - border-radius: 0.5rem; -} - -.ars-button:hover { - background: #57534e77; - cursor: pointer; -} diff --git a/webserver/src/lib.rs b/webserver/src/lib.rs index bdb141f..6cd859a 100644 --- a/webserver/src/lib.rs +++ b/webserver/src/lib.rs @@ -1,4 +1,3 @@ pub mod routes; pub mod server; pub mod static_values; -pub mod users; diff --git a/webserver/src/main.rs b/webserver/src/main.rs index 7069736..1e12bc8 100644 --- a/webserver/src/main.rs +++ b/webserver/src/main.rs @@ -1,7 +1,6 @@ mod routes; mod server; mod static_values; -mod users; const DB_URL: &str = "sqlite://sqlite.db"; @@ -9,7 +8,7 @@ const DB_URL: &str = "sqlite://sqlite.db"; async fn main() { let server = server::start_server("127.0.0.1:3000", DB_URL); - /*let wv_task = tokio::task::spawn_blocking(|| { + let wv_task = tokio::task::spawn_blocking(|| { web_view::builder() .title("Test") .content(web_view::Content::Url("http://localhost:3000")) @@ -27,6 +26,5 @@ async fn main() { e = server => { println!("Axum finished with result {e:?}"); } - }*/ - server.await.unwrap() + } } diff --git a/webserver/src/routes/api/accounts.rs b/webserver/src/routes/api/accounts.rs index b66c4f7..11c4e03 100644 --- a/webserver/src/routes/api/accounts.rs +++ b/webserver/src/routes/api/accounts.rs @@ -1,27 +1,30 @@ use std::sync::Arc; -use axum::extract::{Json, Path, State}; -use hyper::StatusCode; +use axum::{ + extract::{Json, Path, State}, + response::IntoResponse, +}; +use hyper::{header::CONTENT_TYPE, StatusCode}; use serde::Deserialize; use sqlx::SqlitePool; -use crate::users::UserToken; use accounters::models::account::Account; pub async fn account_get( State(db): State>, - uid: UserToken, Path(id): Path, -) -> (StatusCode, String) { +) -> impl IntoResponse { match Account::get_by_id(db.as_ref(), id).await { - Ok(a) => { - if a.get_user() == uid.user_id { - (StatusCode::OK, serde_json::to_string(&a).unwrap()) - } else { - (StatusCode::UNAUTHORIZED, String::new()) - } - } - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), + Ok(a) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&a).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "plain/text")], + format!("{e}"), + ), } } @@ -32,37 +35,53 @@ pub struct AccountRequestCreate { pub async fn account_create( State(db): State>, - uid: UserToken, Json(account): Json, -) -> (StatusCode, String) { - match Account::new(db.as_ref(), uid.user_id, &account.name).await { - Ok(a) => (StatusCode::OK, serde_json::to_string(&a).unwrap()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), +) -> impl IntoResponse { + match Account::new(db.as_ref(), &account.name).await { + Ok(a) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&a).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e}"), + ), } } -pub async fn account_list( - State(db): State>, - uid: UserToken, -) -> (StatusCode, String) { - match Account::list(db.as_ref(), uid.user_id).await { - Ok(a) => (StatusCode::OK, serde_json::to_string(&a).unwrap()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), +pub async fn account_list(State(db): State>) -> impl IntoResponse { + match Account::list(db.as_ref()).await { + Ok(a) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&a).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e}"), + ), } } pub async fn recategorize( State(db): State>, - uid: UserToken, Path(account): Path, -) -> (StatusCode, String) { +) -> impl IntoResponse { let account = Account::get_by_id(db.as_ref(), account).await.unwrap(); - if account.get_user() != uid.user_id { - return (StatusCode::UNAUTHORIZED, String::new()); - } match account.recategorize_transactions(db.as_ref()).await { - Ok(_) => (StatusCode::OK, String::new()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), + Ok(_) => ( + StatusCode::OK, + [(CONTENT_TYPE, "text/plain")], + String::new(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e}"), + ), } } diff --git a/webserver/src/routes/api/categories.rs b/webserver/src/routes/api/categories.rs index c0b37be..2cc080e 100644 --- a/webserver/src/routes/api/categories.rs +++ b/webserver/src/routes/api/categories.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use axum::{extract::State, Json}; -use hyper::StatusCode; +use axum::{extract::State, response::IntoResponse, Json}; +use hyper::{header::CONTENT_TYPE, StatusCode}; use serde::Deserialize; use sqlx::SqlitePool; -use crate::users::UserToken; use accounters::models::categories::Category; #[derive(Deserialize)] @@ -16,18 +15,25 @@ pub struct CategoryCreateRequest { pub async fn create( State(db): State>, - uid: UserToken, Json(new_category): Json, -) -> (StatusCode, String) { +) -> impl IntoResponse { match Category::new(db.as_ref(), &new_category.name, &new_category.description).await { Ok(_) => (StatusCode::OK, String::new()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), } } -pub async fn list(State(db): State>, uid: UserToken) -> (StatusCode, String) { +pub async fn list(State(db): State>) -> impl IntoResponse { match Category::list(db.as_ref()).await { - Ok(c) => (StatusCode::OK, serde_json::to_string(&c).unwrap()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), + Ok(c) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&c).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e:?}"), + ), } } diff --git a/webserver/src/routes/api/rules.rs b/webserver/src/routes/api/rules.rs index 303c675..78e002e 100644 --- a/webserver/src/routes/api/rules.rs +++ b/webserver/src/routes/api/rules.rs @@ -1,11 +1,13 @@ use std::sync::Arc; -use axum::extract::{Json, State}; -use hyper::StatusCode; +use axum::{ + extract::{Json, State}, + response::IntoResponse, +}; +use hyper::{header::CONTENT_TYPE, StatusCode}; use serde::Deserialize; use sqlx::SqlitePool; -use crate::users::UserToken; use accounters::models::rules::Rule; #[derive(Deserialize)] @@ -16,18 +18,33 @@ pub struct RuleCreateRequest { pub async fn create( State(db): State>, - uid: UserToken, Json(rule): Json, -) -> (StatusCode, String) { - match Rule::new(db.as_ref(), uid.user_id, rule.regex, rule.category).await { - Ok(r) => (StatusCode::OK, serde_json::to_string(&r).unwrap()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), +) -> impl IntoResponse { + match Rule::new(db.as_ref(), rule.regex, rule.category).await { + Ok(r) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&r).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e:?}"), + ), } } -pub async fn list(State(db): State>, uid: UserToken) -> (StatusCode, String) { - match Rule::list_by_user(db.as_ref(), uid.user_id).await { - Ok(rule_list) => (StatusCode::OK, serde_json::to_string(&rule_list).unwrap()), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), +pub async fn list(State(db): State>) -> impl IntoResponse { + match Rule::list(db.as_ref()).await { + Ok(rule_list) => ( + StatusCode::OK, + [(CONTENT_TYPE, "application/json")], + serde_json::to_string(&rule_list).unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e:?}"), + ), } } diff --git a/webserver/src/routes/api/transactions.rs b/webserver/src/routes/api/transactions.rs index bba8802..006c93e 100644 --- a/webserver/src/routes/api/transactions.rs +++ b/webserver/src/routes/api/transactions.rs @@ -47,7 +47,7 @@ pub async fn list( Path(account): Path, Query(pagination): Query, ) -> (StatusCode, String) { - match Transaction::list( + match Transaction::list_by_account( db.as_ref(), account, pagination.limit.unwrap_or(100), diff --git a/webserver/src/routes/ui.rs b/webserver/src/routes/ui.rs index cb61e2c..8a79c4a 100644 --- a/webserver/src/routes/ui.rs +++ b/webserver/src/routes/ui.rs @@ -7,7 +7,6 @@ use serde::Serialize; use sqlx::SqlitePool; use tera::{Context, Tera}; -use crate::users::UserToken; use accounters::models::{account::Account, categories::Category, transaction::Transaction}; pub mod account; @@ -23,7 +22,7 @@ struct AccountRender { impl AccountRender { async fn from_account(pool: &SqlitePool, acc: Account) -> Self { - let last_acc = Transaction::list(pool, acc.get_id(), 1, 0, false) + let last_acc = Transaction::list_by_account(pool, acc.get_id(), 1, 0, false) .await .map_or(0.0, |x| { x.get(0) @@ -54,11 +53,10 @@ fn hm_sort(hm: HashMap, collapse: usize) -> Vec<(i32, i64)> { pub async fn index( State(db): State>, State(tmpls): State>, - uid: UserToken, ) -> impl IntoResponse { let mut ctx = Context::new(); - let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap(); + let accounts = Account::list(db.as_ref()).await.unwrap(); let mut acc_render = Vec::new(); for acc in accounts.into_iter() { @@ -69,7 +67,7 @@ pub async fn index( let last_month = Transaction::list_by_date( db.as_ref(), - uid.user_id, + None, Some(Utc::now() - chrono::Duration::days(30)), Some(Utc::now()), None, @@ -119,9 +117,7 @@ pub async fn index( ctx.insert("colors", &colors); - let transactions = Transaction::list_by_user(db.as_ref(), uid.user_id, 10, 0, false) - .await - .unwrap(); + let transactions = Transaction::list(db.as_ref(), 10, 0, false).await.unwrap(); ctx.insert("transactions", &transactions); match tmpls.render("index.html", &ctx) { diff --git a/webserver/src/routes/ui/account.rs b/webserver/src/routes/ui/account.rs index 1b5e221..bae7d2c 100644 --- a/webserver/src/routes/ui/account.rs +++ b/webserver/src/routes/ui/account.rs @@ -11,7 +11,6 @@ use serde::Deserialize; use sqlx::SqlitePool; use tera::{Context, Tera}; -use crate::users::UserToken; use accounters::models::{account::Account, categories::Category, transaction::Transaction}; #[derive(Deserialize)] @@ -31,7 +30,6 @@ fn parse_date(s: &str) -> Option> { pub async fn show( State(db): State>, State(tmpls): State>, - uid: UserToken, Path(account_id): Path, Query(AccountViewParams { from, to }): Query, ) -> impl IntoResponse { @@ -48,14 +46,6 @@ pub async fn show( } }; - if account.get_user() != uid.user_id { - return ( - StatusCode::UNAUTHORIZED, - [(CONTENT_TYPE, "text/plain")], - String::from("You cannot access this resource"), - ); - } - let from = from .and_then(|x| parse_date(&x)) .unwrap_or(Utc::now().duration_trunc(Duration::days(1)).unwrap() - Duration::days(30)); @@ -80,7 +70,8 @@ pub async fn show( .collect(); ctx.insert("categories", &categories); - let txs = match Transaction::list(db.as_ref(), account.get_id(), 10, 0, false).await { + let txs = match Transaction::list_by_account(db.as_ref(), account.get_id(), 10, 0, false).await + { Ok(t) => t, Err(e) => { return ( @@ -109,7 +100,6 @@ pub struct AccountTxListParams { pub async fn list_transactions( State(db): State>, State(tmpls): State>, - uid: UserToken, Path(account_id): Path, Query(AccountTxListParams { entries, page }): Query, ) -> impl IntoResponse { @@ -126,14 +116,6 @@ pub async fn list_transactions( } }; - if account.get_user() != uid.user_id { - return ( - StatusCode::UNAUTHORIZED, - [(CONTENT_TYPE, "text/plain")], - String::from("You cannot access this resource"), - ); - } - let categories: HashMap = Category::list(db.as_ref()) .await .unwrap() @@ -145,7 +127,7 @@ pub async fn list_transactions( let n_entries = entries.unwrap_or(10).max(10); let page = page.unwrap_or(0).max(0); - let txs = match Transaction::list( + let txs = match Transaction::list_by_account( db.as_ref(), account.get_id(), n_entries, @@ -181,7 +163,6 @@ pub async fn list_transactions( pub async fn add_transactions_view( State(db): State>, State(tmpls): State>, - uid: UserToken, Path(account_id): Path, ) -> impl IntoResponse { let mut ctxt = Context::new(); @@ -198,14 +179,6 @@ pub async fn add_transactions_view( } }; - if account.get_user() != uid.user_id { - return ( - StatusCode::UNAUTHORIZED, - [(CONTENT_TYPE, "text/plain")], - String::from("You cannot access this resource"), - ); - } - ctxt.insert("account", &account); ( @@ -224,11 +197,9 @@ pub struct CreateTransactionRequest { pub async fn add_transactions_action( State(db): State>, - uid: UserToken, Path(account_id): Path, Json(body): Json>, ) -> impl IntoResponse { - // TODO missing user id check for tx in body.iter() { if let Err(e) = Transaction::new( db.as_ref(), diff --git a/webserver/src/routes/ui/classifier.rs b/webserver/src/routes/ui/classifier.rs index 89ec53d..a57df02 100644 --- a/webserver/src/routes/ui/classifier.rs +++ b/webserver/src/routes/ui/classifier.rs @@ -13,14 +13,11 @@ use serde::Deserialize; use sqlx::SqlitePool; use tera::{Context, Tera}; -use crate::users::UserToken; - pub async fn view_classifiers( State(db): State>, State(tmpls): State>, - uid: UserToken, ) -> impl IntoResponse { - let rules = match Rule::list_by_user(db.as_ref(), uid.user_id).await { + let rules = match Rule::list(db.as_ref()).await { Ok(r) => r, Err(e) => { return ( @@ -57,7 +54,6 @@ pub async fn view_classifiers( pub async fn rules_new_view( State(db): State>, State(tmpls): State>, - uid: UserToken, ) -> impl IntoResponse { let categories = Category::list(db.as_ref()).await.unwrap(); let mut ctx = Context::new(); @@ -78,10 +74,9 @@ pub struct NewRuleParams { pub async fn rules_new_action( State(db): State>, - uid: UserToken, Form(params): Form, ) -> impl IntoResponse { - match Rule::new(db.as_ref(), uid.user_id, params.regex, params.category).await { + match Rule::new(db.as_ref(), params.regex, params.category).await { Ok(_) => ( StatusCode::MOVED_PERMANENTLY, [(LOCATION, "/classifiers")], @@ -95,7 +90,7 @@ pub async fn rules_new_action( } } -pub async fn category_new_view(State(tmpl): State>, uid: UserToken) -> impl IntoResponse { +pub async fn category_new_view(State(tmpl): State>) -> impl IntoResponse { ( StatusCode::OK, [(CONTENT_TYPE, "text/html;charset=utf-8")], @@ -111,7 +106,6 @@ pub struct CategoryNewRuleParams { pub async fn category_new_action( State(db): State>, - uid: UserToken, Form(params): Form, ) -> impl IntoResponse { match Category::new(db.as_ref(), ¶ms.name, ¶ms.description).await { diff --git a/webserver/src/routes/ui/transaction.rs b/webserver/src/routes/ui/transaction.rs index e9f7f90..eb49f1e 100644 --- a/webserver/src/routes/ui/transaction.rs +++ b/webserver/src/routes/ui/transaction.rs @@ -12,12 +12,9 @@ use serde::{Deserialize, Deserializer}; use sqlx::SqlitePool; use tera::Tera; -use crate::users::UserToken; - pub async fn view( db: State>, tmpl: State>, - user: UserToken, Path(id): Path, ) -> impl IntoResponse { let tx = Transaction::get_by_id(db.as_ref(), id).await.unwrap(); @@ -58,8 +55,6 @@ pub struct TxUpdateRequest { pub async fn update( db: State>, - tmpl: State>, - user: UserToken, Path(id): Path, Form(req): Form, ) -> impl IntoResponse { diff --git a/webserver/src/server.rs b/webserver/src/server.rs index 27d3210..45977dd 100644 --- a/webserver/src/server.rs +++ b/webserver/src/server.rs @@ -1,7 +1,6 @@ use std::net::{AddrParseError, SocketAddr}; use std::sync::Arc; -use axum::headers::ContentType; use hyper::{header, StatusCode}; use sqlx::SqlitePool; diff --git a/webserver/src/users.rs b/webserver/src/users.rs deleted file mode 100644 index 72c1fa4..0000000 --- a/webserver/src/users.rs +++ /dev/null @@ -1,61 +0,0 @@ -use accounters::models::users::User; -use axum::{ - async_trait, - extract::{FromRef, FromRequestParts}, - headers::authorization::{Basic, Bearer}, - headers::Authorization, - http::request::Parts, - response::{IntoResponse, Redirect}, - RequestPartsExt, TypedHeader, -}; -use hyper::StatusCode; - -use crate::server::AppState; - -pub struct AuthRedirect; - -impl IntoResponse for AuthRedirect { - fn into_response(self) -> axum::response::Response { - Redirect::temporary("/login").into_response() - } -} - -pub struct UserToken { - pub user_id: i32, -} - -#[async_trait] -impl FromRequestParts for UserToken -where - AppState: FromRef, - S: Send + Sync, -{ - type Rejection = (StatusCode, [(&'static str, &'static str); 1]); - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - match parts.extract::>>().await { - Ok(auth) => Ok(UserToken { - user_id: auth.0 .0.token().parse().unwrap(), - }), - Err(_) => match parts.extract::>>().await { - Ok(auth) => { - let state = AppState::from_ref(state); - let user = User::get_user(state.db.as_ref(), auth.username()) - .await - .unwrap(); - if user.check_pass(auth.password()) { - Ok(UserToken { - user_id: user.get_id(), - }) - } else { - Err((StatusCode::UNAUTHORIZED, [("", "")])) - } - } - Err(_) => Err(( - StatusCode::UNAUTHORIZED, - [("WWW-Authenticate", "Basic realm=\"Access\"")], - )), - }, - } - } -}