adding authentication and state management

This commit is contained in:
José Conde 2023-01-22 00:00:26 +01:00
parent b1207d0b6f
commit 3191fe5c04
14 changed files with 241 additions and 105 deletions

3
.env
View File

@ -3,4 +3,5 @@ VITE_API_BASE=http://localhost:3000/api/v1
VITE_API_PATH_LIST=/list/today VITE_API_PATH_LIST=/list/today
VITE_API_PATH_WHITELIST=/whitelist VITE_API_PATH_WHITELIST=/whitelist
VITE_API_PATH_NOW_SESSIONS=/ivao/sessions/now VITE_API_PATH_NOW_SESSIONS=/ivao/sessions/now
VITE_API_PATH_NOW_FLIGHTPLANS=/ivao/flightplans/latest VITE_API_PATH_NOW_FLIGHTPLANS=/ivao/flightplans/latest
VITE_API_PATH_AUTHENTICATION=/admin/user/authenticate

68
package-lock.json generated
View File

@ -15,6 +15,7 @@
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"pinia": "^2.0.29",
"redis": "^4.5.1", "redis": "^4.5.1",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-loading-overlay": "^6.0.2", "vue-loading-overlay": "^6.0.2",
@ -2269,6 +2270,56 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "2.0.29",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.29.tgz",
"integrity": "sha512-5z/KpFecq/cIgfeTnulJXldiLcTITRkTe3N58RKYSj0Pc1EdR6oyCdnf5A9jLoVwBqX5LtHhd0kGlpzWvk9oiQ==",
"dependencies": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"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/portfinder": { "node_modules/portfinder": {
"version": "1.0.32", "version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
@ -4559,6 +4610,23 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true "dev": true
}, },
"pinia": {
"version": "2.0.29",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.29.tgz",
"integrity": "sha512-5z/KpFecq/cIgfeTnulJXldiLcTITRkTe3N58RKYSj0Pc1EdR6oyCdnf5A9jLoVwBqX5LtHhd0kGlpzWvk9oiQ==",
"requires": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"portfinder": { "portfinder": {
"version": "1.0.32", "version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",

View File

@ -18,6 +18,7 @@
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"pinia": "^2.0.29",
"redis": "^4.5.1", "redis": "^4.5.1",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-loading-overlay": "^6.0.2", "vue-loading-overlay": "^6.0.2",

View File

@ -1,18 +1,62 @@
<script> <script>
// import { RouterLink, RouterView } from "vue-router"; // import { RouterLink, RouterView } from "vue-router";
// import { useAuth0 } from '@auth0/auth0-vue';
import { useSessionStore } from './stores/session-store';
import { authenticate, isAlive, logout } from './data/http/auth';
import { mapState } from 'pinia';
export default { export default {
setup() {
const store = useSessionStore();
const { setUser, logoutUser, hasRoles } = store;
return {
logoutUser,
setUser,
hasRoles
}
},
async mounted() {
const user = await isAlive();
if (user) {
this.setUser(user);
}
},
data() { data() {
return { return {
username: '',
password: '',
loading: true, loading: true,
list: [], list: [],
whitelist: [], whitelist: [],
isNavMobileOpen: false, isNavMobileOpen: false,
} }
}, },
computed:{
...mapState(useSessionStore, ['user', 'isAuthenticated']),
},
methods: { methods: {
toggleNavMobile() { toggleNavMobile() {
this.isNavMobileOpen = !this.isNavMobileOpen; this.isNavMobileOpen = !this.isNavMobileOpen;
},
async login() {
if (!this.username.trim() || !this.password.trim()) {
return;
}
const user = await authenticate(this.username, this.password);
this.setUser(user);
console.log('user :>> ', user);
this.username = '';
this.password = '';
},
async logout() {
await logout();
this.logoutUser();
this.$router.push('/');
},
async check() {
await isAlive();
} }
} }
} }
@ -37,41 +81,46 @@ export default {
<div class="navbar-start"> <div class="navbar-start">
<router-link class="navbar-item" to="/">ICAO</router-link> <router-link class="navbar-item" to="/">ICAO</router-link>
<router-link class="navbar-item" to="/acars">Acars</router-link> <router-link class="navbar-item" to="/acars">Acars</router-link>
<router-link v-if="hasRoles(['cabal'])" class="navbar-item" to="/cabal">Capt Cabal</router-link>
<router-link v-if="hasRoles(['admin'])" class="navbar-item" to="/admin">Admin</router-link>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="field is-horizontal">
<div v-if="!isAuthenticated" class="field-body">
<div class="field">
<p class="control is-expanded">
<input class="input is-small input-login" @keyup.enter="login" v-model="username" type="text" placeholder="Username">
</p>
</div>
<div class="field is-grouped">
<p class="control is-expanded">
<input class="input is-small input-login" @keyup.enter="login" v-model="password" type="password" placeholder="Password">
</p>
<p class="control">
<a class="button is-small is-light" @click="login">
Login
</a>
</p>
</div>
</div>
<div v-if="isAuthenticated" class="field-body">
<div class="is-6 mr-2 mt-1">{{ user.firstname }} {{ user.lastname }}</div>
<p class="control">
<a class="button is-small is-light" @click="logout">
Logout
</a>
</p>
</div>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container"> <div class="container">
<div> <RouterView /> </div> <div> <RouterView /> </div>
<!-- <section class="section"> </div>
<h1 class="title">Pilotos activos con mas de 200 horas en ACARS</h1>
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th class="has-text-centered">VID</th>
<th class="">Nombre</th>
<th class="has-text-centered">Total Vuelos</th>
<th class="has-text-centered">Horas totales</th>
</tr>
</thead>
<tbody>
<tr v-for="item in whitelist" :key="item.vid">
<td class="has-text-centered">
{{ item.vid }}
</td>
<td>
{{ item.name }}
</td>
<td class="has-text-centered">
{{ item.flights }}
</td>
<td class="has-text-centered">
<FormatTime :value="item.flightTime" />
</td>
</tr>
</tbody>
</table>
</section> -->
</div>
</template> </template>
@ -83,60 +132,7 @@ export default {
.tag { .tag {
font-size: 60% !important; font-size: 60% !important;
} }
/* header { .input-login {
line-height: 1.5; width: 100px;
max-height: 100vh;
} }
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
} */
</style> </style>

View File

@ -22,6 +22,7 @@
export default { export default {
data() { data() {
return { return {
interval: undefined,
isLoading: true, isLoading: true,
list: [], list: [],
board: undefined, board: undefined,
@ -47,9 +48,12 @@
await this.loadData(); await this.loadData();
this.startLoop(); this.startLoop();
}, },
unmounted() {
clearInterval(this.interval);
},
methods: { methods: {
startLoop() { startLoop() {
setInterval(this.loadData, 10000) this.interval = setInterval(this.loadData, 1000 * 30)
}, },
async loadData() { async loadData() {
this.isLoading = true; this.isLoading = true;

22
src/data/http/auth.js Normal file
View File

@ -0,0 +1,22 @@
import {get, post } from './requests.js';
export async function isAlive() {
const { VITE_API_BASE } =
import.meta.env;
const response = await get(`${VITE_API_BASE}/admin/user/alive`);
return response;
}
export async function authenticate(username, password) {
const { VITE_API_BASE, VITE_API_PATH_AUTHENTICATION } =
import.meta.env;
const response = await post(`${VITE_API_BASE}${VITE_API_PATH_AUTHENTICATION}`, { username, password });
return response;
}
export async function logout() {
const { VITE_API_BASE } =
import.meta.env;
const response = await get(`${VITE_API_BASE}/admin/user/logout`);
return response;
}

View File

@ -1,26 +1,26 @@
import { request } from './requests.js'; import {get } from './requests.js';
export async function getMonthlyList() { export async function getMonthlyList() {
const { VITE_API_BASE, VITE_API_PATH_LIST } = const { VITE_API_BASE, VITE_API_PATH_LIST } =
import.meta.env; import.meta.env;
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_LIST}`); const response = await get(`${VITE_API_BASE}${VITE_API_PATH_LIST}`);
return response; return response;
} }
export async function getWhitelist() { export async function getWhitelist() {
const { VITE_API_BASE, VITE_API_PATH_WHITELIST } = const { VITE_API_BASE, VITE_API_PATH_WHITELIST } =
import.meta.env; import.meta.env;
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`); const response = await get(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`);
return response; return response;
} }
export async function getInSessionNow() { export async function getInSessionNow() {
const { VITE_API_BASE, VITE_API_PATH_NOW_SESSIONS } = const { VITE_API_BASE, VITE_API_PATH_NOW_SESSIONS } =
import.meta.env; import.meta.env;
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_NOW_SESSIONS}`); const response = await get(`${VITE_API_BASE}${VITE_API_PATH_NOW_SESSIONS}`);
return response; return response;
} }
export async function getFlightplansNow() { export async function getFlightplansNow() {
const { VITE_API_BASE, VITE_API_PATH_NOW_FLIGHTPLANS } = const { VITE_API_BASE, VITE_API_PATH_NOW_FLIGHTPLANS } =
import.meta.env; import.meta.env;
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_NOW_FLIGHTPLANS}`); const response = await get(`${VITE_API_BASE}${VITE_API_PATH_NOW_FLIGHTPLANS}`);
return response; return response;
} }

View File

@ -1,6 +1,15 @@
import axios from 'axios'; import axios from 'axios';
export const request = async(url, options) => { export const get = async(url) => {
const response = await axios.get(url, options); const response = await axios.get(url, { withCredentials: true });
return response.data; return response.data;
}; };
export const post = async(url, data) => {
try {
const response = await axios.post(url, data, { withCredentials: true });
return response.data;
} catch (err) {
console.log('err :>> ', err.response);
}
}

View File

@ -1,13 +1,17 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import VueMobileDetection from 'vue-mobile-detection'; import VueMobileDetection from 'vue-mobile-detection';
import './assets/css/main.css'; import './assets/css/main.css';
const pinia = createPinia();
const app = createApp(App); const app = createApp(App);
app.use(pinia);
app.use(router); app.use(router);
app.use(VueMobileDetection); app.use(VueMobileDetection);
app.mount('#app'); app.mount('#app');

View File

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import TableAcars from '../components/TableAcars.vue'; import TableAcars from '../components/TableAcars.vue';
import IvaoView from '../views/IvaoView.vue'; import IvaoView from '../views/IvaoView.vue';
import CabalView from '../views/CabalView.vue';
const router = createRouter({ const router = createRouter({
history: createWebHistory( history: createWebHistory(
@ -15,6 +16,11 @@ const router = createRouter({
name: 'acars', name: 'acars',
component: TableAcars, component: TableAcars,
}, },
{
path: '/cabal',
name: 'cabal',
component: CabalView,
},
// { // {
// path: '/about', // path: '/about',
// name: 'about', // name: 'about',

View File

@ -0,0 +1,26 @@
import { defineStore } from 'pinia';
export const useSessionStore = defineStore('user', {
state: () => ({ _user: {}, _isAuthenticated: false }),
getters: {
user: (state) => state._user,
isAuthenticated: (state) => state._isAuthenticated,
},
actions: {
setUser(user) {
this._isAuthenticated = !!user;
this._user = user;
},
logoutUser() {
this._isAuthenticated = false;
this._user = '';
},
hasRoles(roles = []) {
let hasRole = false;
roles.forEach(role => {
hasRole = hasRole || (this._user.roles || []).indexOf(role) !== -1;
})
return hasRole;
}
}
});

8
src/views/CabalView.vue Normal file
View File

@ -0,0 +1,8 @@
<script setup>
</script>
<template>
<main class="section">
<h1 class="title is-2"> Capitán Cabal Hub</h1>
</main>
</template>

View File

@ -1,9 +0,0 @@
<script setup>
import TheWelcome from "../components/TheWelcome.vue";
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

View File

@ -4,7 +4,7 @@
</script> </script>
<template> <template>
<main> <main class="section">
<InSessionNow v-if="!$isMobile()" /> <InSessionNow v-if="!$isMobile()" />
<TableIvao /> <TableIvao />
</main> </main>