feat(web): implement initial logic to handle protected vs public routing
This commit is contained in:
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"dotnet.defaultSolution": "FiscalOS.slnx",
|
||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
|
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
|
||||||
|
|||||||
Generated
+10
@@ -8,6 +8,7 @@
|
|||||||
"name": "fiscalos-web",
|
"name": "fiscalos-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "beta",
|
"vue": "beta",
|
||||||
"vue-router": "^5.0.2"
|
"vue-router": "^5.0.2"
|
||||||
@@ -5145,6 +5146,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "beta",
|
"vue": "beta",
|
||||||
"vue-router": "^5.0.2"
|
"vue-router": "^5.0.2"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>FiscalOS</h1>
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,8 +1,49 @@
|
|||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [],
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/public',
|
||||||
|
redirect: '/public/login',
|
||||||
|
beforeEnter: () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
if (userStore.user) {
|
||||||
|
return { path: '/' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
component: () => import('../views/LoginView.vue')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
beforeEnter: () => {
|
||||||
|
// TODO: Handle expired token
|
||||||
|
// Try to refresh
|
||||||
|
// if refresh fails log out
|
||||||
|
// redirect to login page
|
||||||
|
// if refresh succeeds
|
||||||
|
// log user in and allow
|
||||||
|
// to pass
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
if (userStore.user === null) {
|
||||||
|
return { path: '/public/login' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
component: () => import('../views/HomeView.vue'),
|
||||||
|
}
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const USER_KEY = 'onxGraphAuth';
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
expiresAtInSeconds: number;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type JwtTokenPayload = {
|
||||||
|
sub: string;
|
||||||
|
exp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUserFromLocalStorage(): User | null {
|
||||||
|
const user = localStorage.getItem(USER_KEY);
|
||||||
|
return user === null ? null : JSON.parse(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserStore = ReturnType<typeof useUserStore>;
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('userStore', () => {
|
||||||
|
const user = ref<User | null>(getUserFromLocalStorage());
|
||||||
|
|
||||||
|
function logUserIn(jwtToken: string) {
|
||||||
|
const { sub, exp } = jwtDecode<JwtTokenPayload>(jwtToken);
|
||||||
|
const loggedInUser: User = {
|
||||||
|
id: sub,
|
||||||
|
expiresAtInSeconds: exp,
|
||||||
|
token: jwtToken,
|
||||||
|
};
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(loggedInUser));
|
||||||
|
user.value = loggedInUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logUserOut() {
|
||||||
|
localStorage.removeItem(USER_KEY);
|
||||||
|
user.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: user,
|
||||||
|
logUserIn,
|
||||||
|
logUserOut,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Home View</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const { logUserIn } = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
const data = Object.fromEntries(new FormData(e.currentTarget as HTMLFormElement));
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok === false) {
|
||||||
|
alert('Failed to login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
logUserIn(responseBody.accessToken)
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Login View</h1>
|
||||||
|
<form v-on:submit.prevent="handleSubmit">
|
||||||
|
<div>
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" name="username" id="username">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -7,6 +7,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
|||||||
import mkcert from 'vite-plugin-mkcert';
|
import mkcert from 'vite-plugin-mkcert';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
publicDir: 'static',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
|
|||||||
Reference in New Issue
Block a user