Made application monouser
This commit is contained in:
parent
1c1a9589b7
commit
d2a020b226
17 changed files with 165 additions and 856 deletions
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
let row = sqlx::query("INSERT INTO accounts(user, account_name) VALUES (?,?) RETURNING *")
|
||||
.bind(user)
|
||||
pub async fn new(pool: &SqlitePool, name: &str) -> Result<Self> {
|
||||
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<Vec<Self>> {
|
||||
let rows = sqlx::query("SELECT * FROM accounts WHERE user=?")
|
||||
.bind(user)
|
||||
pub async fn list(pool: &SqlitePool) -> Result<Vec<Self>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Vec<Self>> {
|
||||
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<Self> {
|
||||
sqlx::query("INSERT INTO rules(user, regex, category) VALUES (?,?,?) RETURNING *")
|
||||
.bind(user)
|
||||
pub async fn new(pool: &SqlitePool, regex: String, category: i32) -> sqlx::Result<Self> {
|
||||
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<Vec<Self>> {
|
||||
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<bool, regex::Error> {
|
||||
let re = Regex::new(&self.regex)?;
|
||||
Ok(re.is_match(description))
|
||||
|
|
|
|||
|
|
@ -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<Vec<Self>> {
|
||||
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<Vec<Self>> {
|
||||
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<i32>,
|
||||
after: Option<DateTime<Utc>>,
|
||||
before: Option<DateTime<Utc>>,
|
||||
limit: Option<i32>,
|
||||
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<i32>,
|
||||
after: Option<DateTime<Utc>>,
|
||||
before: Option<DateTime<Utc>>,
|
||||
limit: Option<i32>,
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod routes;
|
||||
pub mod server;
|
||||
pub mod static_values;
|
||||
pub mod users;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Path(id): Path<i32>,
|
||||
) -> (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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Json(account): Json<AccountRequestCreate>,
|
||||
) -> (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<Arc<SqlitePool>>,
|
||||
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<Arc<SqlitePool>>) -> 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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Path(account): Path<i32>,
|
||||
) -> (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}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Json(new_category): Json<CategoryCreateRequest>,
|
||||
) -> (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<Arc<SqlitePool>>, uid: UserToken) -> (StatusCode, String) {
|
||||
pub async fn list(State(db): State<Arc<SqlitePool>>) -> 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:?}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Json(rule): Json<RuleCreateRequest>,
|
||||
) -> (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<Arc<SqlitePool>>, 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<Arc<SqlitePool>>) -> 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:?}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pub async fn list(
|
|||
Path(account): Path<i32>,
|
||||
Query(pagination): Query<PaginationOptions>,
|
||||
) -> (StatusCode, String) {
|
||||
match Transaction::list(
|
||||
match Transaction::list_by_account(
|
||||
db.as_ref(),
|
||||
account,
|
||||
pagination.limit.unwrap_or(100),
|
||||
|
|
|
|||
|
|
@ -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<i32, i64>, collapse: usize) -> Vec<(i32, i64)> {
|
|||
pub async fn index(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<DateTime<Utc>> {
|
|||
pub async fn show(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
uid: UserToken,
|
||||
Path(account_id): Path<i32>,
|
||||
Query(AccountViewParams { from, to }): Query<AccountViewParams>,
|
||||
) -> 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<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
uid: UserToken,
|
||||
Path(account_id): Path<i32>,
|
||||
Query(AccountTxListParams { entries, page }): Query<AccountTxListParams>,
|
||||
) -> 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<i32, String> = 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<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
uid: UserToken,
|
||||
Path(account_id): Path<i32>,
|
||||
) -> 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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Path(account_id): Path<i32>,
|
||||
Json(body): Json<Vec<CreateTransactionRequest>>,
|
||||
) -> impl IntoResponse {
|
||||
// TODO missing user id check
|
||||
for tx in body.iter() {
|
||||
if let Err(e) = Transaction::new(
|
||||
db.as_ref(),
|
||||
|
|
|
|||
|
|
@ -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<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
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<Arc<SqlitePool>>,
|
||||
State(tmpls): State<Arc<Tera>>,
|
||||
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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Form(params): Form<NewRuleParams>,
|
||||
) -> 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<Arc<Tera>>, uid: UserToken) -> impl IntoResponse {
|
||||
pub async fn category_new_view(State(tmpl): State<Arc<Tera>>) -> 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<Arc<SqlitePool>>,
|
||||
uid: UserToken,
|
||||
Form(params): Form<CategoryNewRuleParams>,
|
||||
) -> impl IntoResponse {
|
||||
match Category::new(db.as_ref(), ¶ms.name, ¶ms.description).await {
|
||||
|
|
|
|||
|
|
@ -12,12 +12,9 @@ use serde::{Deserialize, Deserializer};
|
|||
use sqlx::SqlitePool;
|
||||
use tera::Tera;
|
||||
|
||||
use crate::users::UserToken;
|
||||
|
||||
pub async fn view(
|
||||
db: State<Arc<SqlitePool>>,
|
||||
tmpl: State<Arc<Tera>>,
|
||||
user: UserToken,
|
||||
Path(id): Path<i32>,
|
||||
) -> 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<Arc<SqlitePool>>,
|
||||
tmpl: State<Arc<Tera>>,
|
||||
user: UserToken,
|
||||
Path(id): Path<i32>,
|
||||
Form(req): Form<TxUpdateRequest>,
|
||||
) -> impl IntoResponse {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::net::{AddrParseError, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::headers::ContentType;
|
||||
use hyper::{header, StatusCode};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<S> FromRequestParts<S> for UserToken
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, [(&'static str, &'static str); 1]);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
match parts.extract::<TypedHeader<Authorization<Bearer>>>().await {
|
||||
Ok(auth) => Ok(UserToken {
|
||||
user_id: auth.0 .0.token().parse().unwrap(),
|
||||
}),
|
||||
Err(_) => match parts.extract::<TypedHeader<Authorization<Basic>>>().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\"")],
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue