init
This commit is contained in:
commit
5469fda5de
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
2754
Cargo.lock
generated
Normal file
2754
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "oswilno"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = "*"
|
||||||
|
actix-web = "*"
|
||||||
|
actix-cors = "*"
|
||||||
|
actix-rt = "*"
|
||||||
|
actix-files = "*"
|
||||||
|
# actix-web-4-validator = { version = "3.2.0", default-features = false }
|
||||||
|
actix-4-jwt-auth = "0.4.2"
|
||||||
|
# actix-web-security = "*"
|
||||||
|
askama = { version = "*" }
|
||||||
|
validator = { version = "0.14", features = ["derive"] }
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
serde_json = { version = "*" }
|
||||||
|
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
||||||
|
uuid = { version = "*", features = ["serde"] }
|
||||||
|
chrono = { version = "*", features = ["serde"] }
|
||||||
|
gumdrop = "*"
|
||||||
|
tracing = "*"
|
||||||
|
tracing-subscriber = "*"
|
||||||
|
tracing-actix-web = "*"
|
3
askama.toml
Normal file
3
askama.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[general]
|
||||||
|
dirs = ["assets/templates"]
|
||||||
|
whitespace = "preserve"
|
64
assets/css/app.css
Normal file
64
assets/css/app.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* @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('http://fonts.cdnfonts.com/css/noto-sans');
|
||||||
|
|
||||||
|
main {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
article {
|
||||||
|
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"));
|
||||||
|
height: 200px;
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
max-width: 1200px;
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 50px;
|
||||||
|
text-shadow: 2px 2px 2px #c5d1d8;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
width: 300px;
|
||||||
|
line-height: 4;
|
||||||
|
}
|
||||||
|
}
|
48
assets/css/reset.css
Normal file
48
assets/css/reset.css
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
BIN
assets/images/background.jpeg
Normal file
BIN
assets/images/background.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 KiB |
BIN
assets/images/background.webp
Normal file
BIN
assets/images/background.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
207
assets/js/app.js
Normal file
207
assets/js/app.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
const S = Symbol();
|
||||||
|
|
||||||
|
customElements.define('local-services', class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['filter']
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||||
|
shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
* { font-family: 'Noto Sans', sans-serif; }
|
||||||
|
::slotted(local-service[local-services-visible="invisible"]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<section>
|
||||||
|
<input type="text" id="filter" />
|
||||||
|
</section>
|
||||||
|
<section id="items">
|
||||||
|
<slot name="services"></slot>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
{
|
||||||
|
const filter = shadow.querySelector('#filter');
|
||||||
|
let t = null;
|
||||||
|
filter.addEventListener('change', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.filter = ev.target.value;
|
||||||
|
});
|
||||||
|
filter.addEventListener('keyup', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (t) clearTimeout(t);
|
||||||
|
t = setTimeout(() => {
|
||||||
|
this.filter = ev.target.value;
|
||||||
|
t = null;
|
||||||
|
}, 1000 / 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.filter = this.getAttribute('filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldV, newV) {
|
||||||
|
if (oldV == newV) return;
|
||||||
|
switch (name) {
|
||||||
|
case 'filter': return this.filter = newV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get filter() {
|
||||||
|
return this.getAttribute('filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
set filter(value) {
|
||||||
|
this.setAttribute('filter', value);
|
||||||
|
for (const el of this.querySelectorAll('local-service')) {
|
||||||
|
if (!el.name) continue;
|
||||||
|
if (el.name.includes(value)) {
|
||||||
|
el.setAttribute('local-services-visible', 'visible');
|
||||||
|
} else {
|
||||||
|
el.setAttribute('local-services-visible', 'invisible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('local-service', class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['name', 'service-id', 'state']
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||||
|
shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
* { font-family: 'Noto Sans', sans-serif; }
|
||||||
|
</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(name, oldV, newV) {
|
||||||
|
switch (name) {
|
||||||
|
case 'name': return this[S].querySelector('#name').textContent = newV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getAttribute('name') || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
customElements.define('local-service-item', class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['name', 'price']
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||||
|
shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
* { font-family: 'Noto Sans', sans-serif; }
|
||||||
|
#item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
#price {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<section id="item">
|
||||||
|
<h3 id="name"></h3>
|
||||||
|
<ow-price id="price" />
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this[S].querySelector('#name').textContent = this.getAttribute('name');
|
||||||
|
this[S].querySelector('#price').value = this.price();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldV, newV) {
|
||||||
|
switch (name) {
|
||||||
|
case 'name': return this[S].querySelector('#name').textContent = newV;
|
||||||
|
case 'price': return this[S].querySelector('#price').value = newV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
price(s) {
|
||||||
|
const n = parseInt(s || this.getAttribute('price'));
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('ow-price', class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['value', 'currency']
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||||
|
shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
* { font-family: 'Noto Sans', sans-serif; }
|
||||||
|
#price {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<span id="price"></span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this[S].querySelector('#price').textContent = this.formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldV, newV) {
|
||||||
|
switch (name) {
|
||||||
|
case 'price': {
|
||||||
|
this.value = newV;
|
||||||
|
this[S].querySelector('#price').textContent = this.formatted;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get formatted() {
|
||||||
|
let v = this.value;
|
||||||
|
let major = Math.ceil(v / 100);
|
||||||
|
let minor = v % 100;
|
||||||
|
let formatted = `${major},${ minor < 10 ? `0${minor}` : minor }`;
|
||||||
|
return `${formatted}${this.currency}`
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
const n = parseInt(this.getAttribute('value'));
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(v) {
|
||||||
|
this.setAttribute('value', v)
|
||||||
|
}
|
||||||
|
|
||||||
|
get currency() {
|
||||||
|
return this.getAttribute('currency') || 'PLN'
|
||||||
|
}
|
||||||
|
});
|
38
assets/templates/index.html
Normal file
38
assets/templates/index.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OSWilno</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="/assets/css/reset.css" rel="stylesheet" />
|
||||||
|
<link href="/assets/css/app.css" rel="stylesheet" />
|
||||||
|
<script type="module" src=/assets/js/app.js></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="bg">
|
||||||
|
<h1>OS Wilno</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
<h1>Lokalne usługi</h1>
|
||||||
|
<local-services>
|
||||||
|
{% for service in services %}
|
||||||
|
<local-service slot="services" service-id="{{service.id}}" name="{{service.name}}" state="{{service.state.to_str()}}">
|
||||||
|
{% for line in service.description.lines() %}
|
||||||
|
<p slot="description">{{line}}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for item in service.items %}
|
||||||
|
<local-service-item slot="item" name="{{item.name}}" price="{{item.price}}">
|
||||||
|
</local-service-item>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</local-service>
|
||||||
|
{% endfor %}
|
||||||
|
</local-services>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
49
migrations/20220630202317_add_local_services.sql
Normal file
49
migrations/20220630202317_add_local_services.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
CREATE TABLE accounts (
|
||||||
|
id serial unique not null primary key,
|
||||||
|
login text not null,
|
||||||
|
pass text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "Role" AS ENUM (
|
||||||
|
'User',
|
||||||
|
'Admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "LocalServiceState" AS ENUM (
|
||||||
|
'Pending',
|
||||||
|
'Approved',
|
||||||
|
'Banned',
|
||||||
|
'Pinned',
|
||||||
|
'Internal'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tokens (
|
||||||
|
id serial unique not null primary key,
|
||||||
|
claims jsonb not null,
|
||||||
|
iss text not null default 'oswilno', /* issuer */
|
||||||
|
sub int references accounts (id), /* subject */
|
||||||
|
aud text not null default 'public', /* audience */
|
||||||
|
exp timestamp not null, /* expiration time */
|
||||||
|
nbt timestamp not null default now(), /* not before time */
|
||||||
|
iat timestamp not null default now(), /* issued at time */
|
||||||
|
jti uuid not null unique, /* JWT ID - unique */
|
||||||
|
role "Role" not null default 'User'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE local_services (
|
||||||
|
id serial unique not null primary key,
|
||||||
|
owner_id int references accounts (id) not null,
|
||||||
|
name text not null,
|
||||||
|
description text not null,
|
||||||
|
state "LocalServiceState" not null default 'Pending'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE local_service_items (
|
||||||
|
id serial unique not null primary key,
|
||||||
|
local_service_id int references local_services (id) not null,
|
||||||
|
name text not null,
|
||||||
|
price bigint not null,
|
||||||
|
item_order int not null
|
||||||
|
);
|
11
src/auth.rs
Normal file
11
src/auth.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FoundClaims {
|
||||||
|
pub iss: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub aud: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub email_verified: Option<bool>,
|
||||||
|
}
|
34
src/main.rs
Normal file
34
src/main.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#![feature(drain_filter)]
|
||||||
|
use actix_web::{App, HttpServer, web::Data};
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod model;
|
||||||
|
mod routes;
|
||||||
|
|
||||||
|
#[actix_web::main] // or #[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use tracing::{Level};
|
||||||
|
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()
|
||||||
|
.max_connections(8)
|
||||||
|
.min_connections(8)
|
||||||
|
.connect(&std::env::var("DATABASE_URL").expect("No database connection url"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.app_data(Data::new(pool.clone()))
|
||||||
|
.configure(routes::configure)
|
||||||
|
})
|
||||||
|
.bind(("0.0.0.0", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
71
src/model/db.rs
Normal file
71
src/model/db.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{FromRow, Type};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct Account {
|
||||||
|
pub id: i32,
|
||||||
|
pub login: String,
|
||||||
|
pub pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Type)]
|
||||||
|
pub enum Role {
|
||||||
|
User,
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)]
|
||||||
|
pub enum LocalServiceState {
|
||||||
|
Pending,
|
||||||
|
Approved,
|
||||||
|
Banned,
|
||||||
|
Pinned,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalServiceState {
|
||||||
|
pub fn to_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Pending => "Pending",
|
||||||
|
Self::Approved => "Approved",
|
||||||
|
Self::Banned => "Banned",
|
||||||
|
Self::Pinned => "Pinned",
|
||||||
|
Self::Internal => "Internal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct Token {
|
||||||
|
pub id: i32,
|
||||||
|
pub claims: HashMap<String, String>,
|
||||||
|
pub iss: String, /* issuer */
|
||||||
|
pub sub: i32, /* subject */
|
||||||
|
pub aud: String, /* audience */
|
||||||
|
pub exp: NaiveDateTime, /* expiration time */
|
||||||
|
pub nbt: NaiveDateTime, /* not before time */
|
||||||
|
pub iat: NaiveDateTime, /* issued at time */
|
||||||
|
pub jti: Uuid, /* JWT ID - unique */
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct LocalService {
|
||||||
|
pub id: i32,
|
||||||
|
pub owner_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub state: LocalServiceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct LocalServiceItem {
|
||||||
|
pub id: i32,
|
||||||
|
pub local_service_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub price: i64,
|
||||||
|
pub item_order: i32,
|
||||||
|
}
|
2
src/model/mod.rs
Normal file
2
src/model/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod db;
|
||||||
|
pub mod view;
|
27
src/model/view.rs
Normal file
27
src/model/view.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use crate::model::db;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LocalService {
|
||||||
|
pub id: i32,
|
||||||
|
pub owner_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub state: db::LocalServiceState,
|
||||||
|
pub items: Vec<db::LocalServiceItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'v> From<(db::LocalService, &'v mut Vec<db::LocalServiceItem>)> for LocalService {
|
||||||
|
fn from((service, items): (db::LocalService, &'v mut Vec<db::LocalServiceItem>)) -> Self {
|
||||||
|
Self {
|
||||||
|
id: service.id,
|
||||||
|
owner_id: service.owner_id,
|
||||||
|
name: service.name,
|
||||||
|
description: service.description,
|
||||||
|
state: service.state,
|
||||||
|
items: items
|
||||||
|
.drain_filter(|i| i.local_service_id == service.id)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/routes/mod.rs
Normal file
7
src/routes/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use actix_web::web::ServiceConfig;
|
||||||
|
|
||||||
|
mod unrestricted;
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
unrestricted::configure(config);
|
||||||
|
}
|
75
src/routes/unrestricted.rs
Normal file
75
src/routes/unrestricted.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use actix_files::Files;
|
||||||
|
use actix_web::web::{Data, ServiceConfig};
|
||||||
|
use actix_web::*;
|
||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
pub struct IndexTemplate {
|
||||||
|
pub services: Vec<crate::model::view::LocalService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn index(db: Data<sqlx::PgPool>) -> HttpResponse {
|
||||||
|
let pool = db.into_inner();
|
||||||
|
let (services, mut items) = {
|
||||||
|
use crate::model::db::{LocalService, LocalServiceItem};
|
||||||
|
let services: Vec<LocalService> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, name, description, state
|
||||||
|
FROM local_services
|
||||||
|
WHERE state != 'Banned'
|
||||||
|
GROUP BY id, state
|
||||||
|
ORDER BY id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&*pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("{e}");
|
||||||
|
dbg!(&e);
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let items: Vec<LocalServiceItem> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
local_service_id,
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
item_order
|
||||||
|
FROM local_service_items
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&*pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("{e}");
|
||||||
|
dbg!(&e);
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
(services, items)
|
||||||
|
};
|
||||||
|
|
||||||
|
let services: Vec<_> = {
|
||||||
|
use crate::model::view::*;
|
||||||
|
services
|
||||||
|
.into_iter()
|
||||||
|
.map(|service| LocalService::from((service, &mut items)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = IndexTemplate { services }.render().unwrap();
|
||||||
|
HttpResponse::Ok().body(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config
|
||||||
|
.service(Files::new("/assets/images", "./assets/images"))
|
||||||
|
.service(Files::new("/assets/css", "./assets/css"))
|
||||||
|
.service(Files::new("/assets/js", "./assets/js").use_etag(true).prefer_utf8(true).show_files_listing())
|
||||||
|
.service(index);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user