Rename data table, start owned business items

This commit is contained in:
Adrian Woźniak 2022-07-07 16:22:31 +02:00
parent a727a78bcb
commit 4ce82d15fb
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
26 changed files with 647 additions and 208 deletions

152
Cargo.lock generated
View File

@ -2,29 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "actix"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3720d0064a0ce5c0de7bd93bdb0a6caebab2a9b5668746145d7b3b0c5da02914"
dependencies = [
"actix-rt",
"bitflags",
"bytes",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot 0.11.2",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util 0.6.10",
]
[[package]] [[package]]
name = "actix" name = "actix"
version = "0.13.0" version = "0.13.0"
@ -46,7 +23,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
"tokio", "tokio",
"tokio-util 0.7.3", "tokio-util",
] ]
[[package]] [[package]]
@ -63,7 +40,7 @@ dependencies = [
"memchr", "memchr",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tokio-util 0.7.3", "tokio-util",
] ]
[[package]] [[package]]
@ -106,9 +83,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.1.0" version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2e9f6794b5826aff6df65e3a0d0127b271d1c03629c774238f3582e903d4e4" checksum = "6f9ffb6db08c1c3a1f4aef540f1a63193adc73c4fbd40b75a95fc8c5258f6e51"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@ -166,27 +143,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "actix-redis"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dde9fa8bde15d084d459eb59f766c08d00a6f550e7054187878fc9cbaa19115"
dependencies = [
"actix 0.12.0",
"actix-rt",
"actix-service",
"actix-tls",
"actix-web",
"backoff",
"derive_more",
"futures-core",
"log",
"redis-async",
"time 0.3.11",
"tokio",
"tokio-util 0.6.10",
]
[[package]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.5.0" version = "0.5.0"
@ -241,22 +197,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-tls"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297"
dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-utils",
"futures-core",
"log",
"pin-project-lite",
"tokio-util 0.7.3",
]
[[package]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.0" version = "3.0.0"
@ -489,17 +429,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
dependencies = [
"getrandom",
"instant",
"rand",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@ -831,6 +760,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.21" version = "0.3.21"
@ -847,6 +791,17 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-intrusive" name = "futures-intrusive"
version = "0.4.0" version = "0.4.0"
@ -858,6 +813,12 @@ dependencies = [
"parking_lot 0.11.2", "parking_lot 0.11.2",
] ]
[[package]]
name = "futures-io"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.21" version = "0.3.21"
@ -887,10 +848,13 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@ -962,7 +926,7 @@ dependencies = [
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
"tokio-util 0.7.3", "tokio-util",
"tracing", "tracing",
] ]
@ -1313,16 +1277,19 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
name = "oswilno" name = "oswilno"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix 0.13.0", "actix",
"actix-cors", "actix-cors",
"actix-files", "actix-files",
"actix-http",
"actix-identity", "actix-identity",
"actix-redis",
"actix-rt", "actix-rt",
"actix-utils",
"actix-web", "actix-web",
"argon2", "argon2",
"askama", "askama",
"chrono", "chrono",
"futures",
"futures-util",
"gumdrop", "gumdrop",
"password-hash", "password-hash",
"rand", "rand",
@ -1529,21 +1496,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "redis-async"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76b00c604527d485d7a146d1e324ec1cf0a5ec522acb3d05bf7d51a9c28d7c0c"
dependencies = [
"bytes",
"futures-channel",
"futures-sink",
"futures-util",
"log",
"tokio",
"tokio-util 0.6.10",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.13" version = "0.2.13"
@ -2023,20 +1975,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.3" version = "0.7.3"

View File

@ -6,10 +6,11 @@ edition = "2021"
[dependencies] [dependencies]
actix = { version = "*" } actix = { version = "*" }
actix-web = { version = "*" } actix-web = { version = "*" }
actix-http = { version = "3.2.1" }
actix-cors = { version = "*" } actix-cors = { version = "*" }
actix-rt = { version = "*" } actix-rt = { version = "*" }
actix-utils = { version = "3.0.0" }
actix-files = { version = "*" } actix-files = { version = "*" }
actix-redis = { version = "0.11.0" }
actix-identity = { version = "0.4.0" } actix-identity = { version = "0.4.0" }
askama = { version = "*" } askama = { version = "*" }
validator = { version = "0.14", features = ["derive"] } validator = { version = "0.14", features = ["derive"] }
@ -25,3 +26,5 @@ tracing-actix-web = { version = "*" }
argon2 = { version = "0.4.1" } argon2 = { version = "0.4.1" }
password-hash = { version = "0.4.2" } password-hash = { version = "0.4.2" }
rand = { version = "0.8.5", features = [] } rand = { version = "0.8.5", features = [] }
futures = { version = "0.3.21", features = ["async-await", "std"] }
futures-util = { version = "0.3.21", features = [] }

View File

@ -17,11 +17,21 @@
<h1>OS Wilno</h1> <h1>OS Wilno</h1>
</div> </div>
</header> </header>
{% match error %}
{% when Some with (e) %}
<p class="error">{{e}}></p>
{% when None %}
{% endmatch %}
<article> <article>
<ow-nav> <ow-nav>
<ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path> <ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path>
<ow-path path="/news" selected="{{ page.select_news() }}">Aktualności</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> <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 %}
</ow-nav> </ow-nav>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</article> </article>

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<business-items>
{% for item in items %}
<business-item name="{{item.name}}" price="{{item.price}}">
</business-item>
{% endfor %}
</business-items>
{% endblock %}

View File

@ -1,8 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<local-services> <local-businesses>
{% for service in services %} {% for service in services %}
<local-service <local-business
slot="services" slot="services"
service-id="{{service.id}}" service-id="{{service.id}}"
name="{{service.name}}" name="{{service.name}}"
@ -13,11 +13,11 @@
{% endfor %} {% endfor %}
{% for item in service.items %} {% for item in service.items %}
<local-service-item slot="item" name="{{item.name}}" price="{{item.price}}"> <local-business-item slot="item" name="{{item.name}}" price="{{item.price}}">
</local-service-item> </local-business-item>
{% endfor %} {% endfor %}
</local-service> </local-business>
{% endfor %} {% endfor %}
</local-services> </local-businesses>
{% endblock %} {% endblock %}

187
client/dist/app.js vendored
View File

@ -92,6 +92,10 @@ input[type="button"], input[type="submit"] {
width: auto; width: auto;
height: calc(1.5em + 0.75rem + 2px); height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem; padding: .375rem .75rem;
border: 1px solid #495057;
color: #495057;
background: white;
} }
`; `;
class PseudoForm extends HTMLElement { class PseudoForm extends HTMLElement {
@ -173,45 +177,7 @@ customElements.define("form-navigation", class extends HTMLElement {
})); }));
} }
}); });
customElements.define("local-service", class extends HTMLElement { customElements.define("local-business-item", class extends HTMLElement {
static get observedAttributes() {
return [
"name",
"service-id",
"state"
];
}
constructor(){
super();
let b = this[S] = this.attachShadow({
mode: "closed"
});
b.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
connectedCallback() {
this[S].querySelector("#name").textContent = this.getAttribute("name");
}
attributeChangedCallback(b, c, d) {
if (c !== d && "name" === b) return this[S].querySelector("#name").textContent = d;
}
get name() {
return this.getAttribute("name") || "";
}
});
customElements.define("local-service-item", class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return [ return [
"name", "name",
@ -260,7 +226,45 @@ customElements.define("local-service-item", class extends HTMLElement {
return isNaN(b) ? 0 : b; return isNaN(b) ? 0 : b;
} }
}); });
customElements.define("local-services", class extends HTMLElement { customElements.define("local-business", class extends HTMLElement {
static get observedAttributes() {
return [
"name",
"service-id",
"state"
];
}
constructor(){
super();
let b = this[S] = this.attachShadow({
mode: "closed"
});
b.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
connectedCallback() {
this[S].querySelector("#name").textContent = this.getAttribute("name");
}
attributeChangedCallback(b, c, d) {
if (c !== d && "name" === b) return this[S].querySelector("#name").textContent = d;
}
get name() {
return this.getAttribute("name") || "";
}
});
customElements.define("local-businesses", class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return [ return [
"filter" "filter"
@ -899,7 +903,7 @@ customElements.define("register-items-form", class extends PseudoForm {
].map((a)=>a.inputs); ].map((a)=>a.inputs);
} }
}); });
customElements.define("register-company-form", class extends PseudoForm { customElements.define("register-business-form", class extends PseudoForm {
constructor(){ constructor(){
super(); super();
let c = this[S] = this.attachShadow({ let c = this[S] = this.attachShadow({
@ -1226,7 +1230,7 @@ customElements.define("register-form", class extends HTMLElement {
<article> <article>
<register-user-type id="step-0"> </register-user-type> <register-user-type id="step-0"> </register-user-type>
<register-basic-form id="step-1"></register-basic-form> <register-basic-form id="step-1"></register-basic-form>
<register-company-form id="step-2"></register-company-form> <register-business-form id="step-2"></register-business-form>
<register-items-form id="step-3"></register-items-form> <register-items-form id="step-3"></register-items-form>
<register-submit-form id="step-4"></register-submit-form> <register-submit-form id="step-4"></register-submit-form>
<register-user-form id="step-40"></register-user-form> <register-user-form id="step-40"></register-user-form>
@ -1261,6 +1265,105 @@ customElements.define("register-form", class extends HTMLElement {
a < 0 || this.setAttribute("step", a); a < 0 || this.setAttribute("step", a);
} }
}); });
customElements.define("image-input", class extends HTMLElement {
constructor(){
super();
let b = this[S] = this.attachShadow({
mode: "closed"
});
b.innerHTML = `
<style>
:host { display: block; border: 1px solid black; }
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
input { position: absolute; top: -10px; left: -10px; display: none; }
#view { width: 200px; height: 200px; cursor: pointer; }
canvas { width: 200px; height: 200px; }
</style>
<article>
<section id="hidden">
<input id="file" type="file" accept="image/*" />
<img alt="" src="" />
</section>
<div id="view"><canvas width="200" height="200"></canvas></div>
</article>
`;
let c = new FileReader(), d = b.querySelector("#file"), e = b.querySelector("#view"), f = b.querySelector("img"), g = b.querySelector("canvas"), h = g.getContext("2d");
f.addEventListener("load", ()=>{
let a, b;
f.width > f.height ? (a = 200, b = 200 * f.height / f.width) : (a = 200 * f.width / f.height, b = 200), console.log(f.width, f.height), console.log(a, b), f.width = a, f.height = b, h.fillStyle = "#F00", h.rect(0, 0, 200, 200), h.drawImage(f, 0, 0, a, b);
}), d.addEventListener("change", (a)=>{
a.stopPropagation(), c.addEventListener("loadend", (a)=>{
a.total === a.loaded && (f.src = a.target.result || "");
}), c.readAsDataURL(a.target.files[0]);
}), e.addEventListener("click", (a)=>{
a.stopPropagation(), d.click();
});
}
});
customElements.define("business-item", class extends HTMLElement {
static get observedAttributes() {
return [
"name",
"price",
"picture-url"
];
}
constructor(){
super();
let b = this[S] = this.attachShadow({
mode: "closed"
});
b.innerHTML = `
<style>
:host { display: block; }
section { display: flex; justify-content: space-between; }
</style>
<section>
<div id="name"></div>
<price-input></price-input>
<image-input></image-input>
</section>
`;
}
connectedCallback() {
this.filter = this.getAttribute("filter");
}
attributeChangedCallback(a, b, c) {
if (b !== c && "filter" === a) return this.filter = c;
}
get name() {
return this.getAttribute("name");
}
set name(a) {
this.setAttribute("name", a), this.querySelector("#name").textContent = a;
}
get price() {
return this.getAttribute("price");
}
set price(a) {
this.setAttribute("price", a), this.querySelector("price-input").value = a;
}
get picture_url() {
return this.getAttribute("picture-url");
}
set picture_url(a) {
this.setAttribute("picture-url", a), this.querySelector("image-input").src = a;
}
});
customElements.define("business-items", class extends HTMLElement {
constructor(){
super();
let b = this[S] = this.attachShadow({
mode: "closed"
});
b.innerHTML = `
<style>
:host { display: block; }
</style>
<slot></slot>
`;
}
});
if (!document.querySelector("#facebook-jssdk")) { if (!document.querySelector("#facebook-jssdk")) {
window.fbAsyncInit = ()=>{ window.fbAsyncInit = ()=>{
FB.init({ FB.init({

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,5 @@
import "./form-navigation.js"; import "./form-navigation.js";
import "./local-service.js"; import "./local-businesses.js";
import "./local-service-item.js";
import "./local-services.js";
import "./login-form.js"; import "./login-form.js";
import "./ow-account.js"; import "./ow-account.js";
import "./nav/ow-nav.js"; import "./nav/ow-nav.js";
@ -9,6 +7,8 @@ import "./nav/ow-path.js";
import "./price/price-view"; import "./price/price-view";
import "./price/price-input"; import "./price/price-input";
import "./register-form.js"; import "./register-form.js";
import "./business-items";
import { fireFbReady } from "./shared.js"; import { fireFbReady } from "./shared.js";
if (!document.querySelector('#facebook-jssdk')) { if (!document.querySelector('#facebook-jssdk')) {

View File

@ -0,0 +1,18 @@
import { S } from "./shared"
import "./business-items/business-item";
customElements.define('business-items', class extends HTMLElement {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host { display: block; }
</style>
<slot></slot>
`;
}
});

View File

@ -0,0 +1,64 @@
import { S } from "../shared.js";
import "../shared/image-input";
customElements.define('business-item', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'price', 'picture-url']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host { display: block; }
section { display: flex; justify-content: space-between; }
</style>
<section>
<div id="name"></div>
<price-input></price-input>
<image-input></image-input>
</section>
`;
}
connectedCallback() {
this.filter = this.getAttribute('filter');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'filter':
return this.filter = newV;
}
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.querySelector('#name').textContent = v;
}
get price() {
return this.getAttribute('price');
}
set price(v) {
this.setAttribute('price', v);
this.querySelector('price-input').value = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
this.querySelector('image-input').src = v;
}
});

View File

@ -1,6 +1,9 @@
import { S } from "./shared"; import { S } from "./shared";
customElements.define('local-services', class extends HTMLElement { import "./local-businesses/local-business-item";
import "./local-businesses/local-business";
customElements.define('local-businesses', class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ['filter'] return ['filter']
} }

View File

@ -1,6 +1,6 @@
import { S } from "./shared"; import { S } from "../shared";
customElements.define('local-service-item', class extends HTMLElement { customElements.define('local-business-item', class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ['name', 'price'] return ['name', 'price']
} }

View File

@ -1,6 +1,6 @@
import { S } from "./shared"; import { S } from "../shared";
customElements.define('local-service', class extends HTMLElement { customElements.define('local-business', class extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ['name', 'service-id', 'state'] return ['name', 'service-id', 'state']
} }

View File

@ -3,7 +3,7 @@ import { S, FORM_STYLE } from "./shared";
import "./register-form/register-basic-form"; import "./register-form/register-basic-form";
import "./register-form/register-item-form-row.js"; import "./register-form/register-item-form-row.js";
import "./register-form/register-items-form.js"; import "./register-form/register-items-form.js";
import "./register-form/register-company-form"; import "./register-form/register-business-form";
import "./register-form/register-submit-form"; import "./register-form/register-submit-form";
import "./register-form/register-user-type"; import "./register-form/register-user-type";
import "./register-form/register-user-form"; import "./register-form/register-user-form";
@ -69,7 +69,7 @@ customElements.define('register-form', class extends HTMLElement {
<article> <article>
<register-user-type id="step-0"> </register-user-type> <register-user-type id="step-0"> </register-user-type>
<register-basic-form id="step-1"></register-basic-form> <register-basic-form id="step-1"></register-basic-form>
<register-company-form id="step-2"></register-company-form> <register-business-form id="step-2"></register-business-form>
<register-items-form id="step-3"></register-items-form> <register-items-form id="step-3"></register-items-form>
<register-submit-form id="step-4"></register-submit-form> <register-submit-form id="step-4"></register-submit-form>
<register-user-form id="step-40"></register-user-form> <register-user-form id="step-40"></register-user-form>

View File

@ -1,6 +1,6 @@
import { FORM_STYLE, S, PseudoForm } from "../shared"; import { FORM_STYLE, S, PseudoForm } from "../shared";
customElements.define('register-company-form', class extends PseudoForm { customElements.define('register-business-form', class extends PseudoForm {
constructor() { constructor() {
super(); super();

View File

@ -93,6 +93,10 @@ input[type="button"], input[type="submit"] {
width: auto; width: auto;
height: calc(1.5em + 0.75rem + 2px); height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem; padding: .375rem .75rem;
border: 1px solid #495057;
color: #495057;
background: white;
} }
`; `;

View File

@ -0,0 +1,67 @@
import { S } from "../shared.js";
customElements.define('image-input', class extends HTMLElement {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
:host { display: block; border: 1px solid black; }
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
input { position: absolute; top: -10px; left: -10px; display: none; }
#view { width: 200px; height: 200px; cursor: pointer; }
canvas { width: 200px; height: 200px; }
</style>
<article>
<section id="hidden">
<input id="file" type="file" accept="image/*" />
<img alt="" src="" />
</section>
<div id="view"><canvas width="200" height="200"></canvas></div>
</article>
`;
const f = new FileReader();
const input = shadow.querySelector('#file');
const view = shadow.querySelector('#view');
const img = shadow.querySelector('img');
const canvas = shadow.querySelector('canvas');
const ctx = canvas.getContext('2d');
img.addEventListener('load', () => {
let width, height;
if (img.width > img.height) {
width = 200;
height = (img.height * 200) / img.width;
} else {
width = (img.width * 200) / img.height;
height = 200;
}
console.log(img.width, img.height);
console.log(width, height);
img.width = width;
img.height = height;
// ctx.drawImage(img, 0, 0);
ctx.fillStyle = '#F00';
ctx.rect(0, 0, 200, 200);
ctx.drawImage(img, 0, 0, width, height);
});
input.addEventListener('change', ev => {
ev.stopPropagation();
f.addEventListener('loadend', (readerEvent) => {
if (readerEvent.total !== readerEvent.loaded)
return;
img.src = readerEvent.target.result || '';
});
f.readAsDataURL(ev.target.files[0]);
});
view.addEventListener('click', ev => {
ev.stopPropagation();
input.click();
});
}
});

View File

@ -1,12 +1,12 @@
INSERT INTO accounts (login, pass, email) INSERT INTO accounts (login, pass, email)
VALUES ('Foo', 'Bar', 'foo@example.com'); VALUES ('Foo', 'Bar', 'foo@example.com');
INSERT INTO local_services (name, description, owner_id) INSERT INTO local_businesses (name, description, owner_id)
VALUES ('Cheap Tees', 'Unlimited possiblities! You can move the water masks to create your own frame. It gives you an opportunity to fit the layers to your image. Create great final effect with seconds', 1), VALUES ('Cheap Tees', 'Unlimited possiblities! You can move the water masks to create your own frame. It gives you an opportunity to fit the layers to your image. Create great final effect with seconds', 1),
('Tema Model Agency Ltd', 'Special for website developers and app ui designers, to preview their apps in a professional way, showcasing details and focus on Responsive Design for Website and apps, Vol 09.', 1), ('Tema Model Agency Ltd', 'Special for website developers and app ui designers, to preview their apps in a professional way, showcasing details and focus on Responsive Design for Website and apps, Vol 09.', 1),
('Neet Online Test Series - Nots', 'Advanced, easy to edit mockup. It contains everything you need to create a realistic look of your project. Guarantees the a good look for bright and dark designs and perfect fit to the shape. Easy to navigate, well described layers, friendly help file.', 1); ('Neet Online Test Series - Nots', 'Advanced, easy to edit mockup. It contains everything you need to create a realistic look of your project. Guarantees the a good look for bright and dark designs and perfect fit to the shape. Easy to navigate, well described layers, friendly help file.', 1);
INSERT INTO local_service_items (name, price, local_service_id, item_order) INSERT INTO local_business_items (name, price, local_business_id, item_order)
VALUES ('Water Frame', 23423, 1, 1), VALUES ('Water Frame', 23423, 1, 1),
('Macbook Laptop Display 2.0', 927, 1, 2), ('Macbook Laptop Display 2.0', 927, 1, 2),
('Paper Band', 920, 1, 3), ('Paper Band', 920, 1, 3),

View File

@ -11,7 +11,7 @@ CREATE TYPE "Role" AS ENUM (
'Admin' 'Admin'
); );
CREATE TYPE "LocalServiceState" AS ENUM ( CREATE TYPE "LocalBusinessState" AS ENUM (
'Pending', 'Pending',
'Approved', 'Approved',
'Banned', 'Banned',
@ -32,17 +32,17 @@ CREATE TABLE tokens (
role "Role" not null default 'User' role "Role" not null default 'User'
); );
CREATE TABLE local_services ( CREATE TABLE local_businesses (
id serial unique not null primary key, id serial unique not null primary key,
owner_id int references accounts (id) not null, owner_id int references accounts (id) not null,
name text not null, name text not null,
description text not null, description text not null,
state "LocalServiceState" not null default 'Pending' state "LocalBusinessState" not null default 'Pending'
); );
CREATE TABLE local_service_items ( CREATE TABLE local_business_items (
id serial unique not null primary key, id serial unique not null primary key,
local_service_id int references local_services (id) not null, local_business_id int references local_businesses (id) not null,
name text not null, name text not null,
price bigint not null, price bigint not null,
item_order int not null item_order int not null

View File

@ -1,4 +1,5 @@
#![feature(drain_filter)] #![feature(drain_filter)]
#![feature(option_get_or_insert_default)]
use crate::routes::render_index; use crate::routes::render_index;
use actix_identity::{CookieIdentityPolicy, IdentityService}; use actix_identity::{CookieIdentityPolicy, IdentityService};
@ -11,12 +12,7 @@ mod utils;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
use tracing::Level; tracing_subscriber::fmt::init();
use tracing_subscriber::FmtSubscriber;
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
let pool = sqlx::postgres::PgPoolOptions::new() let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(8) .max_connections(8)
@ -33,6 +29,7 @@ async fn main() -> std::io::Result<()> {
App::new() App::new()
.wrap(actix_web::middleware::Compress::default()) .wrap(actix_web::middleware::Compress::default())
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.wrap(tracing_actix_web::TracingLogger::default())
.wrap(IdentityService::new(policy)) .wrap(IdentityService::new(policy))
.app_data(Data::new(pool.clone())) .app_data(Data::new(pool.clone()))
.configure(routes::configure) .configure(routes::configure)

View File

@ -28,7 +28,7 @@ pub enum Role {
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)]
pub enum LocalServiceState { pub enum LocalBusinessState {
Pending, Pending,
Approved, Approved,
Banned, Banned,
@ -36,7 +36,7 @@ pub enum LocalServiceState {
Internal, Internal,
} }
impl LocalServiceState { impl LocalBusinessState {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
Self::Pending => "Pending", Self::Pending => "Pending",
@ -68,13 +68,13 @@ pub struct LocalBusiness {
pub owner_id: i32, pub owner_id: i32,
pub name: String, pub name: String,
pub description: String, pub description: String,
pub state: LocalServiceState, pub state: LocalBusinessState,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LocalBusinessItem { pub struct LocalBusinessItem {
pub id: i32, pub id: i32,
pub local_service_id: i32, pub local_business_id: i32,
pub name: String, pub name: String,
pub price: i64, pub price: i64,
pub item_order: i32, pub item_order: i32,

View File

@ -3,16 +3,17 @@ use serde::{Deserialize, Serialize};
#[derive(Debug)] #[derive(Debug)]
pub enum Page { pub enum Page {
LocalServices, LocalBusinesses,
News, News,
Account, Account,
Register, Register,
Login, Login,
BusinessItems,
} }
impl Page { impl Page {
pub fn select_index(&self) -> &str { pub fn select_index(&self) -> &str {
if matches!(self, Page::LocalServices) { if matches!(self, Page::LocalBusinesses) {
"selected" "selected"
} else { } else {
"" ""
@ -34,21 +35,38 @@ impl Page {
"" ""
} }
} }
pub fn select_business_items(&self) -> &str {
if matches!(self, Page::BusinessItems) {
"selected"
} else {
""
}
}
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct BusinessItemInput { pub struct BusinessItemInput {
pub name: String, pub name: String,
pub price: u32, pub price: u32,
} }
impl BusinessItemInput {
pub fn new<S: Into<String>>(name: S, price: u32) -> Self {
Self {
name: name.into(),
price,
}
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct LocalService { pub struct LocalService {
pub id: i32, pub id: i32,
pub owner_id: i32, pub owner_id: i32,
pub name: String, pub name: String,
pub description: String, pub description: String,
pub state: db::LocalServiceState, pub state: db::LocalBusinessState,
pub items: Vec<db::LocalBusinessItem>, pub items: Vec<db::LocalBusinessItem>,
} }
@ -61,7 +79,7 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
description: service.description, description: service.description,
state: service.state, state: service.state,
items: items items: items
.drain_filter(|i| i.local_service_id == service.id) .drain_filter(|i| i.local_business_id == service.id)
.collect(), .collect(),
} }
} }

View File

@ -1,9 +1,50 @@
use actix_web::web::ServiceConfig; use actix_web::web::ServiceConfig;
use actix_web::{FromRequest, HttpRequest};
use serde::Serializer;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
mod restricted;
mod unrestricted; mod unrestricted;
pub use unrestricted::render_index; pub use unrestricted::render_index;
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
unrestricted::configure(config); unrestricted::configure(config);
restricted::configure(config);
}
pub struct Identity(actix_identity::Identity);
impl Debug for Identity {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.serialize_struct("Identity", 1)?;
Ok(())
}
}
impl Deref for Identity {
type Target = actix_identity::Identity;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromRequest for Identity {
type Error = actix_web::Error;
type Future = Pin<Box<dyn Future<Output = Result<Identity, actix_web::Error>>>>;
#[inline]
fn from_request(req: &HttpRequest, p: &mut actix_http::Payload) -> Self::Future {
use futures::FutureExt;
Box::pin(
actix_identity::Identity::from_request(req, p).map(|ident| match ident {
Ok(ident) => Ok(Self(ident)),
Err(e) => Err(e),
}),
)
}
} }

74
src/routes/restricted.rs Normal file
View File

@ -0,0 +1,74 @@
use crate::model::db;
use crate::model::view::Page;
use crate::routes::Identity;
use crate::utils;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{get, HttpResponse};
use askama::*;
use sqlx::PgPool;
#[derive(Debug, Template)]
#[template(path = "business-items.html")]
struct BusinessItemsTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
items: Vec<db::LocalBusinessItem>,
}
fn render_unauthorized() -> HttpResponse {
HttpResponse::Unauthorized()
.append_header(("Location", "/"))
.body("")
}
macro_rules! authorize {
($id: expr, $pool: expr) => {{
let account = match $id.identity() {
None => return render_unauthorized(),
Some(id) => utils::user_by_id(id, &*$pool).await,
};
match account {
Some(account) => account,
_ => return render_unauthorized(),
}
}};
}
#[get("/account/business-items")]
async fn business_items_page(db: Data<PgPool>, id: Identity) -> HttpResponse {
let pool = db.into_inner();
let account = authorize!(id, pool);
let items: Vec<db::LocalBusinessItem> = sqlx::query_as(
r#"
SELECT
id,
local_business_id,
name,
price,
item_order
FROM local_business_items
ORDER BY item_order DESC
"#,
)
.bind(account.id)
.fetch_all(&*pool)
.await
.map_err(|e| {
tracing::error!("{e}");
dbg!(&e);
e
})
.unwrap_or_default();
let page = BusinessItemsTemplate {
page: Page::BusinessItems,
error: None,
account: Some(account),
items,
};
HttpResponse::Ok().body(page.render().unwrap())
}
pub fn configure(config: &mut ServiceConfig) {
config.service(business_items_page);
}

View File

@ -1,30 +1,32 @@
use crate::model::db; use crate::model::db;
use crate::model::db::AccountType;
use crate::model::view::{self, Page}; use crate::model::view::{self, Page};
use crate::routes::Identity;
use crate::utils; use crate::utils;
use actix_files::Files; use actix_files::Files;
use actix_identity::Identity;
use actix_web::web::{Data, ServiceConfig}; use actix_web::web::{Data, ServiceConfig};
use actix_web::*; use actix_web::*;
use askama::Template; use askama::Template;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use tracing::*;
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
pub struct IndexTemplate { pub struct IndexTemplate {
services: Vec<crate::model::view::LocalService>, services: Vec<view::LocalService>,
account: Option<db::Account>, account: Option<db::Account>,
error: Option<String>, error: Option<String>,
page: Page, page: Page,
} }
#[tracing::instrument]
pub async fn render_index() -> HttpResponse { pub async fn render_index() -> HttpResponse {
HttpResponse::NotFound().body( HttpResponse::NotFound().body(
IndexTemplate { IndexTemplate {
services: vec![], services: vec![],
account: None, account: None,
error: None, error: None,
page: Page::LocalServices, page: Page::LocalBusinesses,
} }
.render() .render()
.unwrap(), .unwrap(),
@ -32,6 +34,7 @@ pub async fn render_index() -> HttpResponse {
} }
#[get("/")] #[get("/")]
#[tracing::instrument]
pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse { pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse {
let pool = db.into_inner(); let pool = db.into_inner();
let record = match id.identity() { let record = match id.identity() {
@ -43,10 +46,10 @@ pub async fn index(db: Data<sqlx::PgPool>, id: Identity) -> HttpResponse {
let services: Vec<LocalBusiness> = sqlx::query_as( let services: Vec<LocalBusiness> = sqlx::query_as(
r#" r#"
SELECT id, owner_id, name, description, state SELECT id, owner_id, name, description, state
FROM local_services FROM local_businesses
WHERE state != 'Banned' WHERE state != 'Banned'
GROUP BY id, state GROUP BY id, state
ORDER BY id ORDER BY id DESC
"#, "#,
) )
.fetch_all(&*pool) .fetch_all(&*pool)
@ -62,11 +65,12 @@ ORDER BY id
r#" r#"
SELECT SELECT
id, id,
local_service_id, local_business_id,
name, name,
price, price,
item_order item_order
FROM local_service_items FROM local_business_items
ORDER BY item_order DESC
"#, "#,
) )
.fetch_all(&*pool) .fetch_all(&*pool)
@ -92,7 +96,7 @@ FROM local_service_items
services, services,
account: record, account: record,
error: None, error: None,
page: Page::LocalServices, page: Page::LocalBusinesses,
} }
.render() .render()
.unwrap(); .unwrap();
@ -108,11 +112,15 @@ struct AccountTemplate {
} }
#[get("/account")] #[get("/account")]
#[tracing::instrument]
async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse { async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
let pool = db.into_inner(); let pool = db.into_inner();
let record = match id.identity() { let record = match id.identity() {
Some(id) => utils::user_by_id(id, &pool).await, Some(id) => utils::user_by_id(id, &pool).await,
_ => None, _ => {
id.forget();
None
}
}; };
HttpResponse::Ok().body( HttpResponse::Ok().body(
AccountTemplate { AccountTemplate {
@ -126,6 +134,7 @@ async fn account_page(id: Identity, db: Data<sqlx::PgPool>) -> HttpResponse {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct RegisterForm { struct RegisterForm {
email: String, email: String,
login: String, login: String,
@ -135,17 +144,67 @@ struct RegisterForm {
items: Option<Vec<view::BusinessItemInput>>, items: Option<Vec<view::BusinessItemInput>>,
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
#[serde(flatten)]
names: HashMap<String, String>,
}
#[tracing::instrument]
fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String, String>) {
let mut h = names
.into_iter()
.filter_map(|(name, value)| {
let mut name = name
.strip_prefix("items")?
.split('[')
.filter(|s| !s.is_empty())
.map(|s| s.strip_suffix(']').unwrap_or(s));
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
match name.next() {
Some(s @ ("name" | "price")) => Some((idx, s.to_string(), value)),
_ => None,
}
})
.fold(
HashMap::with_capacity(60),
|mut memo, (idx, field, value)| {
let item = memo
.entry(idx)
.or_insert_with(view::BusinessItemInput::default);
match field.as_str() {
"name" => {
item.name = value;
}
"price" => {
item.price = value.parse().unwrap_or_default();
}
_ => {}
};
memo
},
);
let mut ids = { h.keys().copied().collect::<Vec<_>>() };
ids.sort();
for id in ids {
if let Some(item) = h.remove(&id) {
items.push(item);
}
}
} }
#[post("/register")] #[post("/register")]
#[tracing::instrument]
async fn register( async fn register(
form: web::Form<RegisterForm>, form: web::Form<RegisterForm>,
db: Data<sqlx::PgPool>, db: Data<sqlx::PgPool>,
id: Identity, id: Identity,
) -> HttpResponse { ) -> HttpResponse {
let form = form.into_inner(); let mut form = form.into_inner();
{
process_items(form.items.get_or_insert_default(), form.names);
}
let pool = db.into_inner(); let pool = db.into_inner();
if form.account_type == AccountType::Admin { if form.account_type == db::AccountType::Admin {
return HttpResponse::BadRequest().body("Security breach attempt detected!"); return HttpResponse::BadRequest().body("Security breach attempt detected!");
} }
@ -204,14 +263,17 @@ RETURNING id, login, email, pass, facebook_id, account_type
} }
}; };
if matches!(form.account_type, AccountType::Business) { debug!("{:?}", form.account_type);
debug!("{:?}", form.items);
if matches!(form.account_type, db::AccountType::Business) {
let name = form.name.as_deref().unwrap_or_default(); let name = form.name.as_deref().unwrap_or_default();
let owner_id = account.id; let owner_id = account.id;
let description = form.description.as_deref().unwrap_or_default(); let description = form.description.as_deref().unwrap_or_default();
let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as( let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as(
r#" r#"
INSERT INTO local_services (name, owner_id, description) INSERT INTO local_businesses (name, owner_id, description)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, owner_id, name, description, state RETURNING id, owner_id, name, description, state
"#, "#,
@ -239,12 +301,12 @@ RETURNING id, owner_id, name, description, state
} }
}; };
for (idx, item) in form.items.as_deref().unwrap_or_default().iter().enumerate() { for (idx, item) in form.items.unwrap_or_default().iter().enumerate() {
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as( let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
r#" r#"
INSERT INTO local_service_items (local_service_id, name, price, item_order) INSERT INTO local_business_items (local_business_id, name, price, item_order)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, local_service_id, name, price, item_order RETURNING id, local_business_id, name, price, item_order
"#, "#,
) )
.bind(business.id) .bind(business.id)
@ -289,6 +351,7 @@ RETURNING id, local_service_id, name, price, item_order
} }
#[post("/logout")] #[post("/logout")]
#[tracing::instrument]
async fn logout(id: Identity) -> HttpResponse { async fn logout(id: Identity) -> HttpResponse {
id.forget(); id.forget();
HttpResponse::SeeOther() HttpResponse::SeeOther()
@ -298,7 +361,7 @@ async fn logout(id: Identity) -> HttpResponse {
services: vec![], services: vec![],
account: None, account: None,
error: None, error: None,
page: Page::LocalServices, page: Page::LocalBusinesses,
} }
.render() .render()
.unwrap(), .unwrap(),
@ -312,6 +375,7 @@ struct LoginForm {
} }
#[post("/login")] #[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<sqlx::PgPool>, id: Identity) -> HttpResponse {
let pool = db.into_inner(); let pool = db.into_inner();
let form = form.into_inner(); let form = form.into_inner();
@ -379,3 +443,25 @@ pub fn configure(config: &mut ServiceConfig) {
.service(logout) .service(logout)
.service(login); .service(login);
} }
#[cfg(test)]
mod tests {
use crate::model::view;
use std::collections::HashMap;
#[test]
fn parse_items() {
let mut items = Vec::with_capacity(0);
let mut names: HashMap<String, String> = HashMap::with_capacity(4);
names.insert("items[0][name]".into(), "a".into());
names.insert("items[0][price]".into(), "10".into());
names.insert("items[1][name]".into(), "b".into());
names.insert("items[1][price]".into(), "20".into());
super::process_items(&mut items, names);
let expected = vec![
view::BusinessItemInput::new("a", 10),
view::BusinessItemInput::new("b", 20),
];
assert_eq!(items, expected);
}
}

View File

@ -2,6 +2,7 @@ use crate::model::db;
use argon2::{Algorithm, Argon2, Params, Version}; use argon2::{Algorithm, Argon2, Params, Version};
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
#[tracing::instrument]
pub fn encrypt(pass: &str) -> password_hash::Result<String> { pub fn encrypt(pass: &str) -> password_hash::Result<String> {
tracing::debug!("Hashing password {:?}", pass); tracing::debug!("Hashing password {:?}", pass);
Ok( Ok(
@ -11,6 +12,7 @@ pub fn encrypt(pass: &str) -> password_hash::Result<String> {
) )
} }
#[tracing::instrument]
pub fn validate(pass: &str, pass_hash: &str) -> password_hash::Result<()> { pub fn validate(pass: &str, pass_hash: &str) -> password_hash::Result<()> {
tracing::debug!("Validating password {:?} {:?}", pass, pass_hash); tracing::debug!("Validating password {:?} {:?}", pass, pass_hash);
@ -20,6 +22,7 @@ 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> { pub async fn user_by_id(id: String, pool: &sqlx::PgPool) -> Option<db::Account> {
match sqlx::query_as( match sqlx::query_as(
r#" r#"