Made application monouser

This commit is contained in:
Manuel Forcén Muñoz 2024-06-03 21:28:34 +02:00
parent 1c1a9589b7
commit d2a020b226
17 changed files with 165 additions and 856 deletions

View file

@ -1,4 +1,3 @@
pub mod routes;
pub mod server;
pub mod static_values;
pub mod users;

View file

@ -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()
}
}

View file

@ -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}"),
),
}
}

View file

@ -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:?}"),
),
}
}

View file

@ -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:?}"),
),
}
}

View file

@ -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),

View file

@ -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) {

View file

@ -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(),

View file

@ -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(), &params.name, &params.description).await {

View file

@ -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 {

View file

@ -1,7 +1,6 @@
use std::net::{AddrParseError, SocketAddr};
use std::sync::Arc;
use axum::headers::ContentType;
use hyper::{header, StatusCode};
use sqlx::SqlitePool;

View file

@ -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\"")],
)),
},
}
}
}