Change state
This commit is contained in:
parent
16b79314fa
commit
e1bd032e2a
@ -1,24 +1,3 @@
|
||||
/* @media (min-width: 1200px) {
|
||||
.bg {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background:
|
||||
radial-gradient(circle, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 45%, rgba(255,255,255,1) 100%),
|
||||
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.bg h1 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 50px;
|
||||
text-shadow: 2px 2px 2px #c5d1d8;
|
||||
color: #FFF;
|
||||
padding-top: 60px;
|
||||
}
|
||||
}*/
|
||||
|
||||
@import url('https://fonts.cdnfonts.com/css/noto-sans');
|
||||
|
||||
* {
|
||||
@ -40,18 +19,18 @@ main {
|
||||
width: 1280px;
|
||||
margin: auto auto;
|
||||
}
|
||||
|
||||
.bg {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bg::after {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 1%, rgba(255,255,255,0) 100%),
|
||||
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 1%, rgba(255, 255, 255, 0) 100%),
|
||||
no-repeat center image-set(url("/assets/images/background.webp") 1x, url("/assets/images/background.jpeg") 1x);
|
||||
height: 200px;
|
||||
width: calc(100% - 300px);
|
||||
max-width: 1200px;
|
||||
@ -68,3 +47,60 @@ main {
|
||||
line-height: 4;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: Georgia, "Times New Roman", Times, serif;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.4em;
|
||||
margin-bottom: .8em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.4em;
|
||||
padding-bottom: .2em;
|
||||
border-bottom: 1px solid #333;
|
||||
margin-top: 1em;
|
||||
margin-bottom: .7em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.8em;
|
||||
margin-bottom: .7em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.3em;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
font-size: 1.3em;
|
||||
margin: 0 0 1.3em 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .4em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #999;
|
||||
margin: 2.5em 2em;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
font: italic 1.2em/1.6 Georgia, "Times New Roman", Times, serif;
|
||||
}
|
||||
|
||||
local-businesses local-business p {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
@ -16,33 +16,39 @@ article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
18
assets/templates/admin/businesses.html
Normal file
18
assets/templates/admin/businesses.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<ow-admin>
|
||||
<admin-businesses>
|
||||
{% for business in businesses %}
|
||||
<admin-edit-business business-id="{{ business.id }}" state="{{ business.state }}">
|
||||
<admin-business
|
||||
business-id="{{ business.id }}"
|
||||
name="{{business.name}}"
|
||||
state="{{ business.state }}"
|
||||
>
|
||||
{{business.description|safe}}
|
||||
</admin-business>
|
||||
</admin-edit-business>
|
||||
{% endfor %}
|
||||
</admin-businesses>
|
||||
</ow-admin>
|
||||
{% endblock %}
|
@ -16,5 +16,6 @@
|
||||
</edit-news-article>
|
||||
{% endfor %}
|
||||
</ow-articles>
|
||||
<article-form></article-form>
|
||||
</ow-admin>
|
||||
{% endblock %}
|
@ -34,15 +34,21 @@
|
||||
{% endmatch %}
|
||||
<article>
|
||||
<ow-nav>
|
||||
<ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path>
|
||||
<ow-path path="/" selected="{{ page.select_marketplace() }}">Targ</ow-path>
|
||||
<ow-path path="/news" selected="{{ page.select_news() }}">Aktualności</ow-path>
|
||||
<ow-path path="/account" selected="{{ page.select_account() }}">Konto</ow-path>
|
||||
{% match account.as_ref() %}
|
||||
{% when Some with (a) %}
|
||||
<ow-path path="/account/business-items" selected="{{ page.select_business_items() }}">Moje usługi</ow-path>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% if page.is_public() %}
|
||||
<ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path>
|
||||
<ow-path path="/" selected="{{ page.select_marketplace() }}">Targ</ow-path>
|
||||
<ow-path path="/news" selected="{{ page.select_news() }}">Aktualności</ow-path>
|
||||
<ow-path path="/account" selected="{{ page.select_account() }}">Konto</ow-path>
|
||||
{% match account.as_ref() %}
|
||||
{% when Some with (a) %}
|
||||
<ow-path path="/account/business-items" selected="{{ page.select_business_items() }}">Moje usługi</ow-path>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% else if page.is_admin() %}
|
||||
<ow-path path="/admin" selected="{{ page.select_admin_news() }}">Admin</ow-path>
|
||||
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}">News</ow-path>
|
||||
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}">Localne Usługi</ow-path>
|
||||
{% endif %}
|
||||
</ow-nav>
|
||||
{% block content %}{% endblock %}
|
||||
</article>
|
||||
|
@ -1,6 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<business-items>
|
||||
<business-items
|
||||
business-id="{{business.id}}"
|
||||
name="{{business.name}}"
|
||||
description="{{business.description}}"
|
||||
>
|
||||
{% for item in items %}
|
||||
<business-item
|
||||
item-id="{{item.id}}"
|
||||
|
@ -8,6 +8,7 @@
|
||||
status="{{ article.status.as_str() }}"
|
||||
published-at="{{ article.published_at|opt_time }}"
|
||||
created-at="{{ article.created_at }}"
|
||||
hide-status="true"
|
||||
>
|
||||
{{article.body|safe}}
|
||||
</news-article>
|
||||
|
@ -2,3 +2,7 @@ import "./admin/ow-admin";
|
||||
|
||||
import "./admin/article-form";
|
||||
import "./admin/edit-news-article";
|
||||
|
||||
import "./admin/admin-business";
|
||||
import "./admin/admin-businesses";
|
||||
import "./admin/admin-edit-business";
|
||||
|
40
client/src/admin/admin-business.js
Normal file
40
client/src/admin/admin-business.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('admin-business', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['business-id', "name"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
<h1 id="name"></h1>
|
||||
</article>
|
||||
<article>
|
||||
<slot></slot>
|
||||
</article>
|
||||
`);
|
||||
}
|
||||
|
||||
get business_id() {
|
||||
return this.getAttribute('business-id');
|
||||
}
|
||||
|
||||
set business_id(v) {
|
||||
this.setAttribute('business-id', v);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name');
|
||||
}
|
||||
|
||||
set name(v) {
|
||||
this.setAttribute('name', v);
|
||||
this.shadowRoot.querySelector('#name').textContent = v;
|
||||
}
|
||||
});
|
12
client/src/admin/admin-businesses.js
Normal file
12
client/src/admin/admin-businesses.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('admin-businesses', class extends Component {
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);
|
||||
}
|
||||
});
|
66
client/src/admin/admin-edit-business.js
Normal file
66
client/src/admin/admin-edit-business.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { Component, BUTTON_STYLE, INPUT_STYLE } from "../shared";
|
||||
|
||||
customElements.define('admin-edit-business', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['business-id', 'state'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#state {
|
||||
min-width: 200px;
|
||||
}
|
||||
#actions > input:not(:last-child) {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
${ BUTTON_STYLE }
|
||||
${ INPUT_STYLE }
|
||||
</style>
|
||||
<section>
|
||||
<slot></slot>
|
||||
<div id="actions">
|
||||
<input value="Usuń" type="button" />
|
||||
<form id="change-state" action="/admin/businesses/set-state" method="post">
|
||||
<input name="id" id="id" type="hidden" />
|
||||
<select id="state" name="state">
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Banned">Banned</option>
|
||||
<option value="Pinned">Pinned</option>
|
||||
<option value="Internal">Internal</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
|
||||
const form = this.shadowRoot.querySelector('#change-state');
|
||||
this.shadowRoot.querySelector('#state').addEventListener('change', ev => {
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
get business_id() {
|
||||
return this.getAttribute('business-id');
|
||||
}
|
||||
|
||||
set business_id(v) {
|
||||
this.setAttribute('business-id', v);
|
||||
this.shadowRoot.querySelector('#id').value = v;
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this.getAttribute('state');
|
||||
}
|
||||
|
||||
set state(v) {
|
||||
this.setAttribute('state', v);
|
||||
this.shadowRoot.querySelector('#state').value = v;
|
||||
}
|
||||
});
|
@ -23,6 +23,7 @@ customElements.define('edit-news-article', class extends Component {
|
||||
Edytuj
|
||||
</ow-path>
|
||||
<form method="post" action="/admin/news/delete">
|
||||
<input name="id" id="id" type="hidden" />
|
||||
<input class="link" type="submit" id="delete" value="Usuń" />
|
||||
</form>
|
||||
</article>
|
||||
@ -32,10 +33,6 @@ customElements.define('edit-news-article', class extends Component {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
this.shadowRoot.querySelector('#delete').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
get article_id() {
|
||||
@ -48,5 +45,6 @@ customElements.define('edit-news-article', class extends Component {
|
||||
const id = this.article_id;
|
||||
if (id === null) return;
|
||||
this.shadowRoot.querySelector('ow-path').path = `/admin/news/${id}`;
|
||||
this.shadowRoot.querySelector('#id').value = id;
|
||||
}
|
||||
});
|
||||
|
@ -7,7 +7,6 @@ customElements.define('ow-admin', class extends Component {
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
<article-form></article-form>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ if (!document.querySelector('#facebook-jssdk')) {
|
||||
xfbml: true,
|
||||
version: 'v14.0'
|
||||
});
|
||||
FB.AppEvents.logPageView();
|
||||
// FB.AppEvents.logPageView();
|
||||
fireFbReady();
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component } from "./shared";
|
||||
import { Component, FORM_STYLE } from "./shared";
|
||||
|
||||
import "./business-items/business-item";
|
||||
import "./register-form/register-item-form-row";
|
||||
@ -6,6 +6,10 @@ import "./register-form/register-item-form-row";
|
||||
customElements.define('business-items', class extends Component {
|
||||
#idx;
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['business-id', 'name', 'description']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
@ -19,7 +23,27 @@ customElements.define('business-items', class extends Component {
|
||||
#shadow {
|
||||
width: 33px;
|
||||
}
|
||||
#description {
|
||||
min-height: 200px;
|
||||
}
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<article id="business-details">
|
||||
<form method="post" action="/business-item/atomic-update">
|
||||
<input type="hidden" name="id" id="id">
|
||||
<div>
|
||||
<label>Nazwa</label>
|
||||
<input name="name" id="name" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Opis</label>
|
||||
<textarea name="description" id="description"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Zapisz" />
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<article id="list">
|
||||
<slot></slot>
|
||||
</article>
|
||||
@ -66,10 +90,10 @@ customElements.define('business-items', class extends Component {
|
||||
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`);
|
||||
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`);
|
||||
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();
|
||||
@ -79,7 +103,7 @@ customElements.define('business-items', class extends Component {
|
||||
ev.stopPropagation();
|
||||
|
||||
const item_id = ev.detail.id;
|
||||
const current = this.querySelector(`business-item[item-id="${item_id}"]`);
|
||||
const current = this.querySelector(`business-item[item-id="${ item_id }"]`);
|
||||
if (!current) return;
|
||||
let next = current.nextElementSibling;
|
||||
if (!next) return;
|
||||
@ -109,4 +133,31 @@ customElements.define('business-items', class extends Component {
|
||||
const el = this.shadowRoot.querySelector('register-item-form-row');
|
||||
el.idx = this.#idx + 1;
|
||||
}
|
||||
|
||||
get business_id() {
|
||||
return this.getAttribute('business-id');
|
||||
}
|
||||
|
||||
set business_id(v) {
|
||||
this.setAttribute('business-id', v);
|
||||
this.shadowRoot.querySelector('#id').value = v;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name');
|
||||
}
|
||||
|
||||
set name(v) {
|
||||
this.setAttribute('name', v);
|
||||
this.shadowRoot.querySelector('#name').value = v;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.getAttribute('description');
|
||||
}
|
||||
|
||||
set description(v) {
|
||||
this.setAttribute('description', v);
|
||||
this.shadowRoot.querySelector('#description').textContent = (v || '').trim();
|
||||
}
|
||||
});
|
||||
|
@ -80,7 +80,6 @@ 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');
|
||||
@ -194,7 +193,6 @@ 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;
|
||||
|
@ -12,7 +12,7 @@ customElements.define('local-businesses', class extends Component {
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
::slotted(local-service[local-business-visible="invisible"]) {
|
||||
::slotted(local-business[local-business-visible="invisible"]) {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
|
@ -13,6 +13,9 @@ customElements.define('local-business', class extends Component {
|
||||
#items {
|
||||
margin-top: 16px;
|
||||
}
|
||||
::slotted(local-business-item) {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
</style>
|
||||
<h2 id="name"></h2>
|
||||
<slot name="description"></slot>
|
||||
|
@ -38,8 +38,10 @@ customElements.define('ow-path', class extends Component {
|
||||
}
|
||||
|
||||
set selected(value) {
|
||||
if (value === 'selected') this.setAttribute('selected', 'selected');
|
||||
else this.removeAttribute('selected');
|
||||
if (value === true || value === 'selected')
|
||||
this.setAttribute('selected', 'selected');
|
||||
else
|
||||
this.removeAttribute('selected');
|
||||
}
|
||||
|
||||
get path() {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Component } from "../shared";
|
||||
import { BLOCK_QUOTE_STYLE, Component } from "../shared";
|
||||
import "../shared/date-time";
|
||||
|
||||
customElements.define('news-article', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ["article-id", "article-title", "status", "body", "created-at", "published-at"]
|
||||
return ["article-id", "article-title", "status", "body", "created-at", "published-at", "hide-status"]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -34,6 +34,14 @@ customElements.define('news-article', class extends Component {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.time, date-time {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
:host([hide-status="true"]) #status {
|
||||
display: none;
|
||||
}
|
||||
${ BLOCK_QUOTE_STYLE }
|
||||
</style>
|
||||
<article>
|
||||
<h1>
|
||||
@ -42,12 +50,12 @@ customElements.define('news-article', class extends Component {
|
||||
</h1>
|
||||
<section id="time">
|
||||
<div class="time">
|
||||
<span>Created at:</span>
|
||||
<span>Napisano:</span>
|
||||
<date-time id="created_at" hide-date="false" hide-time="false">
|
||||
</date-time>
|
||||
</div>
|
||||
<div class="time">
|
||||
<span>Published at:</span>
|
||||
<span>Opublikowano:</span>
|
||||
<date-time id="published_at" hide-date="false" hide-time="false">
|
||||
</date-time>
|
||||
</div>
|
||||
@ -101,4 +109,12 @@ customElements.define('news-article', class extends Component {
|
||||
set published_at(v) {
|
||||
this.shadowRoot.querySelector('#published_at').datetime = v;
|
||||
}
|
||||
|
||||
get hide_status() {
|
||||
return this.getAttribute('hide-status') === 'true';
|
||||
}
|
||||
|
||||
set hide_status(v) {
|
||||
this.setAttribute('hide-status', v);
|
||||
}
|
||||
});
|
||||
|
@ -68,14 +68,6 @@ customElements.define('ow-account', class extends Component {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mode = 'facebook';
|
||||
|
||||
const { id, name, email } = ev.details;
|
||||
console.info({ id, name, email })
|
||||
// form.querySelector('#email').value = email;
|
||||
// form.querySelector('#login').value = name;
|
||||
// form.querySelector('#password').value = crypto.randomUUID();
|
||||
// form.querySelector('#facebook_id').value = id;
|
||||
// form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,6 @@ customElements.define('register-basic-form', class extends PseudoForm {
|
||||
this.shadowRoot.querySelector('form-navigation').next();
|
||||
});
|
||||
this.shadowRoot.querySelector('form-navigation').addEventListener('form:next', ev => {
|
||||
console.log(ev);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -107,7 +107,6 @@ customElements.define('register-item-form-row', class extends PseudoForm {
|
||||
#updateNames() {
|
||||
const idx = this.idx;
|
||||
for (const el of this.#inputs) {
|
||||
console.log(el);
|
||||
el.setAttribute('name', `items[${ idx }][${ el.id }]`);
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,17 @@ textarea {
|
||||
}
|
||||
`;
|
||||
|
||||
export const BLOCK_QUOTE_STYLE = `
|
||||
blockquote {
|
||||
font: 14px/22px normal helvetica, sans-serif;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 50px;
|
||||
padding-left: 15px;
|
||||
border-left: 3px solid var(--border-slim-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FORM_STYLE = `
|
||||
form {
|
||||
display: block;
|
||||
|
@ -41,11 +41,10 @@ customElements.define('facebook-button', class extends Component {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this.#fb_sdk)
|
||||
return console.info("Facebook SDK is not available");
|
||||
return console.warn("Facebook SDK is not available");
|
||||
FB.login((res) => {
|
||||
if (res.status === 'connected') {
|
||||
FB.api("/me?fields=id,name,email", ({ id, name, email }) => {
|
||||
console.info({ id, name, email });
|
||||
this.dispatchEvent(new CustomEvent('facebook:account', {
|
||||
bubbles: true, composed: true, detail: {
|
||||
id, name, email
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component } from "../shared";
|
||||
import { Component, BLOCK_QUOTE_STYLE } from "../shared";
|
||||
|
||||
customElements.define('rich-text-editor', class extends Component {
|
||||
#selection;
|
||||
@ -60,6 +60,7 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
#text-color input, #background-color input {
|
||||
display: none;
|
||||
}
|
||||
${ BLOCK_QUOTE_STYLE }
|
||||
</style>
|
||||
<article>
|
||||
<section id="tools">
|
||||
@ -119,6 +120,12 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
<path d="M487.2 69.7c0 12.9-10.5 23.4-23.4 23.4h-322c-12.9 0-23.4-10.5-23.4-23.4s10.5-23.4 23.4-23.4h322.1c12.9.1 23.3 10.5 23.3 23.4zm-23.3 92.6H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zM38.9 30.8C17.4 30.8 0 48.2 0 69.7s17.4 39 38.9 39 38.9-17.5 38.9-39-17.4-38.9-38.9-38.9zm0 116C17.4 146.8 0 164.2 0 185.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 116C17.4 262.8 0 280.2 0 301.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 115.9C17.4 378.7 0 396.1 0 417.6s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9c0-21.4-17.4-38.9-38.9-38.9z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="short-quote" title="Cytat">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M11.192 15.757c0-.88-.23-1.618-.69-2.217-.326-.412-.768-.683-1.327-.812-.55-.128-1.07-.137-1.54-.028-.16-.95.1-1.956.76-3.022.66-1.065 1.515-1.867 2.558-2.403L9.373 5c-.8.396-1.56.898-2.26 1.505-.71.607-1.34 1.305-1.9 2.094s-.98 1.68-1.25 2.69-.346 2.04-.217 3.1c.168 1.4.62 2.52 1.356 3.35.735.84 1.652 1.26 2.748 1.26.965 0 1.766-.29 2.4-.878.628-.576.94-1.365.94-2.368l.002.003zm9.124 0c0-.88-.23-1.618-.69-2.217-.326-.42-.77-.692-1.327-.817-.56-.124-1.074-.13-1.54-.022-.16-.94.09-1.95.75-3.02.66-1.06 1.514-1.86 2.557-2.4L18.49 5c-.8.396-1.555.898-2.26 1.505a11.29 11.29 0 0 0-1.894 2.094c-.556.79-.97 1.68-1.24 2.69a8.04 8.04 0 0 0-.217 3.1c.165 1.4.615 2.52 1.35 3.35.732.833 1.646 1.25 2.742 1.25.967 0 1.768-.29 2.402-.876.627-.576.942-1.365.942-2.368v.01z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="insert">
|
||||
<button id="image" title="Wstaw obrazek">
|
||||
@ -181,16 +188,23 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
return this.#setWrap(['H5'], { repeat: 'ignore' });
|
||||
}
|
||||
});
|
||||
this.shadowRoot.querySelector('#ordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['ol', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
this.shadowRoot.querySelector('#unordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['ul', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
{
|
||||
this.shadowRoot.querySelector('#ordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['ol', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
this.shadowRoot.querySelector('#unordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['ul', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
this.shadowRoot.querySelector('#short-quote').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['blockquote'], { repeat: 'drop' });
|
||||
});
|
||||
}
|
||||
{
|
||||
let timeout = null;
|
||||
this.shadowRoot.querySelector('#edit').addEventListener('keyup', (ev) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -37,6 +38,12 @@ pub enum LocalBusinessState {
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl Display for LocalBusinessState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalBusinessState {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -126,6 +133,13 @@ pub struct UpdateNewsArticleInput {
|
||||
pub published_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct UpdateLocalBusinessInput {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateLocalBusinessItemInput {
|
||||
pub local_business_id: i32,
|
||||
@ -144,3 +158,8 @@ pub struct UpdateLocalBusinessItemInput {
|
||||
pub item_order: i32,
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct DeleteNewsArticleInput {
|
||||
pub id: i32,
|
||||
}
|
||||
|
@ -7,15 +7,36 @@ pub enum Page {
|
||||
LocalBusinesses,
|
||||
News,
|
||||
Account,
|
||||
Admin,
|
||||
AdminCreateNews,
|
||||
Register,
|
||||
Login,
|
||||
BusinessItems,
|
||||
Marketplace,
|
||||
AdminNews,
|
||||
AdminCreateNews,
|
||||
AdminBusinesses,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn is_public(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Page::LocalBusinesses
|
||||
| Page::News
|
||||
| Page::Account
|
||||
| Page::Register
|
||||
| Page::Login
|
||||
| Page::BusinessItems
|
||||
| Page::Marketplace
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_admin(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Page::AdminNews | Page::AdminCreateNews | Page::AdminBusinesses
|
||||
)
|
||||
}
|
||||
|
||||
pub fn select_index(&self) -> &str {
|
||||
if matches!(self, Page::LocalBusinesses) {
|
||||
"selected"
|
||||
@ -55,6 +76,22 @@ impl Page {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_admin_news(&self) -> &str {
|
||||
if matches!(self, Page::AdminNews) {
|
||||
"selected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_admin_businesses(&self) -> &str {
|
||||
if matches!(self, Page::AdminBusinesses) {
|
||||
"selected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -64,6 +101,12 @@ pub struct BusinessItemInput {
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SetStateBusinessInput {
|
||||
pub id: i32,
|
||||
pub state: db::LocalBusinessState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LocalBusiness {
|
||||
pub id: i32,
|
||||
@ -97,6 +140,13 @@ pub struct CreateBusinessItemInput {
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtomicUpdateBusinessItemInput {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateBusinessItemInput {
|
||||
pub id: i32,
|
||||
@ -131,3 +181,8 @@ pub struct CreateNewsArticleInput {
|
||||
pub body: String,
|
||||
pub status: db::NewsStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteNewsArticleInput {
|
||||
pub id: i32,
|
||||
}
|
||||
|
@ -7,10 +7,15 @@ use crate::model::db::NewsArticle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
VisibleBusinesses,
|
||||
SetOrder {
|
||||
item_id: i32,
|
||||
idx: i32,
|
||||
},
|
||||
UpdateBusiness {
|
||||
input: db::UpdateLocalBusinessInput,
|
||||
},
|
||||
VisibleBusinessItems,
|
||||
DeleteItem {
|
||||
item_id: i32,
|
||||
},
|
||||
@ -45,6 +50,9 @@ pub enum Error {
|
||||
UpdateNewsArticle {
|
||||
input: db::UpdateNewsArticleInput,
|
||||
},
|
||||
DeleteNewsArticle {
|
||||
id: i32,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@ -168,7 +176,10 @@ WHERE id = $1 :: INT
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn account_business_by_id(t: &mut T<'_>, account_id: i32) -> Result<db::LocalBusiness> {
|
||||
pub async fn account_business_by_owner_id(
|
||||
t: &mut T<'_>,
|
||||
account_id: i32,
|
||||
) -> Result<db::LocalBusiness> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
@ -188,6 +199,35 @@ ORDER BY id DESC
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_business(
|
||||
t: &mut T<'_>,
|
||||
input: db::UpdateLocalBusinessInput,
|
||||
) -> Result<db::LocalBusiness> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE local_businesses
|
||||
SET
|
||||
name = $2,
|
||||
description = $3
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
id, owner_id, name, description, state
|
||||
"#,
|
||||
)
|
||||
.bind(input.id)
|
||||
.bind(&input.name)
|
||||
.bind(&input.description)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::UpdateBusiness { input }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn account_items(t: &mut T<'_>, account_id: i32) -> Vec<db::LocalBusinessItem> {
|
||||
sqlx::query_as(
|
||||
@ -570,3 +610,145 @@ RETURNING
|
||||
Error::UpdateNewsArticle { input }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_news_article(t: &mut T<'_>, id: i32) -> Result<Option<NewsArticle>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
DELETE FROM news
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::DeleteNewsArticle { id }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn visible_businesses(t: &mut T<'_>) -> Result<Vec<db::LocalBusiness>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
FROM local_businesses
|
||||
WHERE state != 'Banned'
|
||||
GROUP BY id, state
|
||||
ORDER BY id DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinesses
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn visible_business_items(t: &mut T<'_>) -> Result<Vec<db::LocalBusinessItem>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order,
|
||||
picture_url
|
||||
FROM local_business_items
|
||||
ORDER BY item_order ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinessItems
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn all_businesses(t: &mut T<'_>) -> Result<Vec<db::LocalBusiness>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
FROM local_businesses
|
||||
GROUP BY id, state
|
||||
ORDER BY id DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinesses
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn all_business_items(t: &mut T<'_>) -> Result<Vec<db::LocalBusinessItem>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order,
|
||||
picture_url
|
||||
FROM local_business_items
|
||||
ORDER BY item_order ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinessItems
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn set_business_state(
|
||||
t: &mut T<'_>,
|
||||
id: i32,
|
||||
state: db::LocalBusinessState,
|
||||
) -> Result<db::LocalBusiness> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE local_businesses
|
||||
SET state = $2
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
name,
|
||||
description,
|
||||
state
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(state)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinessItems
|
||||
})
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ macro_rules! ok_or_internal {
|
||||
match $try {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
return Err($crate::routes::Error::DatabaseQuery);
|
||||
}
|
||||
@ -29,7 +29,7 @@ macro_rules! ok_or_internal {
|
||||
match $try {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
return Err($crate::routes::Error::DatabaseQuery.to_json());
|
||||
}
|
||||
@ -37,6 +37,17 @@ macro_rules! ok_or_internal {
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! not_xss {
|
||||
($s: expr, $t: expr) => {
|
||||
if let Err(e) = crate::routes::reject_xss($s) {
|
||||
dbg!(&e);
|
||||
$t.rollback().await.unwrap();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub use unrestricted::render_index;
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
@ -87,6 +98,15 @@ pub enum Error {
|
||||
OwnedBusinessNotFound { account_id: i32 },
|
||||
OwnedBusinessItemNotFound { account_id: i32, business_id: i32 },
|
||||
DatabaseQuery,
|
||||
XSS,
|
||||
}
|
||||
|
||||
pub fn reject_xss(s: &str) -> Result<()> {
|
||||
if s.contains("<script ") || s.contains("<script>") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::XSS)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@ -121,6 +141,7 @@ impl Display for Error {
|
||||
f.write_str("Problem z zapisaniem zmian. Proszę spróbować później")
|
||||
}
|
||||
Error::Forbidden => f.write_str("Tylko admin może wejść na tę stronę"),
|
||||
Error::XSS => f.write_str("Wykryto próbę XSS"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,6 +155,7 @@ impl ResponseError for Error {
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Forbidden => StatusCode::FORBIDDEN,
|
||||
Error::XSS => StatusCode::IM_A_TEAPOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,6 +207,7 @@ impl ResponseError for JsonError {
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Forbidden => StatusCode::FORBIDDEN,
|
||||
Error::XSS => StatusCode::IM_A_TEAPOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ use sqlx::PgPool;
|
||||
use crate::model::{db, view};
|
||||
use crate::queries;
|
||||
use crate::routes::{Identity, Result};
|
||||
pub mod admin;
|
||||
mod business_item;
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "business-items.html")]
|
||||
@ -14,8 +16,10 @@ struct BusinessItemsTemplate {
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
items: Vec<db::LocalBusinessItem>,
|
||||
business: db::LocalBusiness,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! authorize {
|
||||
($t: expr, $id: expr) => {{
|
||||
let account = match $id.identity() {
|
||||
@ -62,434 +66,21 @@ async fn handle_business_items_page(
|
||||
) -> Result<HttpResponse> {
|
||||
let account = authorize!(t, id);
|
||||
|
||||
let business =
|
||||
crate::ok_or_internal!(queries::account_business_by_owner_id(t, account.id).await);
|
||||
let items: Vec<db::LocalBusinessItem> = queries::account_items(t, account.id).await;
|
||||
let page = BusinessItemsTemplate {
|
||||
page: view::Page::BusinessItems,
|
||||
error: None,
|
||||
account: Some(account),
|
||||
items,
|
||||
business,
|
||||
};
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(page.render().unwrap()))
|
||||
}
|
||||
|
||||
mod business_item {
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::{post, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Error, Identity, Result};
|
||||
use crate::{queries, routes};
|
||||
|
||||
#[post("/update")]
|
||||
#[tracing::instrument]
|
||||
async fn update_business_item(
|
||||
form: Form<view::UpdateBusinessItemInput>,
|
||||
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);
|
||||
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
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);
|
||||
}
|
||||
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>,
|
||||
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);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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::ModifyBusinessItemInput>,
|
||||
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);
|
||||
|
||||
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 = queries::account_items(&mut t, account.id).await;
|
||||
for (idx, item) in items.into_iter().enumerate() {
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
actix_web::web::scope("/business-item")
|
||||
.service(new_business_item)
|
||||
.service(update_business_item)
|
||||
.service(delete_business_item)
|
||||
.service(move_item),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod admin {
|
||||
use actix_web::web::{Data, Form, Path, ServiceConfig};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::model::view::Page;
|
||||
use crate::model::{db, view};
|
||||
use crate::queries;
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::filters;
|
||||
|
||||
macro_rules! require_admin {
|
||||
($t: expr, $id: expr) => {{
|
||||
let account = authorize!(&mut $t, $id);
|
||||
if account.account_type == crate::model::db::AccountType::Admin {
|
||||
return Err(crate::routes::Error::Forbidden);
|
||||
}
|
||||
account
|
||||
}};
|
||||
(json; $t: expr, $id: expr) => {{
|
||||
let account = authorize!(json; &mut $t, $id);
|
||||
if account.account_type == crate::model::db::AccountType::Admin {
|
||||
return Err(crate::routes::Error::Forbidden.to_json());
|
||||
}
|
||||
account
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/index.html")]
|
||||
struct AdminTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
news: Vec<db::NewsArticle>,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn admin(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let news = queries::all_news(&mut t).await.unwrap_or_default();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AdminTemplate {
|
||||
page: Page::Admin,
|
||||
error: None,
|
||||
account: None,
|
||||
news,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/edit.html")]
|
||||
struct EditTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
article: db::NewsArticle,
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit_news_article(
|
||||
path: Path<(i32,)>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let article_id = path.into_inner().0;
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let article = match queries::news_article_by_id(&mut t, article_id).await {
|
||||
Ok(article) => article,
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
};
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
EditTemplate {
|
||||
page: Page::Admin,
|
||||
error: None,
|
||||
account: None,
|
||||
article,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::CreateNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
if let Err(e) = queries::create_news_article(
|
||||
&mut t,
|
||||
db::CreateNewsArticleInput {
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AdminTemplate {
|
||||
page: Page::AdminCreateNews,
|
||||
error: Some("Failed".into()),
|
||||
account: None,
|
||||
news: vec![],
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/update")]
|
||||
async fn update_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::UpdateNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
match queries::update_news_article(
|
||||
&mut t,
|
||||
db::UpdateNewsArticleInput {
|
||||
id: form.id,
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
published_at: if matches!(form.status, db::NewsStatus::Published) {
|
||||
Some(chrono::Utc::now().naive_utc())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
Ok(..) => {
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn news_article_upload(
|
||||
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 account = require_admin!(json; t, id);
|
||||
t.commit().await.ok();
|
||||
crate::routes::uploads::hande_upload(payload, Some(account.id), "news").await
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("/admin")
|
||||
.service(
|
||||
web::scope("/news")
|
||||
.service(create_news_article)
|
||||
.service(news_article_upload)
|
||||
.service(edit_news_article)
|
||||
.service(update_news_article),
|
||||
)
|
||||
.service(admin),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(business_items_page);
|
||||
business_item::configure(config);
|
||||
|
334
src/routes/restricted/admin.rs
Normal file
334
src/routes/restricted/admin.rs
Normal file
@ -0,0 +1,334 @@
|
||||
use actix_web::web::{Data, Form, Path, ServiceConfig};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::model::view::{Page, SetStateBusinessInput};
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::filters;
|
||||
use crate::{authorize, queries};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! require_admin {
|
||||
($t: expr, $id: expr) => {{
|
||||
let account = authorize!(&mut $t, $id);
|
||||
if account.account_type == crate::model::db::AccountType::Admin {
|
||||
return Err(crate::routes::Error::Forbidden);
|
||||
}
|
||||
account
|
||||
}};
|
||||
(json; $t: expr, $id: expr) => {{
|
||||
let account = authorize!(json; &mut $t, $id);
|
||||
if account.account_type == crate::model::db::AccountType::Admin {
|
||||
return Err(crate::routes::Error::Forbidden.to_json());
|
||||
}
|
||||
account
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/news.html")]
|
||||
struct AdminNewsTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
news: Vec<db::NewsArticle>,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn admin(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin/news"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn admin_news(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let news = queries::all_news(&mut t).await.unwrap_or_default();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AdminNewsTemplate {
|
||||
page: Page::AdminNews,
|
||||
error: None,
|
||||
account: None,
|
||||
news,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/edit.html")]
|
||||
struct EditTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
article: db::NewsArticle,
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit_news_article(
|
||||
path: Path<(i32,)>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let article_id = path.into_inner().0;
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let article = match queries::news_article_by_id(&mut t, article_id).await {
|
||||
Ok(article) => article,
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
};
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
EditTemplate {
|
||||
page: Page::AdminNews,
|
||||
error: None,
|
||||
account: None,
|
||||
article,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::CreateNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
if let Err(e) = queries::create_news_article(
|
||||
&mut t,
|
||||
db::CreateNewsArticleInput {
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AdminNewsTemplate {
|
||||
page: Page::AdminCreateNews,
|
||||
error: Some("Failed".into()),
|
||||
account: None,
|
||||
news: vec![],
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin/news"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/update")]
|
||||
async fn update_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::UpdateNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
match queries::update_news_article(
|
||||
&mut t,
|
||||
db::UpdateNewsArticleInput {
|
||||
id: form.id,
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
published_at: if matches!(form.status, db::NewsStatus::Published) {
|
||||
Some(chrono::Utc::now().naive_utc())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin/news"))
|
||||
.finish())
|
||||
}
|
||||
Ok(..) => {
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin/news"))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/delete")]
|
||||
async fn delete_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::DeleteNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
match queries::delete_news_article(&mut t, form.id).await {
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
Ok(..) => {
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn news_article_upload(
|
||||
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 account = require_admin!(json; t, id);
|
||||
t.commit().await.ok();
|
||||
crate::routes::uploads::hande_upload(payload, Some(account.id), "news").await
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/businesses.html")]
|
||||
struct AdminBusinessesTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
businesses: Vec<view::LocalBusiness>,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn admin_businesses(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let (services, mut items) = {
|
||||
use crate::model::db::{LocalBusiness, LocalBusinessItem};
|
||||
let services: Vec<LocalBusiness> =
|
||||
queries::all_businesses(&mut t).await.unwrap_or_default();
|
||||
|
||||
let items: Vec<LocalBusinessItem> = queries::all_business_items(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
(services, items)
|
||||
};
|
||||
|
||||
let businesses: Vec<_> = services
|
||||
.into_iter()
|
||||
.map(|service| view::LocalBusiness::from((service, &mut items)))
|
||||
.collect();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AdminBusinessesTemplate {
|
||||
page: Page::AdminBusinesses,
|
||||
error: None,
|
||||
account: None,
|
||||
businesses,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/set-state")]
|
||||
async fn admin_business_set_state(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<SetStateBusinessInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
let form = form.into_inner();
|
||||
dbg!(&form);
|
||||
|
||||
if let Err(e) = queries::set_business_state(&mut t, form.id, form.state).await {
|
||||
dbg!(e);
|
||||
}
|
||||
|
||||
if let Err(e) = t.commit().await {
|
||||
dbg!(e);
|
||||
}
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin/businesses"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("/admin")
|
||||
.service(
|
||||
web::scope("/news")
|
||||
.service(admin_news)
|
||||
.service(create_news_article)
|
||||
.service(news_article_upload)
|
||||
.service(edit_news_article)
|
||||
.service(update_news_article)
|
||||
.service(delete_news_article),
|
||||
)
|
||||
.service(
|
||||
web::scope("/businesses")
|
||||
.service(admin_businesses)
|
||||
.service(admin_business_set_state),
|
||||
)
|
||||
.service(admin),
|
||||
);
|
||||
}
|
245
src/routes/restricted/business_item.rs
Normal file
245
src/routes/restricted/business_item.rs
Normal file
@ -0,0 +1,245 @@
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::{post, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Error, Identity, Result};
|
||||
use crate::{authorize, not_xss, queries, routes};
|
||||
|
||||
#[post("/update")]
|
||||
#[tracing::instrument]
|
||||
async fn update_business_item(
|
||||
form: Form<view::UpdateBusinessItemInput>,
|
||||
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);
|
||||
not_xss!(&form.name, t);
|
||||
not_xss!(&form.picture_url, t);
|
||||
|
||||
{
|
||||
let business: db::LocalBusiness =
|
||||
match queries::account_business_by_owner_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 = 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,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
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);
|
||||
}
|
||||
t.commit().await.ok();
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/account/business-items"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/atomic-update")]
|
||||
#[tracing::instrument]
|
||||
async fn atomic_update_business_item(
|
||||
form: Form<view::AtomicUpdateBusinessItemInput>,
|
||||
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);
|
||||
not_xss!(&form.name, t);
|
||||
not_xss!(&form.description, t);
|
||||
|
||||
{
|
||||
let business: db::LocalBusiness = match queries::update_business(
|
||||
&mut t,
|
||||
db::UpdateLocalBusinessInput {
|
||||
id: form.id,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(item) => item,
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Err(Error::OwnedBusinessItemNotFound {
|
||||
account_id: account.id,
|
||||
business_id: form.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
info!("{:?}", business);
|
||||
}
|
||||
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>,
|
||||
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);
|
||||
not_xss!(&form.name, t);
|
||||
not_xss!(&form.picture_url, t);
|
||||
|
||||
let business: db::LocalBusiness =
|
||||
match queries::account_business_by_owner_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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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::ModifyBusinessItemInput>,
|
||||
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);
|
||||
|
||||
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 = queries::account_items(&mut t, account.id).await;
|
||||
for (idx, item) in items.into_iter().enumerate() {
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
actix_web::web::scope("/business-item")
|
||||
.service(new_business_item)
|
||||
.service(update_business_item)
|
||||
.service(delete_business_item)
|
||||
.service(move_item)
|
||||
.service(atomic_update_business_item),
|
||||
);
|
||||
}
|
@ -12,7 +12,7 @@ use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::filters;
|
||||
use crate::{queries, utils};
|
||||
use crate::{not_xss, queries, routes, utils};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
@ -39,7 +39,7 @@ pub async fn render_index() -> HttpResponse {
|
||||
|
||||
#[get("/")]
|
||||
#[tracing::instrument]
|
||||
pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
pub async fn index(db: Data<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() {
|
||||
@ -48,45 +48,13 @@ pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> Result<HttpResponse>
|
||||
};
|
||||
let (services, mut items) = {
|
||||
use crate::model::db::{LocalBusiness, LocalBusinessItem};
|
||||
let services: Vec<LocalBusiness> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
FROM local_businesses
|
||||
WHERE state != 'Banned'
|
||||
GROUP BY id, state
|
||||
ORDER BY id DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
e
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let services: Vec<LocalBusiness> = queries::visible_businesses(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let items: Vec<LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order,
|
||||
picture_url
|
||||
FROM local_business_items
|
||||
ORDER BY item_order ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
e
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let items: Vec<LocalBusinessItem> = queries::visible_business_items(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
(services, items)
|
||||
};
|
||||
|
||||
@ -122,7 +90,7 @@ struct AccountTemplate {
|
||||
|
||||
#[get("/account")]
|
||||
#[tracing::instrument]
|
||||
async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> Result<HttpResponse> {
|
||||
async fn account_page(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let account = match id.identity() {
|
||||
@ -207,14 +175,18 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
|
||||
|
||||
#[post("/register")]
|
||||
#[tracing::instrument]
|
||||
async fn register(form: web::Form<RegisterForm>, db: Data<PgPool>, id: Identity) -> HttpResponse {
|
||||
async fn register(
|
||||
form: web::Form<RegisterForm>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut form = form.into_inner();
|
||||
dbg!(&form);
|
||||
process_items(form.items.get_or_insert_default(), form.names);
|
||||
|
||||
let pool = db.into_inner();
|
||||
if form.account_type == db::AccountType::Admin {
|
||||
return HttpResponse::BadRequest().body("Security breach attempt detected!");
|
||||
return Ok(HttpResponse::BadRequest().body("Security breach attempt detected!"));
|
||||
}
|
||||
|
||||
let mut t = pool.begin().await.unwrap();
|
||||
@ -225,7 +197,7 @@ async fn register(form: web::Form<RegisterForm>, db: Data<PgPool>, id: Identity)
|
||||
tracing::error!("{:?}", e);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().body(
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Zapisanie hasła nie powiodło się".into()),
|
||||
@ -233,7 +205,7 @@ async fn register(form: web::Form<RegisterForm>, db: Data<PgPool>, id: Identity)
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@ -260,7 +232,7 @@ RETURNING id, login, email, pass, facebook_id, account_type
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().body(
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
@ -268,7 +240,7 @@ RETURNING id, login, email, pass, facebook_id, account_type
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@ -277,6 +249,9 @@ RETURNING id, login, email, pass, facebook_id, account_type
|
||||
let owner_id = account.id;
|
||||
let description = form.description.as_deref().unwrap_or_default();
|
||||
|
||||
not_xss!(name, t);
|
||||
not_xss!(description, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_businesses (name, owner_id, description)
|
||||
@ -295,7 +270,7 @@ RETURNING id, owner_id, name, description, state
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().body(
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
@ -303,11 +278,14 @@ RETURNING id, owner_id, name, description, state
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
|
||||
not_xss!(&item.name, t);
|
||||
not_xss!(&item.picture_url, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
|
||||
@ -328,7 +306,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
error!("{e} {:?}", dir);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().content_type("text/html").body(
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some(
|
||||
@ -338,14 +316,14 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
}
|
||||
.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().content_type("text/html").body(
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some(
|
||||
@ -355,7 +333,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
));
|
||||
}
|
||||
let path = path.to_str().map(String::from).unwrap_or_default();
|
||||
path.strip_prefix('.')
|
||||
@ -370,7 +348,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest().content_type("text/html").body(
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
@ -378,7 +356,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -386,7 +364,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
|
||||
t.commit().await.unwrap();
|
||||
|
||||
HttpResponse::SeeOther()
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.body(
|
||||
AccountTemplate {
|
||||
@ -396,7 +374,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
@ -487,7 +465,7 @@ async fn upload(
|
||||
_ => None,
|
||||
};
|
||||
t.commit().await.ok();
|
||||
crate::routes::uploads::hande_upload(payload, id, "accounts").await
|
||||
routes::uploads::hande_upload(payload, id, "accounts").await
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
Loading…
Reference in New Issue
Block a user