Compare commits

...

10 Commits

10 changed files with 159 additions and 22 deletions

9
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,10 +507,12 @@ 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", "thiserror",
"tokio", "tokio",
@@ -1620,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",

View File

@@ -14,4 +14,6 @@ poem-openapi = { version = "3.0.5", features = ["websocket"] }
serde = "1.0.190" serde = "1.0.190"
thiserror = "1.0.50" thiserror = "1.0.50"
anyhow = "1.0.75" anyhow = "1.0.75"
serde_json = "1.0.108"
chrono = { version = "0.4.31", features = ["serde"] }

View File

@@ -1,11 +1,11 @@
<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 Chat from './components/Chat.vue'
import Header from './components/Header.vue'
const routes = { const routes = {
'/': Chat, '/': Chat,
@@ -15,6 +15,8 @@ const routes = {
'/chat': Chat, '/chat': Chat,
} }
let user = ref('')
const currentPath = ref(window.location.hash) const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
@@ -25,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

@@ -1,8 +1,11 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const input = ref('') const input = ref('')
const msgs = ref(['Server: Welcome to chat app', 'Server: This is an example message']) const msgs = ref([{ username: 'Server', message: 'Welcome to Chat App' }, { username: 'Server', message: 'This is an example message' }])
let ws; let ws;
async function sendMsg(ev) { async function sendMsg(ev) {
@@ -21,13 +24,15 @@ onMounted(async () => {
ws.onerror = function (event) { ws.onerror = function (event) {
console.log('WebSocket connection failed:', event); console.log('WebSocket connection failed:', event);
emit('update:modelValue', '')
//alert("Not logged in!") //alert("Not logged in!")
window.location.hash = "/login" window.location.hash = "/login"
}; };
ws.onmessage = function (event) { ws.onmessage = async function (event) {
console.log(event) console.log("event data: " + event.data)
msgs.value.push(event.data) msgs.value.push(JSON.parse(event.data))
console.log("msgs:" + msgs.value)
}; };
console.log(ws) console.log(ws)
@@ -37,15 +42,24 @@ onMounted(async () => {
<template> <template>
<main> <main>
<div class="overflow-auto position-relative pb-5"> <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 }}</div>
</div>
<!-- <!--
<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 v-for="msg in msgs" class="input-group">
<div class="alert alert-primary">User</div> <div class="alert alert-primary">{{ msg.username }}</div>
<div class="alert alert-secondary">{{ msg }}</div> <div class="alert alert-secondary">{{ msg.message }}</div>
</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"> <form onsubmit="event.preventDefault();" class="container-fluid fixed-bottom mb-3">
<div class="input-group"> <div class="input-group">
<input v-model="input" type="text" class="form-control text-bg-secondary" placeholder="Type message here" <input v-model="input" type="text" class="form-control text-bg-secondary" placeholder="Type message here"
@@ -57,3 +71,10 @@ onMounted(async () => {
</main> </main>
</template> </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'
@@ -19,6 +22,7 @@ async function login(ev) {
let resp = await fetch(api_url, fetch_options) let resp = await fetch(api_url, fetch_options)
if (resp.ok) { if (resp.ok) {
emit('update:modelValue', await resp.json())
window.location.hash = "/" window.location.hash = "/"
} else { } else {
user.value = '' user.value = ''
@@ -29,6 +33,7 @@ async function login(ev) {
onMounted(async () => { onMounted(async () => {
let resp = await fetch("/api/user") let resp = await fetch("/api/user")
if (resp.ok) { if (resp.ok) {
emit('update:modelValue', await resp.json())
window.location.hash = "/" window.location.hash = "/"
} }
}) })

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('')
@@ -23,6 +26,8 @@ async function signup(ev) {
let resp = await fetch(api_url, fetch_options) let resp = await fetch(api_url, fetch_options)
if (resp.ok) { if (resp.ok) {
let user = resp.json()
emit('update:modelValue', user)
err.value = '' err.value = ''
window.location.hash = "/" window.location.hash = "/"
} }

View File

@@ -35,7 +35,7 @@ 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,3 +1,4 @@
use chrono::{offset::Utc, DateTime};
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use poem::{ use poem::{
endpoint::StaticFilesEndpoint, endpoint::StaticFilesEndpoint,
@@ -15,6 +16,7 @@ use poem_openapi::{
ApiResponse, Object, OpenApi, OpenApiService, Tags, 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; use tokio::sync::broadcast::Sender;
@@ -124,7 +126,14 @@ impl Api {
tokio::spawn(async move { tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await { while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(text) = msg { if let Message::Text(text) = msg {
if sender.send(format!("{name}: {text}")).is_err() { if sender
.send(ChatMsg {
username: name.clone(),
message: text,
timestamp: Utc::now(),
})
.is_err()
{
break; break;
} }
} }
@@ -133,7 +142,11 @@ impl Api {
tokio::spawn(async move { tokio::spawn(async move {
while let Ok(msg) = receiver.recv().await { while let Ok(msg) = receiver.recv().await {
if sink.send(Message::Text(msg)).await.is_err() { if sink
.send(Message::Text(serde_json::to_string(&msg).unwrap()))
.await
.is_err()
{
break; break;
} }
} }
@@ -240,9 +253,16 @@ impl Api {
} }
} }
#[derive(Serialize, Deserialize, Clone)]
struct ChatMsg {
username: String,
message: String,
timestamp: DateTime<Utc>,
}
struct Api { struct Api {
db: Pool<Sqlite>, db: Pool<Sqlite>,
channel: Sender<String>, channel: Sender<ChatMsg>,
signup_open: bool, //TODO Should be in the db so it can change without restart signup_open: bool, //TODO Should be in the db so it can change without restart
} }
@@ -255,7 +275,7 @@ async fn main() -> anyhow::Result<()> {
let session = ServerSession::new(CookieConfig::default(), MemoryStorage::new()); let session = ServerSession::new(CookieConfig::default(), MemoryStorage::new());
let channel = tokio::sync::broadcast::channel::<String>(128).0; 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(