Compare commits
27 Commits
16dac8941b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aead788292 | |||
| e639604ef6 | |||
| dfa0a6a7e8 | |||
| 784dd60bef | |||
| 9244e94dd7 | |||
| 6d8ba5cc77 | |||
| 6e5915fbbd | |||
| 6d24f03648 | |||
| cd57573288 | |||
| 3d9c211106 | |||
| c66d68572d | |||
| 77ab2cb769 | |||
| eb9cbd9f66 | |||
| eba7e4b0b7 | |||
| 54efb85dc3 | |||
| 7a7a4b7fce | |||
| d27883bc97 | |||
| 748b7968cd | |||
| 62621e7fe6 | |||
| 753863585d | |||
| e37926be57 | |||
| 3d4d56f616 | |||
| ffa9d81cf8 | |||
| dba021fc30 | |||
| 6643d432e7 | |||
| 729056b752 | |||
| 1dd0b7caaa |
39
Cargo.lock
generated
39
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,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",
|
||||||
]
|
]
|
||||||
@@ -1023,6 +1029,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1271,8 +1287,10 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
"httpdate",
|
||||||
"hyper",
|
"hyper",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"multer",
|
"multer",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -1607,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",
|
||||||
@@ -2007,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",
|
||||||
@@ -2269,6 +2287,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@@ -4,16 +4,16 @@ version = "1.3.58"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
poem = { version = "1.3.58", features = ["websocket", "session", "anyhow"] }
|
poem = { version = "1.3.58", features = ["websocket", "session", "anyhow", "static-files"] }
|
||||||
tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros"] }
|
||||||
futures-util = "0.3.17"
|
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"] }
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
|
||||||
:root {
|
|
||||||
--vt-c-white: #ffffff;
|
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
|
||||||
--vt-c-black-soft: #222222;
|
|
||||||
--vt-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
|
||||||
--color-text: var(--vt-c-text-light-1);
|
|
||||||
|
|
||||||
--section-gap: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition:
|
|
||||||
color 0.5s,
|
|
||||||
background-color 0.5s;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,35 +0,0 @@
|
|||||||
@import './base.css';
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
.green {
|
|
||||||
text-decoration: none;
|
|
||||||
color: hsla(160, 100%, 37%, 1);
|
|
||||||
transition: 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
a:hover {
|
|
||||||
background-color: hsla(160, 100%, 37%, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
client/src/components/Chat.vue
Normal file
80
client/src/components/Chat.vue
Normal 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>
|
||||||
|
|
||||||
@@ -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'
|
||||||
}
|
}
|
||||||
@@ -26,13 +30,29 @@ async function click(ev) {
|
|||||||
|
|
||||||
count.value = await get_clicks()
|
count.value = await get_clicks()
|
||||||
}
|
}
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="form-signin w-100 m-auto">
|
<main class="form-signin w-100 m-auto">
|
||||||
<p class="mt-5 mb-3 text-body">You have accessed this content {{ count }} times this session.</p>
|
<p class="mt-5 mb-3 text-body">You have accessed this content {{ count }} times this session.</p>
|
||||||
<button type="submit" @click="click" class="btn btn-primary w-100 py-2">Refresh Content</button>
|
<button type="submit" @click="click" class="btn btn-primary w-100 py-2 m-3">Refresh Content</button>
|
||||||
|
<button type="submit" @click="signout" class="btn btn-primary w-100 py-2 m-3">Sign out</button>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
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'
|
||||||
@@ -16,35 +20,29 @@ 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 = ''
|
||||||
pass.value = ''
|
pass.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
let resp = await fetch("/api/user")
|
||||||
|
if (resp.ok) {
|
||||||
|
emit('update:modelValue', await resp.json())
|
||||||
|
window.location.hash = "/"
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="form-signin w-100 m-auto">
|
<main class="form-signin w-100 m-auto position-absolute top-50 start-50 translate-middle">
|
||||||
<form onsubmit="event.preventDefault();">
|
<form onsubmit="event.preventDefault();">
|
||||||
<!-- <img class="mb-4" src="../assets/brand/bootstrap-logo.svg" alt="" width="72" height="57"> -->
|
<IconLogo />
|
||||||
<svg class="bi me-2" width="72" height="57" role="img" viewBox="0 0 16 16">
|
|
||||||
<path d="M4.5 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z" />
|
|
||||||
<path
|
|
||||||
d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H8.5v3a1.5 1.5 0 0 1 1.5 1.5h5.5a.5.5 0 0 1 0 1H10A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5H.5a.5.5 0 0 1 0-1H6A1.5 1.5 0 0 1 7.5 10V7H2a2 2 0 0 1-2-2V4zm1 0v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1zm6 7.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5z" />
|
|
||||||
</svg>
|
|
||||||
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
|
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
@@ -61,17 +59,12 @@ async function login(ev) {
|
|||||||
|
|
||||||
<button type="submit" @click="login" class="btn btn-primary w-100 py-2">Sign in</button>
|
<button type="submit" @click="login" class="btn btn-primary w-100 py-2">Sign in</button>
|
||||||
|
|
||||||
<p class="mt-5 mb-3 text-body-secondary">© 2023</p>
|
<p class="mt-5 mb-3 text-body-secondary">© 2023 Lucas Schumacher</p>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.form-signin {
|
.form-signin {
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from '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('')
|
||||||
@@ -19,18 +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 ans = await resp.json()
|
let user = resp.json()
|
||||||
console.log(ans)
|
emit('update:modelValue', user)
|
||||||
|
err.value = ''
|
||||||
window.location.hash = "/"
|
window.location.hash = "/"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -40,14 +38,9 @@ async function signup(ev) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="form-signin w-100 m-auto my-auto">
|
<main class="form-signin w-100 m-auto my-auto position-absolute top-50 start-50 translate-middle">
|
||||||
<form onsubmit="event.preventDefault();">
|
<form onsubmit="event.preventDefault();">
|
||||||
<!-- <img class="mb-4" src="../assets/brand/bootstrap-logo.svg" alt="" width="72" height="57"> -->
|
<IconLogo />
|
||||||
<svg class="bi me-2" width="72" height="57" role="img" viewBox="0 0 16 16">
|
|
||||||
<path d="M4.5 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z" />
|
|
||||||
<path
|
|
||||||
d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H8.5v3a1.5 1.5 0 0 1 1.5 1.5h5.5a.5.5 0 0 1 0 1H10A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5H.5a.5.5 0 0 1 0-1H6A1.5 1.5 0 0 1 7.5 10V7H2a2 2 0 0 1-2-2V4zm1 0v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1zm6 7.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5z" />
|
|
||||||
</svg>
|
|
||||||
<h1 class="h3 mb-3 fw-normal">Sign up</h1>
|
<h1 class="h3 mb-3 fw-normal">Sign up</h1>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
@@ -58,30 +51,29 @@ async function signup(ev) {
|
|||||||
<input type="password" v-model="pass" class="form-control" id="floatingPassword" placeholder="Password">
|
<input type="password" v-model="pass" class="form-control" id="floatingPassword" placeholder="Password">
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
|
||||||
<input type="text" v-model="refe" class="form-control" id="floatingInput" placeholder="">
|
|
||||||
<label for="floatingInput">Referral Code</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating">
|
|
||||||
<p> {{ err }} </p>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
Already have an account?
|
<input type="text" v-model="refe" class="form-control" id="floatingCode" placeholder="">
|
||||||
<a href="#/login">Log in</a>
|
<label for="floatingCode">Referral Code</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="err != ''" class="form-floating alert alert-danger" role="alert">
|
||||||
|
<div class="me-3">{{ err }}</div>
|
||||||
|
<button @click="err = ''" type="button" class="btn-close me-2 position-absolute top-50 end-0 translate-middle-y"
|
||||||
|
aria-label="close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" @click="signup" class="btn btn-primary w-100 py-2">Sign up</button>
|
<button type="submit" @click="signup" class="btn btn-primary w-100 py-2">Sign up</button>
|
||||||
<p class="mt-5 mb-3 text-body-secondary">© 2023</p>
|
|
||||||
|
<div class="form-floating m-2">
|
||||||
|
Already have an account?
|
||||||
|
<a href="#/login">Log in</a>
|
||||||
|
</div>
|
||||||
|
<p class="mt-5 mb-3 text-body-secondary">© 2023 Lucas Schumacher</p>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.form-signin {
|
.form-signin {
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -91,15 +83,7 @@ body {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-signin input[type="email"] {
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="password"] {
|
.form-signin input[type="password"] {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
7
client/src/components/icons/IconLogo.vue
Normal file
7
client/src/components/icons/IconLogo.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg class="bi me-2" width="72" height="57" role="img" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M4.5 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z" />
|
||||||
|
<path
|
||||||
|
d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H8.5v3a1.5 1.5 0 0 1 1.5 1.5h5.5a.5.5 0 0 1 0 1H10A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5H.5a.5.5 0 0 1 0-1H6A1.5 1.5 0 0 1 7.5 10V7H2a2 2 0 0 1-2-2V4zm1 0v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1zm6 7.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -4,8 +4,6 @@ import './scss/styles.scss'
|
|||||||
// Import all of Bootstrap's JS
|
// Import all of Bootstrap's JS
|
||||||
import * as bootstrap from 'bootstrap'
|
import * as bootstrap from 'bootstrap'
|
||||||
|
|
||||||
import './assets/main.css'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
|||||||
220
src/main.rs
220
src/main.rs
@@ -1,106 +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::{
|
||||||
//get,
|
endpoint::StaticFilesEndpoint,
|
||||||
handler, //Result,
|
error::ResponseError,
|
||||||
|
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,
|
|
||||||
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 {
|
||||||
@@ -157,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
|
||||||
@@ -165,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 {
|
||||||
@@ -222,6 +208,12 @@ impl Api {
|
|||||||
UserResponse::AuthError
|
UserResponse::AuthError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[oai(path = "/auth", method = "delete", tag = "ApiTags::User")]
|
||||||
|
async fn deauth_user(&self, session: &Session) -> Result<()> {
|
||||||
|
session.purge();
|
||||||
|
Result::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[oai(path = "/auth", method = "post", tag = "ApiTags::User")]
|
#[oai(path = "/auth", method = "post", tag = "ApiTags::User")]
|
||||||
async fn auth_user(&self, user: Json<UserLogin>, session: &Session) -> UserResponse {
|
async fn auth_user(&self, user: Json<UserLogin>, session: &Session) -> UserResponse {
|
||||||
let password = user.password.as_str();
|
let password = user.password.as_str();
|
||||||
@@ -243,16 +235,15 @@ 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;
|
||||||
}
|
}
|
||||||
match session.get("num") {
|
match session.get::<u32>("num") {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
session.set("num", i + 1);
|
let new: u32 = i + 1;
|
||||||
NumResponse::Ok(Json(i))
|
session.set("num", new);
|
||||||
|
NumResponse::Ok(Json(new))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
session.set("num", 1_u32);
|
session.set("num", 1_u32);
|
||||||
@@ -262,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() {
|
||||||
@@ -271,24 +275,22 @@ 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",
|
||||||
"0.0",
|
"0.0",
|
||||||
);
|
);
|
||||||
|
let static_endpoint = StaticFilesEndpoint::new("./client/dist").index_file("index.html");
|
||||||
|
|
||||||
let app = Route::new()
|
let app = Route::new()
|
||||||
//.at("/api/", get(index))
|
.nest("/", static_endpoint)
|
||||||
//.at(
|
|
||||||
// "/api/ws/:name",
|
|
||||||
// get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)),
|
|
||||||
//)
|
|
||||||
//.data(dbpool)
|
|
||||||
.nest("/api", api)
|
.nest("/api", api)
|
||||||
.with(session);
|
.with(session);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user