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,17 +1,8 @@
-- Add migration script here -- 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( CREATE TABLE IF NOT EXISTS accounts(
account_id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER PRIMARY KEY AUTOINCREMENT,
user INTEGER, account_name TEXT
account_name TEXT,
FOREIGN KEY (user) REFERENCES users(user_id)
); );
CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS categories (
@ -22,10 +13,8 @@ CREATE TABLE IF NOT EXISTS categories (
CREATE TABLE IF NOT EXISTS rules( CREATE TABLE IF NOT EXISTS rules(
rule_id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id INTEGER PRIMARY KEY AUTOINCREMENT,
user INTEGER,
regex TEXT, regex TEXT,
category INTEGER, category INTEGER,
FOREIGN KEY (user) REFERENCES users(user_id)
FOREIGN KEY (category) REFERENCES categories(category_id) FOREIGN KEY (category) REFERENCES categories(category_id)
); );

View file

@ -1,4 +1,3 @@
use chrono::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Result, SqlitePool}; use sqlx::{FromRow, Result, SqlitePool};
@ -7,7 +6,6 @@ use super::{rules::Rule, transaction::Transaction};
#[derive(FromRow, Serialize, Deserialize, Debug)] #[derive(FromRow, Serialize, Deserialize, Debug)]
pub struct Account { pub struct Account {
account_id: i32, account_id: i32,
user: i32,
account_name: String, account_name: String,
} }
@ -16,10 +14,6 @@ impl Account {
self.account_id self.account_id
} }
pub fn get_user(&self) -> i32 {
self.user
}
pub fn get_account_name(&self) -> &str { pub fn get_account_name(&self) -> &str {
self.account_name.as_str() self.account_name.as_str()
} }
@ -42,18 +36,16 @@ impl Account {
.and_then(|r| Account::from_row(&r)) .and_then(|r| Account::from_row(&r))
} }
pub async fn new(pool: &SqlitePool, user: i32, name: &str) -> Result<Self> { pub async fn new(pool: &SqlitePool, name: &str) -> Result<Self> {
let row = sqlx::query("INSERT INTO accounts(user, account_name) VALUES (?,?) RETURNING *") let row = sqlx::query("INSERT INTO accounts(account_name) VALUES (?) RETURNING *")
.bind(user)
.bind(name) .bind(name)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
Self::from_row(&row) Self::from_row(&row)
} }
pub async fn list(pool: &SqlitePool, user: i32) -> Result<Vec<Self>> { pub async fn list(pool: &SqlitePool) -> Result<Vec<Self>> {
let rows = sqlx::query("SELECT * FROM accounts WHERE user=?") let rows = sqlx::query("SELECT * FROM accounts")
.bind(user)
.fetch_all(pool) .fetch_all(pool)
.await?; .await?;
let mut res = Vec::new(); let mut res = Vec::new();
@ -64,7 +56,7 @@ impl Account {
} }
pub async fn recategorize_transactions(&self, pool: &SqlitePool) -> Result<()> { 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?; let mut tx_list = Transaction::list_uncategorized(pool, self.account_id).await?;
for tx in tx_list.iter_mut() { for tx in tx_list.iter_mut() {
println!("Checking {}", tx.get_description()); println!("Checking {}", tx.get_description());
@ -84,7 +76,6 @@ impl Account {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Account; use super::Account;
use crate::models::users::User;
use sqlx::SqlitePool; use sqlx::SqlitePool;
async fn get_db() -> SqlitePool { async fn get_db() -> SqlitePool {
@ -96,19 +87,10 @@ mod tests {
std::fs::remove_file("account_test.db").unwrap(); 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] #[tokio::test]
async fn create_test() { async fn create_test() {
let pool = get_db().await; let pool = get_db().await;
let user = new_user(&pool).await; Account::new(&pool, "account_test").await.unwrap();
Account::new(&pool, user.get_id(), "account_test")
.await
.unwrap();
remove_db(pool).await; remove_db(pool).await;
} }
} }

View file

@ -5,7 +5,6 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize)] #[derive(FromRow, Serialize)]
pub struct Rule { pub struct Rule {
pub rule_id: i32, pub rule_id: i32,
pub user: i32,
pub regex: String, pub regex: String,
pub category: i32, pub category: i32,
} }
@ -19,27 +18,8 @@ impl Rule {
.and_then(|r| Rule::from_row(&r)) .and_then(|r| Rule::from_row(&r))
} }
pub async fn list_by_user(pool: &SqlitePool, user: i32) -> sqlx::Result<Vec<Self>> { pub async fn new(pool: &SqlitePool, regex: String, category: i32) -> sqlx::Result<Self> {
let mut res = Vec::new(); sqlx::query("INSERT INTO rules(regex, category) VALUES (?,?) RETURNING *")
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)
.bind(regex) .bind(regex)
.bind(category) .bind(category)
.fetch_one(pool) .fetch_one(pool)
@ -47,6 +27,26 @@ impl Rule {
.and_then(|r| Rule::from_row(&r)) .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> { pub fn matches(&self, description: &str) -> Result<bool, regex::Error> {
let re = Regex::new(&self.regex)?; let re = Regex::new(&self.regex)?;
Ok(re.is_match(description)) Ok(re.is_match(description))

View file

@ -53,7 +53,25 @@ impl Transaction {
.and_then(|x| Transaction::from_row(&x)) .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, pool: &SqlitePool,
account: i32, account: i32,
limit: i32, limit: i32,
@ -78,41 +96,19 @@ impl Transaction {
Ok(res) 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>( pub fn query_by_date<'a>(
account: i32, account: Option<i32>,
after: Option<DateTime<Utc>>, after: Option<DateTime<Utc>>,
before: Option<DateTime<Utc>>, before: Option<DateTime<Utc>>,
limit: Option<i32>, limit: Option<i32>,
asc: bool, asc: bool,
) -> sqlx::QueryBuilder<'a, Sqlite> { ) -> sqlx::QueryBuilder<'a, Sqlite> {
let mut query = sqlx::QueryBuilder::new("SELECT * FROM TRANSACTIONS WHERE account="); let mut query = sqlx::QueryBuilder::new("SELECT * FROM transactions WHERE TRUE ");
query.push_bind(account);
if let Some(acc) = account {
query.push(" AND account=");
query.push_bind(acc);
}
if let Some(after) = after { if let Some(after) = after {
query.push(" AND tx_date >= "); query.push(" AND tx_date >= ");
@ -140,7 +136,7 @@ impl Transaction {
pub async fn list_by_date( pub async fn list_by_date(
pool: &SqlitePool, pool: &SqlitePool,
account: i32, account: Option<i32>,
after: Option<DateTime<Utc>>, after: Option<DateTime<Utc>>,
before: Option<DateTime<Utc>>, before: Option<DateTime<Utc>>,
limit: Option<i32>, limit: Option<i32>,
@ -284,7 +280,7 @@ impl Transaction {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Transaction; use super::Transaction;
use crate::models::{account::Account, users::User}; use crate::models::account::Account;
use sqlx::SqlitePool; use sqlx::SqlitePool;
async fn get_db() -> SqlitePool { async fn get_db() -> SqlitePool {
@ -296,15 +292,10 @@ mod tests {
std::fs::remove_file("tx_test.db").unwrap(); 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] #[tokio::test]
async fn create_test() { async fn create_test() {
let pool = get_db().await; let pool = get_db().await;
let user = new_user(&pool).await; let acc = Account::new(&pool, "tx_test").await.unwrap();
let acc = Account::new(&pool, user.get_id(), "tx_test").await.unwrap();
let tx = Transaction::new( let tx = Transaction::new(
&pool, &pool,
acc.get_id(), acc.get_id(),

View file

@ -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;
}

View file

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

View file

@ -1,7 +1,6 @@
mod routes; mod routes;
mod server; mod server;
mod static_values; mod static_values;
mod users;
const DB_URL: &str = "sqlite://sqlite.db"; const DB_URL: &str = "sqlite://sqlite.db";
@ -9,7 +8,7 @@ const DB_URL: &str = "sqlite://sqlite.db";
async fn main() { async fn main() {
let server = server::start_server("127.0.0.1:3000", DB_URL); 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() web_view::builder()
.title("Test") .title("Test")
.content(web_view::Content::Url("http://localhost:3000")) .content(web_view::Content::Url("http://localhost:3000"))
@ -27,6 +26,5 @@ async fn main() {
e = server => { e = server => {
println!("Axum finished with result {e:?}"); println!("Axum finished with result {e:?}");
} }
}*/ }
server.await.unwrap()
} }

View file

@ -1,27 +1,30 @@
use std::sync::Arc; use std::sync::Arc;
use axum::extract::{Json, Path, State}; use axum::{
use hyper::StatusCode; extract::{Json, Path, State},
response::IntoResponse,
};
use hyper::{header::CONTENT_TYPE, StatusCode};
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::users::UserToken;
use accounters::models::account::Account; use accounters::models::account::Account;
pub async fn account_get( pub async fn account_get(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Path(id): Path<i32>, Path(id): Path<i32>,
) -> (StatusCode, String) { ) -> impl IntoResponse {
match Account::get_by_id(db.as_ref(), id).await { match Account::get_by_id(db.as_ref(), id).await {
Ok(a) => { Ok(a) => (
if a.get_user() == uid.user_id { StatusCode::OK,
(StatusCode::OK, serde_json::to_string(&a).unwrap()) [(CONTENT_TYPE, "application/json")],
} else { serde_json::to_string(&a).unwrap(),
(StatusCode::UNAUTHORIZED, String::new()) ),
} Err(e) => (
} StatusCode::INTERNAL_SERVER_ERROR,
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), [(CONTENT_TYPE, "plain/text")],
format!("{e}"),
),
} }
} }
@ -32,37 +35,53 @@ pub struct AccountRequestCreate {
pub async fn account_create( pub async fn account_create(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Json(account): Json<AccountRequestCreate>, Json(account): Json<AccountRequestCreate>,
) -> (StatusCode, String) { ) -> impl IntoResponse {
match Account::new(db.as_ref(), uid.user_id, &account.name).await { match Account::new(db.as_ref(), &account.name).await {
Ok(a) => (StatusCode::OK, serde_json::to_string(&a).unwrap()), Ok(a) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), 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( pub async fn account_list(State(db): State<Arc<SqlitePool>>) -> impl IntoResponse {
State(db): State<Arc<SqlitePool>>, match Account::list(db.as_ref()).await {
uid: UserToken, Ok(a) => (
) -> (StatusCode, String) { StatusCode::OK,
match Account::list(db.as_ref(), uid.user_id).await { [(CONTENT_TYPE, "application/json")],
Ok(a) => (StatusCode::OK, serde_json::to_string(&a).unwrap()), serde_json::to_string(&a).unwrap(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), ),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(CONTENT_TYPE, "text/plain")],
format!("{e}"),
),
} }
} }
pub async fn recategorize( pub async fn recategorize(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Path(account): Path<i32>, Path(account): Path<i32>,
) -> (StatusCode, String) { ) -> impl IntoResponse {
let account = Account::get_by_id(db.as_ref(), account).await.unwrap(); 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 { match account.recategorize_transactions(db.as_ref()).await {
Ok(_) => (StatusCode::OK, String::new()), Ok(_) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), 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 std::sync::Arc;
use axum::{extract::State, Json}; use axum::{extract::State, response::IntoResponse, Json};
use hyper::StatusCode; use hyper::{header::CONTENT_TYPE, StatusCode};
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::users::UserToken;
use accounters::models::categories::Category; use accounters::models::categories::Category;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -16,18 +15,25 @@ pub struct CategoryCreateRequest {
pub async fn create( pub async fn create(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Json(new_category): Json<CategoryCreateRequest>, Json(new_category): Json<CategoryCreateRequest>,
) -> (StatusCode, String) { ) -> impl IntoResponse {
match Category::new(db.as_ref(), &new_category.name, &new_category.description).await { match Category::new(db.as_ref(), &new_category.name, &new_category.description).await {
Ok(_) => (StatusCode::OK, String::new()), Ok(_) => (StatusCode::OK, String::new()),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), 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 { match Category::list(db.as_ref()).await {
Ok(c) => (StatusCode::OK, serde_json::to_string(&c).unwrap()), Ok(c) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), 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 std::sync::Arc;
use axum::extract::{Json, State}; use axum::{
use hyper::StatusCode; extract::{Json, State},
response::IntoResponse,
};
use hyper::{header::CONTENT_TYPE, StatusCode};
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::users::UserToken;
use accounters::models::rules::Rule; use accounters::models::rules::Rule;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -16,18 +18,33 @@ pub struct RuleCreateRequest {
pub async fn create( pub async fn create(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Json(rule): Json<RuleCreateRequest>, Json(rule): Json<RuleCreateRequest>,
) -> (StatusCode, String) { ) -> impl IntoResponse {
match Rule::new(db.as_ref(), uid.user_id, rule.regex, rule.category).await { match Rule::new(db.as_ref(), rule.regex, rule.category).await {
Ok(r) => (StatusCode::OK, serde_json::to_string(&r).unwrap()), Ok(r) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), 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) { pub async fn list(State(db): State<Arc<SqlitePool>>) -> impl IntoResponse {
match Rule::list_by_user(db.as_ref(), uid.user_id).await { match Rule::list(db.as_ref()).await {
Ok(rule_list) => (StatusCode::OK, serde_json::to_string(&rule_list).unwrap()), Ok(rule_list) => (
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")), 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>, Path(account): Path<i32>,
Query(pagination): Query<PaginationOptions>, Query(pagination): Query<PaginationOptions>,
) -> (StatusCode, String) { ) -> (StatusCode, String) {
match Transaction::list( match Transaction::list_by_account(
db.as_ref(), db.as_ref(),
account, account,
pagination.limit.unwrap_or(100), pagination.limit.unwrap_or(100),

View file

@ -7,7 +7,6 @@ use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::{Context, Tera}; use tera::{Context, Tera};
use crate::users::UserToken;
use accounters::models::{account::Account, categories::Category, transaction::Transaction}; use accounters::models::{account::Account, categories::Category, transaction::Transaction};
pub mod account; pub mod account;
@ -23,7 +22,7 @@ struct AccountRender {
impl AccountRender { impl AccountRender {
async fn from_account(pool: &SqlitePool, acc: Account) -> Self { 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 .await
.map_or(0.0, |x| { .map_or(0.0, |x| {
x.get(0) x.get(0)
@ -54,11 +53,10 @@ fn hm_sort(hm: HashMap<i32, i64>, collapse: usize) -> Vec<(i32, i64)> {
pub async fn index( pub async fn index(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut ctx = Context::new(); 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(); let mut acc_render = Vec::new();
for acc in accounts.into_iter() { for acc in accounts.into_iter() {
@ -69,7 +67,7 @@ pub async fn index(
let last_month = Transaction::list_by_date( let last_month = Transaction::list_by_date(
db.as_ref(), db.as_ref(),
uid.user_id, None,
Some(Utc::now() - chrono::Duration::days(30)), Some(Utc::now() - chrono::Duration::days(30)),
Some(Utc::now()), Some(Utc::now()),
None, None,
@ -119,9 +117,7 @@ pub async fn index(
ctx.insert("colors", &colors); ctx.insert("colors", &colors);
let transactions = Transaction::list_by_user(db.as_ref(), uid.user_id, 10, 0, false) let transactions = Transaction::list(db.as_ref(), 10, 0, false).await.unwrap();
.await
.unwrap();
ctx.insert("transactions", &transactions); ctx.insert("transactions", &transactions);
match tmpls.render("index.html", &ctx) { match tmpls.render("index.html", &ctx) {

View file

@ -11,7 +11,6 @@ use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::{Context, Tera}; use tera::{Context, Tera};
use crate::users::UserToken;
use accounters::models::{account::Account, categories::Category, transaction::Transaction}; use accounters::models::{account::Account, categories::Category, transaction::Transaction};
#[derive(Deserialize)] #[derive(Deserialize)]
@ -31,7 +30,6 @@ fn parse_date(s: &str) -> Option<DateTime<Utc>> {
pub async fn show( pub async fn show(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
Path(account_id): Path<i32>, Path(account_id): Path<i32>,
Query(AccountViewParams { from, to }): Query<AccountViewParams>, Query(AccountViewParams { from, to }): Query<AccountViewParams>,
) -> impl IntoResponse { ) -> 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 let from = from
.and_then(|x| parse_date(&x)) .and_then(|x| parse_date(&x))
.unwrap_or(Utc::now().duration_trunc(Duration::days(1)).unwrap() - Duration::days(30)); .unwrap_or(Utc::now().duration_trunc(Duration::days(1)).unwrap() - Duration::days(30));
@ -80,7 +70,8 @@ pub async fn show(
.collect(); .collect();
ctx.insert("categories", &categories); 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, Ok(t) => t,
Err(e) => { Err(e) => {
return ( return (
@ -109,7 +100,6 @@ pub struct AccountTxListParams {
pub async fn list_transactions( pub async fn list_transactions(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
Path(account_id): Path<i32>, Path(account_id): Path<i32>,
Query(AccountTxListParams { entries, page }): Query<AccountTxListParams>, Query(AccountTxListParams { entries, page }): Query<AccountTxListParams>,
) -> impl IntoResponse { ) -> 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()) let categories: HashMap<i32, String> = Category::list(db.as_ref())
.await .await
.unwrap() .unwrap()
@ -145,7 +127,7 @@ pub async fn list_transactions(
let n_entries = entries.unwrap_or(10).max(10); let n_entries = entries.unwrap_or(10).max(10);
let page = page.unwrap_or(0).max(0); let page = page.unwrap_or(0).max(0);
let txs = match Transaction::list( let txs = match Transaction::list_by_account(
db.as_ref(), db.as_ref(),
account.get_id(), account.get_id(),
n_entries, n_entries,
@ -181,7 +163,6 @@ pub async fn list_transactions(
pub async fn add_transactions_view( pub async fn add_transactions_view(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
Path(account_id): Path<i32>, Path(account_id): Path<i32>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut ctxt = Context::new(); 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); ctxt.insert("account", &account);
( (
@ -224,11 +197,9 @@ pub struct CreateTransactionRequest {
pub async fn add_transactions_action( pub async fn add_transactions_action(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Path(account_id): Path<i32>, Path(account_id): Path<i32>,
Json(body): Json<Vec<CreateTransactionRequest>>, Json(body): Json<Vec<CreateTransactionRequest>>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// TODO missing user id check
for tx in body.iter() { for tx in body.iter() {
if let Err(e) = Transaction::new( if let Err(e) = Transaction::new(
db.as_ref(), db.as_ref(),

View file

@ -13,14 +13,11 @@ use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::{Context, Tera}; use tera::{Context, Tera};
use crate::users::UserToken;
pub async fn view_classifiers( pub async fn view_classifiers(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
) -> impl IntoResponse { ) -> 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, Ok(r) => r,
Err(e) => { Err(e) => {
return ( return (
@ -57,7 +54,6 @@ pub async fn view_classifiers(
pub async fn rules_new_view( pub async fn rules_new_view(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
State(tmpls): State<Arc<Tera>>, State(tmpls): State<Arc<Tera>>,
uid: UserToken,
) -> impl IntoResponse { ) -> impl IntoResponse {
let categories = Category::list(db.as_ref()).await.unwrap(); let categories = Category::list(db.as_ref()).await.unwrap();
let mut ctx = Context::new(); let mut ctx = Context::new();
@ -78,10 +74,9 @@ pub struct NewRuleParams {
pub async fn rules_new_action( pub async fn rules_new_action(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Form(params): Form<NewRuleParams>, Form(params): Form<NewRuleParams>,
) -> impl IntoResponse { ) -> 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(_) => ( Ok(_) => (
StatusCode::MOVED_PERMANENTLY, StatusCode::MOVED_PERMANENTLY,
[(LOCATION, "/classifiers")], [(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, StatusCode::OK,
[(CONTENT_TYPE, "text/html;charset=utf-8")], [(CONTENT_TYPE, "text/html;charset=utf-8")],
@ -111,7 +106,6 @@ pub struct CategoryNewRuleParams {
pub async fn category_new_action( pub async fn category_new_action(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
uid: UserToken,
Form(params): Form<CategoryNewRuleParams>, Form(params): Form<CategoryNewRuleParams>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match Category::new(db.as_ref(), &params.name, &params.description).await { 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 sqlx::SqlitePool;
use tera::Tera; use tera::Tera;
use crate::users::UserToken;
pub async fn view( pub async fn view(
db: State<Arc<SqlitePool>>, db: State<Arc<SqlitePool>>,
tmpl: State<Arc<Tera>>, tmpl: State<Arc<Tera>>,
user: UserToken,
Path(id): Path<i32>, Path(id): Path<i32>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let tx = Transaction::get_by_id(db.as_ref(), id).await.unwrap(); let tx = Transaction::get_by_id(db.as_ref(), id).await.unwrap();
@ -58,8 +55,6 @@ pub struct TxUpdateRequest {
pub async fn update( pub async fn update(
db: State<Arc<SqlitePool>>, db: State<Arc<SqlitePool>>,
tmpl: State<Arc<Tera>>,
user: UserToken,
Path(id): Path<i32>, Path(id): Path<i32>,
Form(req): Form<TxUpdateRequest>, Form(req): Form<TxUpdateRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {

View file

@ -1,7 +1,6 @@
use std::net::{AddrParseError, SocketAddr}; use std::net::{AddrParseError, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use axum::headers::ContentType;
use hyper::{header, StatusCode}; use hyper::{header, StatusCode};
use sqlx::SqlitePool; 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\"")],
)),
},
}
}
}