Move and fix register

This commit is contained in:
Adrian Woźniak 2022-07-12 15:51:24 +02:00
parent 6d09816afc
commit dbeb542ac3
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
14 changed files with 787 additions and 292 deletions

View File

@ -19,7 +19,7 @@
</header>
{% match error %}
{% when Some with (e) %}
<p class="error">{{e}}></p>
<p class="error">{{e}}</p>
{% when None %}
{% endmatch %}
<article>

View File

@ -36,6 +36,10 @@ customElements.define('business-items', class extends Component {
<input type="hidden" name="item_order" id="item_order" />
</form>
</article>
<form id="moveForm" action="/business-item/move" method="post">
<input name="id" type="hidden" />
<input name="item_order" type="hidden" />
</form>
`);
const form = this.shadowRoot.querySelector('#create-new-item-form');
this.addEventListener('item:submit', ev => {
@ -56,6 +60,35 @@ customElements.define('business-items', class extends Component {
form.submit();
}
});
const moveForm = this.shadowRoot.querySelector('#moveForm');
this.addEventListener('item:up', ev => {
ev.preventDefault();
ev.stopPropagation();
const item_id = ev.detail.id;
const current = this.querySelector(`business-item[item-id="${item_id}"]`);
if (!current) return console.warn(`business-item[item-id="${item_id}"] not found`);
let prev = current.previousElementSibling;
if (!prev) return console.warn(`prev of business-item[item-id="${item_id}"] not found`);
moveForm.querySelector('[name=id]').value = item_id;
moveForm.querySelector('[name=item_order]').value = prev.item_order;
moveForm.submit();
});
this.addEventListener('item:down', ev => {
console.warn(ev);
ev.preventDefault();
ev.stopPropagation();
const item_id = ev.detail.id;
const current = this.querySelector(`business-item[item-id="${item_id}"]`);
if (!current) return;
let next = current.nextElementSibling;
if (!next) return;
moveForm.querySelector('[name=id]').value = item_id;
moveForm.querySelector('[name=item_order]').value = next.item_order;
moveForm.submit();
});
}
connectedCallback() {

View File

@ -71,12 +71,6 @@ customElements.define('business-item', class extends Component {
<form id="deleteForm" action="/business-item/delete" method="post">
<input id="delete-id" name="id" type="hidden" />
</form>
<form id="moveUpForm" action="/business-item/move-up" method="post">
<input name="id" type="hidden" />
</form>
<form id="moveDownForm" action="/business-item/move-down" method="post">
<input name="id" type="hidden" />
</form>
</section>
`);
@ -86,6 +80,7 @@ customElements.define('business-item', class extends Component {
ev.preventDefault();
ev.stopPropagation();
console.log(imageInput, ev);
this.picture_url = imageInput.url;
const updateForm = this.shadowRoot.querySelector('#updateForm');
@ -113,35 +108,27 @@ customElements.define('business-item', class extends Component {
});
{
const form = this.shadowRoot.querySelector('#moveUpForm');
const button = this.shadowRoot.querySelector('#move-up');
button.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
form.querySelector('[name=id]').value = this.item_id;
this.dispatchEvent(new CustomEvent('item:up', {
bubbles: true,
composed: true,
detail: { id: this.item_id, item_order: this.item_order }
}));
form.submit();
});
}
{
const form = this.shadowRoot.querySelector('#moveDownForm');
const button = this.shadowRoot.querySelector('#move-down');
button.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
form.querySelector('[name=id]').value = this.item_id;
this.dispatchEvent(new CustomEvent('item:down', {
bubbles: true,
composed: true,
detail: { id: this.item_id, item_order: this.item_order }
detail: { id: this.item_id }
}));
form.submit();
});
}
}
@ -156,7 +143,7 @@ customElements.define('business-item', class extends Component {
if (oldV === newV) return;
switch (name) {
case 'price':
return this.price = newV / 100.0;
return this.price = newV;
}
}
@ -174,7 +161,8 @@ customElements.define('business-item', class extends Component {
}
get item_order() {
return this.getAttribute('item-order');
const v = parseInt(this.getAttribute('item-order'));
return isNaN(v) ? null : v;
}
set item_order(v) {
@ -197,7 +185,8 @@ customElements.define('business-item', class extends Component {
set price(v) {
this.setAttribute('price', v);
this.shadowRoot.querySelector('price-input').value = v;
this.shadowRoot.querySelector('price-input').value = v / 100.0;
this.shadowRoot.querySelector('#price').value = v;
}
get picture_url() {
@ -205,8 +194,10 @@ customElements.define('business-item', class extends Component {
}
set picture_url(v) {
console.log('picture_url', v);
if (!v.startsWith("/")) v = "";
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('image-input').url = v;
this.shadowRoot.querySelector('#picture_url').value = v;
}
});

View File

@ -26,17 +26,17 @@ customElements.define('register-item-form-row', class extends PseudoForm {
<image-input></image-input>
<div id="name">
<label>Nazwa</label>
<input class="item-name" name="items[none][name]" type="text" required />
<input id="name" class="item-name" name="items[none][name]" type="text" required />
</div>
<div id="price">
<label>Cena</label>
<price-input class="item-price" name="items[none][price]" required >
<price-input id="price" class="item-price" name="items[none][price]" required >
</price-input>
</div>
<input id="submit-button" type="submit" value="Zapisz" />
<input id="remove-button" type="submit" value="Usuń" />
<input type="hidden" name="picture_url" id="picture_url" />
<input type="hidden" name="items[none][picture_url]" id="picture_url" />
<slot name="tail"></slot>
</form>
</section>
@ -78,7 +78,7 @@ customElements.define('register-item-form-row', class extends PseudoForm {
connectedCallback() {
super.connectedCallback();
this.updateNames(this.idx);
this.#updateNames(this.idx);
}
attributeChangedCallback(name, oldV, newV) {
@ -93,17 +93,22 @@ customElements.define('register-item-form-row', class extends PseudoForm {
}
get inputs() {
return this.#inputs.map(extract);
}
get #inputs() {
return [
extract(this.shadowRoot.querySelector('.item-name')),
extract(this.shadowRoot.querySelector('.item-price')),
this.shadowRoot.querySelector('.item-name'),
this.shadowRoot.querySelector('.item-price'),
this.shadowRoot.querySelector('#picture_url'),
];
}
updateNames() {
#updateNames() {
const idx = this.idx;
for (const el of this.shadowRoot.querySelectorAll('.field')) {
const id = el.id;
el.querySelector('input, price-input').setAttribute('name', `items[${ idx }][${ id }]`);
for (const el of this.#inputs) {
console.log(el);
el.setAttribute('name', `items[${ idx }][${ el.id }]`);
}
}
@ -114,6 +119,7 @@ customElements.define('register-item-form-row', class extends PseudoForm {
set idx(idx) {
this.setAttribute('idx', idx);
this.#updateNames(idx);
}
get name() {
@ -125,6 +131,15 @@ customElements.define('register-item-form-row', class extends PseudoForm {
this.shadowRoot.querySelector('.item-name').value = v;
}
get price() {
return this.getAttribute('name');
}
set price(v) {
this.setAttribute('price', v);
this.shadowRoot.querySelector('.item-price').value = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}

View File

@ -67,11 +67,13 @@ customElements.define('register-submit-form', class extends PseudoForm {
for (const row of items) {
const el = host.appendChild(document.createElement('div'));
el.className = 'item-view';
const [name, price] = row;
const [name, price, img] = row;
el.innerHTML = `
<img src="${img.value}" />
<input type="text" name="${ name.name }" value="${ name.value }" readonly />
<input type="hidden" name="${ price.name }" value="${ price.value }" readonly />
<input type="hidden" name="${ img.name }" value="${ img.value }" readonly />
<price-view value="${ price.value }"></price-view>
`;
}

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct FoundClaims {
pub iss: String,
pub sub: String,

View File

@ -9,6 +9,7 @@ use crate::routes::render_index;
mod auth;
mod model;
pub mod queries;
mod routes;
mod utils;

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Type};
use uuid::Uuid;
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Serialize, Deserialize, Type)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Type)]
pub enum AccountType {
User,
Business,
@ -81,3 +81,22 @@ pub struct LocalBusinessItem {
pub item_order: i32,
pub picture_url: String,
}
#[derive(Debug)]
pub struct CreateLocalBusinessItemInput {
pub local_business_id: i32,
pub name: String,
pub price: i64,
pub item_order: i32,
pub picture_url: String,
}
#[derive(Debug)]
pub struct UpdateLocalBusinessItemInput {
pub id: i32,
pub local_business_id: i32,
pub name: String,
pub price: i64,
pub item_order: i32,
pub picture_url: String,
}

View File

@ -55,7 +55,7 @@ impl Page {
}
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusinessItemInput {
pub name: String,
pub price: u32,
@ -90,7 +90,7 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
#[derive(Debug, serde::Deserialize)]
pub struct CreateBusinessItemInput {
pub name: String,
pub price: i32,
pub price: i64,
pub picture_url: String,
pub item_order: i32,
}
@ -99,12 +99,18 @@ pub struct CreateBusinessItemInput {
pub struct UpdateBusinessItemInput {
pub id: i32,
pub name: String,
pub price: i32,
pub price: i64,
pub picture_url: String,
pub item_order: i32,
}
#[derive(Debug, serde::Deserialize)]
pub struct DeleteBusinessItemInput {
pub struct ModifyBusinessItemInput {
pub id: i32,
}
#[derive(Debug, serde::Deserialize)]
pub struct MoveBusinessItemInput {
pub id: i32,
pub item_order: i32,
}

421
src/queries/mod.rs Normal file
View File

@ -0,0 +1,421 @@
use std::cmp::Ordering;
use tracing::error;
use crate::model::db;
#[derive(Debug)]
pub enum Error {
SetOrder {
item_id: i32,
idx: i32,
},
DeleteItem {
item_id: i32,
},
CreateItem {
input: db::CreateLocalBusinessItemInput,
},
UpdateItem {
input: db::UpdateLocalBusinessItemInput,
},
UpdateItemOrder {
id: i32,
item_order: i32,
},
AllItems,
OwnedBusiness {
account_id: i32,
},
AccountByEmail {
email: String,
},
Item {
item_id: i32,
},
}
pub type Result<T> = std::result::Result<T, Error>;
pub type T<'l> = sqlx::Transaction<'l, sqlx::Postgres>;
#[tracing::instrument]
pub async fn create_item(
t: &mut T<'_>,
input: db::CreateLocalBusinessItemInput,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, picture_url, item_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(input.local_business_id)
.bind(&input.name)
.bind(input.price)
.bind(if input.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
input.picture_url.clone()
})
.bind(input.item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::CreateItem { input }
})
}
#[tracing::instrument]
pub async fn set_item_order(
t: &mut T<'_>,
item_id: i32,
idx: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
item_order = $2
WHERE
id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(item_id)
.bind(idx as i32 + 1)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::SetOrder { item_id, idx }
})
}
#[tracing::instrument]
pub async fn delete_item(t: &mut T<'_>, item_id: i32) -> Result<Option<db::LocalBusinessItem>> {
sqlx::query_as(
r#"
DELETE FROM local_business_items
WHERE id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(item_id)
.fetch_optional(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::DeleteItem { item_id }
})
}
#[tracing::instrument]
pub async fn account_by_id(t: &mut T<'_>, id: String) -> Option<db::Account> {
match sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE id = $1 :: INT
"#,
)
.bind(id)
.fetch_optional(t)
.await
{
Ok(res) => res,
Err(e) => {
error!("{e}");
dbg!(e);
None
}
}
}
#[tracing::instrument]
pub async fn account_business_by_id(t: &mut T<'_>, account_id: i32) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned' AND owner_id = $1
GROUP BY id, state
ORDER BY id DESC
"#,
)
.bind(account_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::OwnedBusiness { account_id }
})
}
#[tracing::instrument]
pub async fn account_items(t: &mut T<'_>, account_id: i32) -> Vec<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_businesses.owner_id = $1
ORDER BY item_order ASC
"#,
)
.bind(account_id)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AllItems
})
.unwrap_or_default()
}
#[tracing::instrument]
pub async fn item_by_id(
t: &mut T<'_>,
account_id: i32,
item_id: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_business_items.id = $1 AND owner_id = $2
ORDER BY item_order ASC
"#,
)
.bind(item_id)
.bind(account_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Item { item_id }
})
}
#[tracing::instrument]
pub async fn move_item(
t: &mut T<'_>,
account_id: i32,
item_id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
let mut current = item_by_id(t, account_id, item_id).await?;
let all: Vec<db::LocalBusinessItem> = sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_businesses.owner_id = $1
ORDER BY item_order ASC
"#,
)
.bind(account_id)
.fetch_all(&mut *t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Item { item_id }
})?;
let idx = all
.iter()
.position(|p| p.id == item_id)
.ok_or(Error::Item { item_id })?;
dbg!(idx);
match item_order.cmp(&current.item_order) {
Ordering::Less => {
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| {
dbg!(prev_idx);
all.get(prev_idx)
}) {
dbg!(
"Less and found",
current.id,
current.item_order,
prev.id,
prev.item_order,
);
dbg!(update_item_order(&mut *t, current.id, prev.item_order).await?);
dbg!(update_item_order(&mut *t, prev.id, current.item_order).await?);
} else {
dbg!("Less and not found, skipping...");
}
}
Ordering::Equal => {
dbg!("Equal, skipping...");
}
Ordering::Greater => {
if let Some(next) = idx.checked_add(1).and_then(|next_idx| {
dbg!(next_idx);
all.get(next_idx)
}) {
dbg!(
"Greater and found",
current.id,
current.item_order,
next.id,
next.item_order,
);
dbg!(update_item_order(&mut *t, current.id, next.item_order).await?);
dbg!(update_item_order(&mut *t, next.id, current.item_order).await?);
} else {
dbg!("Greater and not found, skipping...");
}
}
};
current.item_order = item_order;
Ok(current)
}
#[tracing::instrument]
async fn update_item_order(
t: &mut T<'_>,
id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
item_order = $2
WHERE
id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(id)
.bind(item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateItemOrder { id, item_order }
})
}
#[tracing::instrument]
pub async fn update_item(
t: &mut T<'_>,
input: db::UpdateLocalBusinessItemInput,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
name = $3,
price = $4,
picture_url = $5,
item_order = $6
WHERE
local_business_id = $1 AND
id = $2
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(input.local_business_id)
.bind(input.id)
.bind(&input.name)
.bind(input.price)
.bind(if input.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
input.picture_url.clone()
})
.bind(input.item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateItem { input }
})
}
pub async fn account_by_email(t: &mut T<'_>, email: String) -> Result<db::Account> {
sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE email = $1
"#,
)
.bind(&email)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AccountByEmail { email }
})
}

View File

@ -12,6 +12,30 @@ use serde::Serializer;
mod restricted;
mod unrestricted;
#[macro_export]
macro_rules! ok_or_internal {
($try: expr) => {
match $try {
Ok(res) => res,
Err(e) => {
tracing::error!("{e}");
dbg!(e);
return Err($crate::routes::Error::DatabaseQuery);
}
}
};
(json $try: expr) => {
match $try {
Ok(res) => res,
Err(e) => {
tracing::error!("{e}");
dbg!(e);
return Err($crate::routes::Error::DatabaseQuery.to_json());
}
}
};
}
pub use unrestricted::render_index;
pub fn configure(config: &mut ServiceConfig) {
@ -60,6 +84,7 @@ pub enum Error {
UploadFailed,
OwnedBusinessNotFound { account_id: i32 },
OwnedBusinessItemNotFound { account_id: i32, business_id: i32 },
DatabaseQuery,
}
impl Error {
@ -90,6 +115,9 @@ impl Display for Error {
business_id, account_id
)),
Error::UploadFailed => f.write_str("Nie można zapisać pliku"),
Error::DatabaseQuery => {
f.write_str("Problem z zapisaniem zmian. Proszę spróbować później")
}
}
}
}
@ -101,6 +129,7 @@ impl ResponseError for Error {
Error::OwnedBusinessNotFound { .. } => StatusCode::BAD_REQUEST,
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
Error::UploadFailed => StatusCode::BAD_REQUEST,
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
@ -150,6 +179,7 @@ impl ResponseError for JsonError {
Error::OwnedBusinessNotFound { .. } => StatusCode::BAD_REQUEST,
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
Error::UploadFailed => StatusCode::BAD_REQUEST,
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@ -1,13 +1,11 @@
use std::sync::Arc;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{get, HttpResponse};
use askama::*;
use sqlx::PgPool;
use crate::model::{db, view};
use crate::queries;
use crate::routes::{Identity, Result};
use crate::utils;
#[derive(Debug, Template)]
#[template(path = "business-items.html")]
@ -19,10 +17,10 @@ struct BusinessItemsTemplate {
}
macro_rules! authorize {
($id: expr, $pool: expr) => {{
($t: expr, $id: expr) => {{
let account = match $id.identity() {
None => return Err(crate::routes::Error::Unauthorized),
Some(id) => crate::utils::user_by_id(id, &*$pool).await,
Some(id) => crate::queries::account_by_id($t, id).await,
};
match account {
Some(account) => account,
@ -34,13 +32,27 @@ macro_rules! authorize {
#[get("/account/business-items")]
#[tracing::instrument]
async fn business_items_page(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
handle_business_items_page(db.into_inner(), id).await
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
match handle_business_items_page(&mut t, id).await {
Ok(res) => {
t.commit().await.ok();
Ok(res)
}
Err(res) => {
t.rollback().await.ok();
Err(res)
}
}
}
async fn handle_business_items_page(pool: Arc<PgPool>, id: Identity) -> Result<HttpResponse> {
let account = authorize!(id, pool);
async fn handle_business_items_page(
t: &mut sqlx::Transaction<'_, sqlx::postgres::Postgres>,
id: Identity,
) -> Result<HttpResponse> {
let account = authorize!(t, id);
let items: Vec<db::LocalBusinessItem> = utils::account_items(account.id, pool.clone()).await;
let items: Vec<db::LocalBusinessItem> = queries::account_items(t, account.id).await;
let page = BusinessItemsTemplate {
page: view::Page::BusinessItems,
error: None,
@ -59,9 +71,8 @@ mod business_item {
use tracing::{error, info};
use crate::model::{db, view};
use crate::routes::restricted::handle_business_items_page;
use crate::routes::{Error, Identity, Result};
use crate::utils;
use crate::{queries, routes};
#[post("/update")]
#[tracing::instrument]
@ -73,61 +84,57 @@ mod business_item {
let form = form.into_inner();
dbg!(&form);
let pool = db.into_inner();
let account = authorize!(id, pool);
let mut t = crate::ok_or_internal!(pool.begin().await);
let account = authorize!(&mut t, id);
{
let business: db::LocalBusiness =
utils::account_business_by_id(account.id, pool.clone()).await?;
match queries::account_business_by_id(&mut t, account.id).await {
Ok(business) => business,
Err(e) => {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Err(routes::Error::OwnedBusinessNotFound {
account_id: account.id,
});
}
};
let item: db::LocalBusinessItem = sqlx::query_as(
r#"
UPDATE local_business_items
SET
name = $3,
price = $4,
picture_url = $5,
item_order = $6
WHERE
local_business_id = $1 AND
id = $2
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
let item: db::LocalBusinessItem = match queries::update_item(
&mut t,
db::UpdateLocalBusinessItemInput {
id: form.id,
local_business_id: business.id,
name: form.name,
price: form.price,
item_order: form.item_order,
picture_url: form.picture_url,
},
)
.bind(business.id)
.bind(form.id)
.bind(form.name)
.bind(form.price)
.bind(if form.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
form.picture_url
})
.bind(form.item_order)
.fetch_one(&*pool)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::OwnedBusinessItemNotFound {
account_id: account.id,
business_id: business.id,
{
Ok(item) => item,
Err(e) => {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Err(Error::OwnedBusinessItemNotFound {
account_id: account.id,
business_id: business.id,
});
}
})?;
};
info!("{:?}", item);
}
handle_business_items_page(pool, id).await?;
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/account/business-items"))
.finish())
}
#[post("/new")]
#[tracing::instrument]
async fn new_business_item(
form: Form<view::CreateBusinessItemInput>,
db: Data<PgPool>,
@ -136,116 +143,109 @@ RETURNING
let form = form.into_inner();
dbg!(&form);
let pool = db.into_inner();
let account = authorize!(id, pool);
let mut t = crate::ok_or_internal!(pool.begin().await);
let account = authorize!(&mut t, id);
{
let business: db::LocalBusiness =
utils::account_business_by_id(account.id, pool.clone()).await?;
let item: db::LocalBusinessItem = sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, picture_url, item_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(business.id)
.bind(form.name)
.bind(form.price)
.bind(if form.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
form.picture_url
})
.bind(form.item_order)
.fetch_one(&*pool)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::OwnedBusinessItemNotFound {
account_id: account.id,
business_id: business.id,
let business: db::LocalBusiness =
match queries::account_business_by_id(&mut t, account.id).await {
Ok(business) => business,
Err(e) => {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Err(routes::Error::OwnedBusinessNotFound {
account_id: account.id,
});
}
})?;
info!("{:?}", item);
}
};
handle_business_items_page(pool, id).await?;
if let Err(e) = queries::create_item(
&mut t,
db::CreateLocalBusinessItemInput {
local_business_id: business.id,
name: form.name,
price: form.price,
item_order: form.item_order,
picture_url: form.picture_url,
},
)
.await
{
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Err(Error::OwnedBusinessItemNotFound {
account_id: account.id,
business_id: business.id,
});
};
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/account/business-items"))
.finish())
}
#[post("/delete")]
#[tracing::instrument]
async fn delete_business_item(
form: Form<view::DeleteBusinessItemInput>,
form: Form<view::ModifyBusinessItemInput>,
db: Data<PgPool>,
id: Identity,
) -> Result<HttpResponse> {
let form = form.into_inner();
dbg!(&form);
let pool = db.into_inner();
let account = authorize!(id, pool);
dbg!(&account);
let mut t = crate::ok_or_internal!(pool.begin().await);
let account = authorize!(&mut t, id);
if let Err(e) = sqlx::query_as::<_, db::LocalBusinessItem>(
r#"
DELETE FROM local_business_items
WHERE id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(form.id)
.fetch_optional(&*pool)
.await
{
tracing::error!("{e}");
if let Err(e) = queries::delete_item(&mut t, form.id).await {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().finish());
}
let items = utils::account_items(account.id, pool.clone()).await;
let items = queries::account_items(&mut t, account.id).await;
for (idx, item) in items.into_iter().enumerate() {
if let Err(e) = sqlx::query_as::<_, db::LocalBusinessItem>(
r#"
UPDATE local_business_items
SET
item_order = $2
WHERE
id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(item.id)
.bind(idx as i32 + 1)
.fetch_optional(&*pool)
.await
{
error!("{e}");
if let Err(e) = queries::set_item_order(&mut t, item.id, idx as i32 + 1).await {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().finish());
}
}
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/account/business-items"))
.finish())
}
#[post("/move")]
#[tracing::instrument]
async fn move_item(
form: Form<view::MoveBusinessItemInput>,
db: Data<PgPool>,
id: Identity,
) -> Result<HttpResponse> {
let form = form.into_inner();
dbg!(&form);
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let account = authorize!(&mut t, id);
match queries::move_item(&mut t, account.id, form.id, form.item_order).await {
Ok(item) => item,
Err(e) => {
error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().finish());
}
};
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/account/business-items"))
.finish())
@ -256,7 +256,8 @@ RETURNING
actix_web::web::scope("/business-item")
.service(new_business_item)
.service(update_business_item)
.service(delete_business_item),
.service(delete_business_item)
.service(move_item),
);
}
}

View File

@ -7,12 +7,13 @@ use actix_web::*;
use askama::Template;
use futures_util::stream::StreamExt as _;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tracing::*;
use crate::model::db;
use crate::model::view::{self, Page};
use crate::routes::{Error, Identity, JsonResult, Result};
use crate::utils;
use crate::{queries, routes, utils};
#[derive(Template)]
#[template(path = "index.html")]
@ -43,8 +44,9 @@ pub async fn render_index() -> HttpResponse {
#[tracing::instrument]
pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let record = match id.identity() {
Some(id) => utils::user_by_id(id, &pool).await,
Some(id) => queries::account_by_id(&mut t, id).await,
_ => None,
};
let (services, mut items) = {
@ -61,7 +63,7 @@ ORDER BY id DESC
.fetch_all(&*pool)
.await
.map_err(|e| {
eprintln!("{e}");
error!("{e}");
dbg!(&e);
e
})
@ -83,7 +85,7 @@ ORDER BY item_order ASC
.fetch_all(&*pool)
.await
.map_err(|e| {
eprintln!("{e}");
error!("{e}");
dbg!(&e);
e
})
@ -107,6 +109,9 @@ ORDER BY item_order ASC
}
.render()
.unwrap();
t.commit().await.ok();
Ok(HttpResponse::Ok()
.append_header(("Content-Type", "text/html"))
.body(body))
@ -124,13 +129,15 @@ struct AccountTemplate {
#[tracing::instrument]
async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let record = match id.identity() {
Some(id) => utils::user_by_id(id, &pool).await,
Some(id) => queries::account_by_id(&mut t, id).await,
_ => {
id.forget();
None
}
};
t.commit().await.ok();
Ok(HttpResponse::Ok().body(
AccountTemplate {
account: record,
@ -205,15 +212,10 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
#[post("/register")]
#[tracing::instrument]
async fn register(
form: web::Form<RegisterForm>,
db: Data<sqlx::PgPool>,
id: Identity,
) -> HttpResponse {
async fn register(form: web::Form<RegisterForm>, db: Data<PgPool>, id: Identity) -> HttpResponse {
let mut form = form.into_inner();
{
process_items(form.items.get_or_insert_default(), form.names);
}
dbg!(&form);
process_items(form.items.get_or_insert_default(), form.names);
let pool = db.into_inner();
if form.account_type == db::AccountType::Admin {
@ -325,7 +327,51 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
.bind(if item.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
item.picture_url
let name = item.picture_url.split('/').last().unwrap_or_default();
let dir = item_picture_write_dir(format!("{}", account.id));
if let Err(e) = std::fs::create_dir_all(&dir) {
error!("{e} {:?}", dir);
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest()
.append_header(("Content-Type", "text/html"))
.body(
AccountTemplate {
account: None,
error: Some(
"Problem z utworzeniem konta. Nie można zapisać zdjęcia."
.into(),
),
page: Page::Register,
}
.render()
.unwrap(),
);
}
let path = dir.join(name);
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
error!("{e} {:?}", item.picture_url);
dbg!(e);
t.rollback().await.unwrap();
return HttpResponse::BadRequest()
.append_header(("Content-Type", "text/html"))
.body(
AccountTemplate {
account: None,
error: Some(
"Problem z utworzeniem konta. Nie można zapisać zdjęcia."
.into(),
),
page: Page::Register,
}
.render()
.unwrap(),
);
}
let path = path.to_str().map(String::from).unwrap_or_default();
path.strip_prefix('.')
.map(String::from)
.unwrap_or_else(|| path)
})
.fetch_one(&mut t)
.await;
@ -392,24 +438,17 @@ struct LoginForm {
#[post("/login")]
#[tracing::instrument]
async fn login(form: web::Form<LoginForm>, db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse {
async fn login(form: web::Form<LoginForm>, db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let form = form.into_inner();
let record: db::Account = match sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE email = $1
"#,
)
.bind(form.email)
.fetch_one(&*pool)
.await
{
let record: db::Account = match queries::account_by_email(&mut t, form.email).await {
Ok(record) => record,
Err(e) => {
tracing::error!("{e}");
return HttpResponse::Ok().body(
tracing::error!("{e:?}");
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::Ok().body(
AccountTemplate {
account: None,
error: Some("Nie znaleziono konta".into()),
@ -417,11 +456,14 @@ WHERE email = $1
}
.render()
.unwrap(),
);
));
}
};
if utils::validate(&form.password, &record.pass).is_err() {
return HttpResponse::BadRequest().body(
if let Err(e) = utils::validate(&form.password, &record.pass) {
tracing::error!("{e}");
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().body(
AccountTemplate {
account: None,
error: Some("Hasło i/lub adres e-mail są nieprawidłowe".into()),
@ -429,10 +471,12 @@ WHERE email = $1
}
.render()
.unwrap(),
);
));
}
id.remember(format!("{}", record.id));
HttpResponse::Ok()
t.commit().await.ok();
Ok(HttpResponse::Ok()
.append_header(("Content-Type", "text/html"))
.body(
AccountTemplate {
@ -442,7 +486,7 @@ WHERE email = $1
}
.render()
.unwrap(),
)
))
}
#[derive(Serialize)]
@ -450,13 +494,25 @@ struct UploadResponse {
path: String,
}
fn item_picture_write_dir(account_id: String) -> PathBuf {
PathBuf::new().join("./uploads").join(account_id)
}
#[post("/upload")]
async fn upload(mut payload: actix_multipart::Multipart, id: Identity) -> JsonResult<HttpResponse> {
let path = PathBuf::new().join(
id.identity()
.map(|id| format!("./uploads/{id}"))
.unwrap_or_else(|| "./uploads/tmp".into()),
);
async fn upload(
mut payload: actix_multipart::Multipart,
db: Data<PgPool>,
id: Identity,
) -> JsonResult<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(json pool.begin().await);
let id = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await.map(|a| a.id),
_ => None,
};
t.commit().await.ok();
let path = item_picture_write_dir(id.map(|id| format!("{id}")).unwrap_or_else(|| "tmp".into()));
std::fs::create_dir_all(&path).map_err(|e| {
error!("Cannot create upload directory {:?}", path);
dbg!(e);
@ -465,7 +521,7 @@ async fn upload(mut payload: actix_multipart::Multipart, id: Identity) -> JsonRe
if let Some(item) = payload.next().await {
let mut field = item.map_err(|e| {
warn!("Malformed upload file",);
warn!("Malformed upload file");
dbg!(e);
Error::UploadFailed.to_json()
})?;

View File

@ -1,11 +1,5 @@
use std::sync::Arc;
use argon2::{Algorithm, Argon2, Params, Version};
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use tracing::error;
use crate::model::db;
use crate::routes::Error;
#[tracing::instrument]
pub fn encrypt(pass: &str) -> password_hash::Result<String> {
@ -27,80 +21,6 @@ pub fn validate(pass: &str, pass_hash: &str) -> password_hash::Result<()> {
)
}
#[tracing::instrument]
pub async fn user_by_id(id: String, pool: &sqlx::PgPool) -> Option<db::Account> {
match sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE id = $1 :: INT
"#,
)
.bind(id)
.fetch_optional(pool)
.await
{
Ok(res) => res,
Err(e) => {
tracing::error!("{e}");
None
}
}
}
#[tracing::instrument]
pub async fn account_business_by_id(
account_id: i32,
pool: Arc<sqlx::PgPool>,
) -> crate::routes::Result<db::LocalBusiness> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned' AND owner_id = $1
GROUP BY id, state
ORDER BY id DESC
"#,
)
.bind(account_id)
.fetch_one(&*pool)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::OwnedBusinessNotFound { account_id }
})
}
#[tracing::instrument]
pub async fn account_items(account_id: i32, pool: Arc<sqlx::PgPool>) -> Vec<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_businesses.owner_id = $1
ORDER BY item_order ASC
"#,
)
.bind(account_id)
.fetch_all(&*pool)
.await
.map_err(|e| {
tracing::error!("{e}");
dbg!(&e);
e
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use crate::utils::{encrypt, validate};