Compare commits

...

19 Commits

Author SHA1 Message Date
aead788292 Show timestamp 2023-11-09 13:42:40 -05:00
e639604ef6 Added timestamp to msg object 2023-11-07 16:19:48 -05:00
dfa0a6a7e8 Fix mismatched button margins 2023-11-07 12:59:07 -05:00
784dd60bef Fix Login saving old user state 2023-11-07 12:57:21 -05:00
9244e94dd7 Fix ThemeToggle button size 2023-11-07 12:11:21 -05:00
6d8ba5cc77 Use better way to check if logged in 2023-11-06 11:28:10 -05:00
6e5915fbbd Add new message style 2023-11-06 11:00:28 -05:00
6d24f03648 Switch to object oriented message format 2023-11-05 17:00:17 -05:00
cd57573288 Added basic app wide user state 2023-11-05 12:15:11 -05:00
3d9c211106 Add basic header 2023-11-05 08:43:35 -05:00
c66d68572d Use fixed positioning for input 2023-11-02 14:16:46 -04:00
77ab2cb769 Clean up imports 2023-11-01 17:24:45 -04:00
eb9cbd9f66 Clean up src 2023-11-01 17:15:29 -04:00
eba7e4b0b7 Add chat component 2023-11-01 13:51:39 -04:00
54efb85dc3 Use wss in test page when connected with https 2023-11-01 02:25:12 -04:00
7a7a4b7fce Use relative link for ws on test page 2023-11-01 02:09:28 -04:00
d27883bc97 Caddy fmt 2023-11-01 01:50:05 -04:00
748b7968cd Add chat websocket 2023-11-01 00:14:34 -04:00
62621e7fe6 Persist themes with user default 2023-10-30 23:45:37 -04:00
11 changed files with 310 additions and 165 deletions

18
Cargo.lock generated
View File

@@ -242,7 +242,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen",
"windows-targets", "windows-targets",
] ]
@@ -504,11 +507,14 @@ version = "1.3.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bcrypt", "bcrypt",
"chrono",
"futures-util", "futures-util",
"poem", "poem",
"poem-openapi", "poem-openapi",
"serde", "serde",
"serde_json",
"sqlx", "sqlx",
"thiserror",
"tokio", "tokio",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -1619,9 +1625,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.107" version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -2019,18 +2025,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.49" version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.49" version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -10,10 +10,10 @@ futures-util = "0.3.17"
tracing-subscriber = { version ="0.3.9", features = ["env-filter"] } tracing-subscriber = { version ="0.3.9", features = ["env-filter"] }
sqlx = { version = "0.7.2", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.7.2", features = ["runtime-tokio", "sqlite"] }
bcrypt = "0.15.0" bcrypt = "0.15.0"
anyhow = "1.0.75"
poem-openapi = { version = "3.0.5", features = ["websocket"] } poem-openapi = { version = "3.0.5", features = ["websocket"] }
serde = "1.0.190" serde = "1.0.190"
thiserror = "1.0.50"
[profile.dev.package.sqlx-macros] anyhow = "1.0.75"
opt-level = 3 serde_json = "1.0.108"
chrono = { version = "0.4.31", features = ["serde"] }

View File

@@ -1,18 +1,22 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, onBeforeMount } from 'vue'
import Signup from './components/Signup.vue' import Signup from './components/Signup.vue'
import Login from './components/Login.vue' import Login from './components/Login.vue'
import Clicker from './components/Clicker.vue' import Clicker from './components/Clicker.vue'
import ThemeToggle from './components/ThemeToggle.vue' import Chat from './components/Chat.vue'
import Header from './components/Header.vue'
const routes = { const routes = {
'/': Clicker, '/': Chat,
'/signup': Signup, '/signup': Signup,
'/login': Login, '/login': Login,
'/button': Clicker, '/button': Clicker,
'/chat': Chat,
} }
let user = ref('')
const currentPath = ref(window.location.hash) const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
@@ -23,17 +27,23 @@ const currentView = computed(() => {
return routes[currentPath.value.slice(1) || '/'] || NotFound return routes[currentPath.value.slice(1) || '/'] || NotFound
}) })
onBeforeMount(async () => {
let resp = await fetch("/api/user")
if (resp.ok) {
user.value = await resp.json()
} else {
window.location.hash = "/login"
}
})
</script> </script>
<template> <template>
<Header v-model=user />
<Suspense> <Suspense>
<component :is="currentView" /> <component :is="currentView" v-model=user />
<template #fallback> <template #fallback>
Loading... Loading...
</template> </template>
</Suspense> </Suspense>
<div class="position-fixed bottom-0 end-0 mb-3 me-3">
<ThemeToggle />
</div>
</template> </template>

View File

@@ -0,0 +1,80 @@
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const input = ref('')
const msgs = ref([{ username: 'Server', message: 'Welcome to Chat App' }, { username: 'Server', message: 'This is an example message' }])
let ws;
async function sendMsg(ev) {
ev.preventDefault()
let msg = input.value
input.value = ''
ws.send(msg)
}
onMounted(async () => {
let proto = window.location.protocol == 'https:' ? 'wss://' : 'ws://'
let host = window.location.host
ws = new WebSocket(proto + host + "/api/ws")
ws.onerror = function (event) {
console.log('WebSocket connection failed:', event);
emit('update:modelValue', '')
//alert("Not logged in!")
window.location.hash = "/login"
};
ws.onmessage = async function (event) {
console.log("event data: " + event.data)
msgs.value.push(JSON.parse(event.data))
console.log("msgs:" + msgs.value)
};
console.log(ws)
})
</script>
<template>
<main>
<div class="overflow-auto position-relative pb-5">
<!--
<div v-for="msg in msgs" class="card m-3 text-bg-secondary">
<div class="card-body">{{ msg.username }}: {{ msg.message }}</div>
</div>
<div v-for="msg in msgs" class="input-group">
<div class="alert alert-primary">{{ msg.username }}</div>
<div class="alert alert-secondary">{{ msg.message }}</div>
</div>
-->
<div v-for="msg in msgs" class="d-flex text-body-secondary pt-2 px-3 border-bottom">
<p class="pb-2 mb-0 small lh-sm word-wrap">
<div class="d-flex justify-content-between">
<strong class="d-block text-gray-dark">{{ msg.username }}</strong>
<div>{{ msg.timestamp }}</div>
</div>
{{ msg.message }}
</p>
</div>
<form onsubmit="event.preventDefault();" class="container-fluid fixed-bottom mb-3">
<div class="input-group">
<input v-model="input" type="text" class="form-control text-bg-secondary" placeholder="Type message here"
aria-label="Chat message" aria-describedby="button-submit">
<button @click="sendMsg" class="btn btn-primary" id="button-submit">Send</button>
</div>
</form>
</div>
</main>
</template>
<style scoped>
.word-wrap {
word-break: break-all;
width: 100vw;
}
</style>

View File

@@ -1,6 +1,9 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
async function get_clicks() { async function get_clicks() {
const api_url = '/api/num' const api_url = '/api/num'
@@ -12,6 +15,7 @@ async function get_clicks() {
return ans return ans
} }
else { else {
emit('update:modelValue', '')
window.location.hash = "/login" window.location.hash = "/login"
return 'ERROR' return 'ERROR'
} }
@@ -38,6 +42,7 @@ async function signout(ev) {
} }
let resp = await fetch(api_url, fetch_options) let resp = await fetch(api_url, fetch_options)
console.log(resp) console.log(resp)
emit('update:modelValue', '')
window.location.reload() window.location.reload()
} }

View File

@@ -0,0 +1,66 @@
<script setup>
import ThemeToggle from './ThemeToggle.vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
async function signout(ev) {
ev.preventDefault()
const api_url = '/api/auth'
const fetch_options = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}
let resp = await fetch(api_url, fetch_options)
console.log(resp)
emit('update:modelValue', '')
window.location.reload()
}
</script>
<template>
<nav class="navbar bg-body-tertiary navbar-expand-lg shadow-sm position-relative" aria-label="Chat">
<div class="container-fluid">
<a class="navbar-brand" href="#">Chat</a>
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasNavbarLight"
aria-controls="offcanvasNavbarLight" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNavbarLight"
aria-labelledby="offcanvasNavbarLightLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasNavbarLightLabel">Chat</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body px-2">
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<li v-if="modelValue.hasOwnProperty('username')" class="nav-item m-2">
<button @click="signout" class="btn btn-outline-success">Sign out</button>
</li>
<li class="nav-item m-2">
<div>
<ThemeToggle />
</div>
</li>
</ul>
</div>
</div>
</div>
</nav>
</template>
<style scoped>
.fill-height {
height: 100vh;
}
</style>

View File

@@ -5,6 +5,9 @@ import IconLogo from './icons/IconLogo.vue';
const user = ref('') const user = ref('')
const pass = ref('') const pass = ref('')
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
async function login(ev) { async function login(ev) {
ev.preventDefault() ev.preventDefault()
const api_url = '/api/auth' const api_url = '/api/auth'
@@ -17,18 +20,9 @@ async function login(ev) {
} }
} }
console.log(user.value)
console.log(pass.value)
console.log(api_url)
console.log(payload)
console.log(fetch_options)
let resp = await fetch(api_url, fetch_options) let resp = await fetch(api_url, fetch_options)
console.log(resp)
if (resp.ok) { if (resp.ok) {
let ans = await resp.json() emit('update:modelValue', await resp.json())
console.log(ans)
window.location.hash = "/" window.location.hash = "/"
} else { } else {
user.value = '' user.value = ''
@@ -38,8 +32,8 @@ async function login(ev) {
onMounted(async () => { onMounted(async () => {
let resp = await fetch("/api/user") let resp = await fetch("/api/user")
console.log(resp)
if (resp.ok) { if (resp.ok) {
emit('update:modelValue', await resp.json())
window.location.hash = "/" window.location.hash = "/"
} }
}) })
@@ -71,11 +65,6 @@ onMounted(async () => {
</template> </template>
<style scoped> <style scoped>
/* html,
body {
height: 100%;
} */
.form-signin { .form-signin {
max-width: 330px; max-width: 330px;
padding: 1rem; padding: 1rem;

View File

@@ -2,6 +2,9 @@
import { ref } from 'vue' import { ref } from 'vue'
import IconLogo from './icons/IconLogo.vue'; import IconLogo from './icons/IconLogo.vue';
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const user = ref('') const user = ref('')
const pass = ref('') const pass = ref('')
const refe = ref('') const refe = ref('')
@@ -10,7 +13,6 @@ const err = ref('')
async function signup(ev) { async function signup(ev) {
ev.preventDefault() ev.preventDefault()
console.log(ev)
const api_url = '/api/user' const api_url = '/api/user'
const payload = { username: user.value, password: pass.value, referral: refe.value } const payload = { username: user.value, password: pass.value, referral: refe.value }
const fetch_options = { const fetch_options = {
@@ -21,19 +23,12 @@ async function signup(ev) {
} }
} }
console.log(user.value)
console.log(pass.value)
console.log(api_url)
console.log(payload)
console.log(fetch_options)
let resp = await fetch(api_url, fetch_options) let resp = await fetch(api_url, fetch_options)
console.log(resp)
if (resp.ok) { if (resp.ok) {
let user = resp.json()
emit('update:modelValue', user)
err.value = '' err.value = ''
let ans = await resp.json()
console.log(ans)
window.location.hash = "/" window.location.hash = "/"
} }
else { else {

View File

@@ -9,30 +9,33 @@ const theme_icons = {
'dark': IconMoonStars, 'dark': IconMoonStars,
} }
const currentTheme = ref('light') let storedTheme = localStorage.getItem('theme')
if (!storedTheme) {
storedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const currentTheme = ref(storedTheme)
document.documentElement.setAttribute('data-bs-theme', storedTheme)
async function click(ev) { async function click(ev) {
ev.preventDefault() ev.preventDefault()
let newTheme = 'light'
if (currentTheme.value == 'light') { if (currentTheme.value == 'light') {
currentTheme.value = 'dark' newTheme = 'dark'
} else {
currentTheme.value = 'light'
}
document.documentElement.setAttribute('data-bs-theme', currentTheme.value)
} }
const currentIcon = computed(() => { currentTheme.value = newTheme
let icon = theme_icons[currentTheme.value] document.documentElement.setAttribute('data-bs-theme', newTheme)
//console.log(currentTheme.value) localStorage.setItem('theme', newTheme)
//console.log(icon) }
return icon
}) const currentIcon = computed(() => theme_icons[currentTheme.value])
</script> </script>
<template> <template>
<button type="button" @click="click" class="btn btn-primary align-items-center w-100 py-2"> <button type="button" @click="click" class="btn btn-primary align-items-center py-2">
<component :is="currentIcon" /> <component :is="currentIcon" />
</button> </button>
</template> </template>

View File

@@ -1,108 +1,24 @@
//use anyhow::Result; use chrono::{offset::Utc, DateTime};
//use bcrypt::{hash, verify, DEFAULT_COST};
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use poem::{ use poem::{
endpoint::StaticFilesEndpoint, endpoint::StaticFilesEndpoint,
//get, error::ResponseError,
handler, http::StatusCode,
listener::TcpListener, listener::TcpListener,
session::{CookieConfig, MemoryStorage, ServerSession, Session}, session::{CookieConfig, MemoryStorage, ServerSession, Session},
web::{ web::websocket::{BoxWebSocketUpgraded, Message, WebSocket},
websocket::{Message, WebSocket}, EndpointExt, Result, Route, Server,
Data, Html, Path,
},
EndpointExt,
IntoResponse,
Result,
Route,
Server,
}; };
use poem_openapi::{ use poem_openapi::{
payload::Json, types::Password, ApiResponse, Object, OpenApi, OpenApiService, Tags, payload::Json,
registry::{MetaResponse, MetaResponses, Registry},
types::Password,
ApiResponse, Object, OpenApi, OpenApiService, Tags,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tokio::sync::broadcast::Sender;
#[handler]
fn index() -> Html<&'static str> {
Html(
r###"
<body>
<form id="loginForm">
Name: <input id="nameInput" type="text" />
<button type="submit">Login</button>
</form>
<form id="sendForm" hidden>
Text: <input id="msgInput" type="text" />
<button type="submit">Send</button>
</form>
<textarea id="msgsArea" cols="50" rows="30" hidden></textarea>
</body>
<script>
let ws;
const loginForm = document.querySelector("#loginForm");
const sendForm = document.querySelector("#sendForm");
const nameInput = document.querySelector("#nameInput");
const msgInput = document.querySelector("#msgInput");
const msgsArea = document.querySelector("#msgsArea");
nameInput.focus();
loginForm.addEventListener("submit", function(event) {
event.preventDefault();
loginForm.hidden = true;
sendForm.hidden = false;
msgsArea.hidden = false;
msgInput.focus();
ws = new WebSocket("ws://127.0.0.1:2000/api/ws/" + nameInput.value);
ws.onmessage = function(event) {
msgsArea.value += event.data + "\r\n";
}
});
sendForm.addEventListener("submit", function(event) {
event.preventDefault();
ws.send(msgInput.value);
msgInput.value = "";
});
</script>
"###,
)
}
#[handler]
fn ws(
Path(name): Path<String>,
ws: WebSocket,
sender: Data<&tokio::sync::broadcast::Sender<String>>,
) -> impl IntoResponse {
let sender = sender.clone();
let mut receiver = sender.subscribe();
ws.on_upgrade(move |socket| async move {
let (mut sink, mut stream) = socket.split();
tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(text) = msg {
if sender.send(format!("{name}: {text}")).is_err() {
break;
}
}
}
});
tokio::spawn(async move {
while let Ok(msg) = receiver.recv().await {
if sink.send(Message::Text(msg)).await.is_err() {
break;
}
}
});
})
}
#[derive(Debug, Object, Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Object, Clone, Eq, PartialEq, Serialize, Deserialize)]
struct User { struct User {
@@ -159,6 +75,7 @@ enum NumResponse {
#[oai(status = 403)] #[oai(status = 403)]
AuthError, AuthError,
} }
#[derive(Tags)] #[derive(Tags)]
enum ApiTags { enum ApiTags {
/// Operations about user /// Operations about user
@@ -167,12 +84,79 @@ enum ApiTags {
async fn valid_code(code: &str) -> bool { async fn valid_code(code: &str) -> bool {
"changeme" == code "changeme" == code
} }
struct Api { #[derive(Debug, thiserror::Error)]
db: Pool<Sqlite>, #[error("API Error")]
signup_open: bool, //TODO Should be in the db so it can change without restart struct ApiError();
impl ResponseError for ApiError {
fn status(&self) -> StatusCode {
StatusCode::FORBIDDEN
} }
}
impl ApiResponse for ApiError {
fn meta() -> MetaResponses {
MetaResponses {
responses: vec![MetaResponse {
description: "An Error response",
status: Some(403),
content: vec![],
headers: vec![],
}],
}
}
fn register(_registry: &mut Registry) {}
}
#[OpenApi] #[OpenApi]
impl Api { impl Api {
#[oai(path = "/ws", method = "get")]
async fn ws(
&self,
websock: WebSocket,
session: &Session,
) -> Result<BoxWebSocketUpgraded, ApiError> {
let name = session.get::<String>("user").ok_or(ApiError {})?;
let sender = self.channel.clone();
let mut receiver = sender.subscribe();
let x = websock
.on_upgrade(move |socket| async move {
let (mut sink, mut stream) = socket.split();
tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(text) = msg {
if sender
.send(ChatMsg {
username: name.clone(),
message: text,
timestamp: Utc::now(),
})
.is_err()
{
break;
}
}
}
});
tokio::spawn(async move {
while let Ok(msg) = receiver.recv().await {
if sink
.send(Message::Text(serde_json::to_string(&msg).unwrap()))
.await
.is_err()
{
break;
}
}
});
})
.boxed();
Result::Ok(x)
}
#[oai(path = "/user", method = "post", tag = "ApiTags::User")] #[oai(path = "/user", method = "post", tag = "ApiTags::User")]
async fn create_user(&self, user_form: Json<NewUser>, session: &Session) -> NewUserResponse { async fn create_user(&self, user_form: Json<NewUser>, session: &Session) -> NewUserResponse {
let has_referral = match &user_form.referral { let has_referral = match &user_form.referral {
@@ -251,9 +235,7 @@ impl Api {
} }
#[oai(path = "/num", method = "get", tag = "ApiTags::User")] #[oai(path = "/num", method = "get", tag = "ApiTags::User")]
//async fn get_num(&self, session: &Session) -> Json<u32> {
async fn get_num(&self, session: &Session) -> NumResponse { async fn get_num(&self, session: &Session) -> NumResponse {
//if session.get("user")
if let None = session.get::<String>("user") { if let None = session.get::<String>("user") {
return NumResponse::AuthError; return NumResponse::AuthError;
} }
@@ -271,6 +253,19 @@ impl Api {
} }
} }
#[derive(Serialize, Deserialize, Clone)]
struct ChatMsg {
username: String,
message: String,
timestamp: DateTime<Utc>,
}
struct Api {
db: Pool<Sqlite>,
channel: Sender<ChatMsg>,
signup_open: bool, //TODO Should be in the db so it can change without restart
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
if std::env::var_os("RUST_LOG").is_none() { if std::env::var_os("RUST_LOG").is_none() {
@@ -280,11 +275,13 @@ async fn main() -> anyhow::Result<()> {
let session = ServerSession::new(CookieConfig::default(), MemoryStorage::new()); let session = ServerSession::new(CookieConfig::default(), MemoryStorage::new());
//let dbpool = SqlitePool::connect("sqlite:chat.db").await?; let channel = tokio::sync::broadcast::channel::<ChatMsg>(128).0;
let dbpool = Pool::<Sqlite>::connect("sqlite:chat.db").await?; let dbpool = Pool::<Sqlite>::connect("sqlite:chat.db").await?;
let api = OpenApiService::new( let api = OpenApiService::new(
Api { Api {
db: dbpool, db: dbpool,
channel,
signup_open: false, signup_open: false,
}, },
"Chat", "Chat",
@@ -293,12 +290,6 @@ async fn main() -> anyhow::Result<()> {
let static_endpoint = StaticFilesEndpoint::new("./client/dist").index_file("index.html"); let static_endpoint = StaticFilesEndpoint::new("./client/dist").index_file("index.html");
let app = Route::new() let app = Route::new()
//.at("/api/", get(index))
//.at(
// "/api/ws/:name",
// get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)),
//)
//.data(dbpool)
.nest("/", static_endpoint) .nest("/", static_endpoint)
.nest("/api", api) .nest("/api", api)
.with(session); .with(session);