feat(web): implement initial logic to handle protected vs public routing

This commit is contained in:
Stevan Freeborn
2026-02-21 07:32:28 -06:00
parent df859b91b0
commit dee331fb0d
10 changed files with 161 additions and 2 deletions
+1
View File
@@ -1,4 +1,5 @@
{
"dotnet.defaultSolution": "FiscalOS.slnx",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
+10
View File
@@ -8,6 +8,7 @@
"name": "fiscalos-web",
"version": "0.0.0",
"dependencies": {
"jwt-decode": "^4.0.0",
"pinia": "^3.0.4",
"vue": "beta",
"vue-router": "^5.0.2"
@@ -5145,6 +5146,15 @@
"dev": true,
"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": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+1
View File
@@ -17,6 +17,7 @@
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"jwt-decode": "^4.0.0",
"pinia": "^3.0.4",
"vue": "beta",
"vue-router": "^5.0.2"
+3 -1
View File
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
<template>
<h1>FiscalOS</h1>
<RouterView />
</template>
<style scoped></style>
+42 -1
View File
@@ -1,8 +1,49 @@
import { useUserStore } from "@/stores/userStore";
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
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;
+49
View File
@@ -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,
};
});
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<h1>Home View</h1>
</template>
<style scoped></style>
+47
View File
@@ -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

+1
View File
@@ -7,6 +7,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
import mkcert from 'vite-plugin-mkcert';
export default defineConfig({
publicDir: 'static',
plugins: [
vue(),
vueJsx(),