Compare commits
10 Commits
c66d68572d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aead788292 | |||
| e639604ef6 | |||
| dfa0a6a7e8 | |||
| 784dd60bef | |||
| 9244e94dd7 | |||
| 6d8ba5cc77 | |||
| 6e5915fbbd | |||
| 6d24f03648 | |||
| cd57573288 | |||
| 3d9c211106 |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
client/src/components/Header.vue
Normal file
66
client/src/components/Header.vue
Normal 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>
|
||||||
@@ -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 = "/"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 = "/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
28
src/main.rs
28
src/main.rs
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user