Add homepage for logged in users, logout button, retrieving user info from backend

This commit is contained in:
ReiMerc 2023-12-11 23:03:03 +01:00
parent 7c3bd135d1
commit 839cbe5463
11 changed files with 257 additions and 23 deletions

View File

@ -18,6 +18,10 @@ public class UserController : ControllerBase
return BadRequest("Username and password required"); return BadRequest("Username and password required");
} }
if (ApplicationState.DbContext!.Users.FirstOrDefault(user => user.Username == input["username"]!.ToString()) != null) {
return BadRequest("User already exists");
}
// Hash password // Hash password
var passwordHasher = new PasswordHasher<object>(); var passwordHasher = new PasswordHasher<object>();
string hashedPassword = passwordHasher.HashPassword(null, input["password"]!.ToString()); string hashedPassword = passwordHasher.HashPassword(null, input["password"]!.ToString());
@ -81,4 +85,49 @@ public class UserController : ControllerBase
return Ok(); return Ok();
} }
[HttpPost("Logout")]
public IActionResult LogOut()
{
// Validate
if (Request.Cookies["session"] == null) {
return BadRequest("You are not logged in");
}
// Get user
var user = ApplicationState.DbContext!.Users.FirstOrDefault(user => user.SessionToken == Request.Cookies["session"]!.ToString());
if (user == null) {
return BadRequest("Invalid session token");
}
// Log out
user.SessionToken = null;
ApplicationState.DbContext!.SaveChanges();
Response.Cookies.Delete("session");
return Ok();
}
[HttpGet("UserInfo")]
public IActionResult UserInfo()
{
// Validate
if (Request.Cookies["session"] == null) {
return BadRequest("You are not logged in");
}
// Get user
var user = ApplicationState.DbContext!.Users.FirstOrDefault(user => user.SessionToken == Request.Cookies["session"]!.ToString());
if (user == null) {
return BadRequest("Invalid session token");
}
var data = new {
username = user.Username,
touchCode = user.TouchCode,
};
return new JsonResult(data);
}
} }

View File

@ -8,8 +8,10 @@
"name": "slik-dispenser", "name": "slik-dispenser",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@vueuse/core": "^10.7.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.5" "vue-router": "^4.2.5",
"vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
@ -384,6 +386,11 @@
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz",
@ -504,6 +511,89 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.9.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.9.tgz",
"integrity": "sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==" "integrity": "sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA=="
}, },
"node_modules/@vueuse/core": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.0.tgz",
"integrity": "sha512-4EUDESCHtwu44ZWK3Gc/hZUVhVo/ysvdtwocB5vcauSV4B7NiGY5972WnsojB3vRNdxvAt7kzJWE2h9h7C9d5w==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.7.0",
"@vueuse/shared": "10.7.0",
"vue-demi": ">=0.14.6"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.0.tgz",
"integrity": "sha512-GlaH7tKP2iBCZ3bHNZ6b0cl9g0CJK8lttkBNUX156gWvNYhTKEtbweWLm9rxCPIiwzYcr/5xML6T8ZUEt+DkvA==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.0.tgz",
"integrity": "sha512-kc00uV6CiaTdc3i1CDC4a3lBxzaBE9AgYNtFN87B5OOscqeWElj/uza8qVDmk7/U8JbqoONLbtqiLJ5LGRuqlw==",
"dependencies": {
"vue-demi": ">=0.14.6"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@ -737,6 +827,17 @@
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.0" "vue": "^3.2.0"
} }
},
"node_modules/vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.11"
},
"peerDependencies": {
"vue": "^3.2.0"
}
} }
} }
} }

View File

@ -8,8 +8,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^10.7.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.5" "vue-router": "^4.2.5",
"vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",

View File

@ -1,23 +1,49 @@
<script setup> <script setup>
import { RouterLink, RouterView } from 'vue-router'; import { RouterLink, RouterView, useRouter } from "vue-router";
import { request } from "./assets/helpers.js";
import { ref, watch, computed } from "vue";
import { useEventBus } from "@vueuse/core";
import { useStore } from "vuex";
function dispense() { const router = useRouter();
const xhr = new XMLHttpRequest(); const bus = useEventBus("login");
xhr.open("POST", import.meta.env.VITE_API_URL + "/dispense"); const userStore = useStore("userStore")
xhr.send();
let loggedIn = ref(!!document.cookie.match(/session=/));
if (loggedIn.value) getUserInfo();
bus.on(() => {
loggedIn.value = true
getUserInfo();
});
async function getUserInfo() {
userStore.dispatch("updateUserInfo");
}
async function logout() {
await request("POST", "/logout");
loggedIn.value = false;
router.push("/");
} }
</script> </script>
<template> <template>
<header> <header>
<nav> <nav>
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink> <RouterLink to="/about">About</RouterLink>
<template v-if="!loggedIn">
<RouterLink to="/login">Login</RouterLink> <RouterLink to="/login">Login</RouterLink>
<RouterLink to="/register">Register</RouterLink> <RouterLink to="/register">Register</RouterLink>
</nav> </template>
<template v-else>
<span style="color: #BDBDBD;">{{ userStore.state.userInfo?.username ?? "Loading.." }}</span>
<a href="#" @click.prevent="logout">Log out</a>
</template>
</nav>
<RouterView /> <RouterView />
</header> </header>

View File

@ -1,17 +1,26 @@
// Send request to backend API
function request(method, path, data = null) { function request(method, path, data = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.onload = () => { xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) if (xhr.status >= 200 && xhr.status < 300)
resolve(xhr.responseText ? JSON.parse(xhr.responseText) : null); resolve(xhr.responseText ? JSON.parse(xhr.responseText) : null);
else else
reject(xhr.responseText || "HTTP " + xhr.status); reject(xhr.responseText || "HTTP " + xhr.status);
} }
xhr.onerror = () => reject("Something went wrong"); xhr.onerror = () => reject("Something went wrong");
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.open(method, import.meta.env.VITE_API_URL + path); xhr.open(method, import.meta.env.VITE_API_URL + path);
if (data) xhr.setRequestHeader("Content-Type", "application/json"),
if (data) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(data)); xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
}); });
} }

View File

@ -1,4 +1,4 @@
body, h1{ body, h1, h2, h3, h4, h5, h6 {
margin: 0; margin: 0;
} }
@ -30,12 +30,13 @@ input {
nav { nav {
padding: 5px; padding: 5px;
} }
nav a { nav > * {
padding: 5px 10px; padding: 5px 10px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
color: white; color: white;
text-decoration: none; text-decoration: none;
transition-duration: 200ms; transition-duration: 200ms;
border: none;
} }
nav a:not(.router-link-active):hover { nav a:not(.router-link-active):hover {
background-color: rgba(0, 0, 0, 0.61); background-color: rgba(0, 0, 0, 0.61);

View File

@ -1,4 +1,5 @@
import './assets/main.css'; import './assets/main.css';
import userStore from "./stores/userStore";
import { createApp } from 'vue'; import { createApp } from 'vue';
import App from './App.vue'; import App from './App.vue';
@ -7,6 +8,7 @@ import router from './router';
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
app.use(userStore, "userStore");
app.mount('#app'); app.mount('#app');

View File

@ -0,0 +1,19 @@
import { createStore } from "vuex";
import { request } from "../assets/helpers.js";
export default createStore({
state: {
userInfo: null,
},
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo;
}
},
actions: {
async updateUserInfo({ commit }) {
const userInfo = await request("GET", "/userinfo");
commit("setUserInfo", userInfo);
},
}
});

View File

@ -1,11 +1,27 @@
<script setup> <script setup>
import { request } from "../assets/helpers.js";
import { useStore } from "vuex";
const userStore = useStore("userStore");
async function dispense() {
await request("POST", "/dispense");
}
</script> </script>
<template> <template>
<main> <main>
<template v-if="userStore.state.userInfo">
<h2>Welcome back, {{ userStore.state.userInfo.username }}</h2>
<br>
<button @click="dispense">Dispense the m&m</button>
</template>
<template v-else>
<h1>M&M Dispenser</h1> <h1>M&M Dispenser</h1>
<button @click="dispense">Dispense the m&m</button> <p>Please login to continue</p>
</template>
</main> </main>
</template> </template>

View File

@ -1,6 +1,11 @@
<script setup> <script setup>
import { request } from "../assets/helpers.js"; import { request } from "../assets/helpers.js";
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router";
import { useEventBus } from "@vueuse/core";
const router = useRouter();
const bus = useEventBus("login");
const username = ref(null); const username = ref(null);
const password = ref(null); const password = ref(null);
@ -9,12 +14,13 @@ const error = ref("");
function login() { function login() {
request("POST", "/login", { username: username.value.value, password: password.value.value }) request("POST", "/login", { username: username.value.value, password: password.value.value })
.then(success) .catch(err => error.value = err)
.catch(err => error.value = err); .then(success);
} }
function success() { function success() {
alert("Successfully logged in"); bus.emit();
router.push("/");
} }
</script> </script>

View File

@ -1,6 +1,9 @@
<script setup> <script setup>
import { request } from "../assets/helpers.js"; import { request } from "../assets/helpers.js";
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const username = ref(null); const username = ref(null);
const password = ref(null); const password = ref(null);
@ -14,7 +17,7 @@ function register() {
} }
function success() { function success() {
alert("Successfully registered an account"); router.push("/login");
} }
</script> </script>