login, logout, store, ...

This commit is contained in:
Markus Thielen 2023-03-09 11:19:53 +01:00
parent 896ca59ace
commit b5a169282b
7 changed files with 70 additions and 115 deletions

View File

@ -2,12 +2,13 @@
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/Welcome.vue' import HelloWorld from './components/Welcome.vue'
import { store } from './store.js' import { store } from './store.js'
import {clearSession} from "./store.js";
/** /**
* Logout the user. * Logout the user.
*/ */
function logOut() { function logOut() {
store.loggedIn = false; clearSession();
location.href = "/";
} }
</script> </script>
@ -22,7 +23,7 @@ function logOut() {
<nav> <nav>
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink> <RouterLink to="/about">About</RouterLink>
<a v-if="store.session" href="" @click="logOut">Log Out</a> <a v-if="store.session" href="" @click="logOut()">Log Out</a>
</nav> </nav>
</div> </div>
</header> </header>

View File

@ -1,27 +0,0 @@
<!--
Dummy component that handles OAuth login callback requests.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<script setup>
import { onMounted } from 'vue'
/**
* onMounted lifecycle hook
*/
onMounted(() => {
});
export default {
name: "OAuthCallback"
mounted: {
},
}
</script>

View File

@ -10,6 +10,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import OAUthError from "../components/OAUthError.vue"; import OAUthError from "../components/OAUthError.vue";
import { objectToQueryString } from "../util/net"; import { objectToQueryString } from "../util/net";
import {createUser, oauthCallbackHandler} from "../util/oauth";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -44,48 +45,23 @@ router.beforeEach(async (to, from) => {
if (to.path === "/oauth-callback") { if (to.path === "/oauth-callback") {
let queryString = objectToQueryString(to.query); let queryString = objectToQueryString(to.query);
console.info(`Got /oauth-callback with query string '${queryString}`); console.info(`Got /oauth-callback with query string '${queryString}`);
/* Pass the query parameters to the broker to complete login. We will get
* the session token and user data in return. /* Pass the query string to the handler function */
* try {
* The URL we post to is broker's base URL and the `openid_connect_path` await oauthCallbackHandler(queryString);
* setting from broker's config. /* Success; redirect to home */
*/ } catch (err) {
const response = await fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, { console.error("Failed to finish login: " + err);
method: "POST",
});
if (!response.ok) {
/* redirect to error component */
console.error("Failed to complete login/get session data: " + response.statusText);
return {
name: 'oauth-error',
query: {
code: response.status,
addlInfo: await response.text()
}
}
} }
/* store session data; this is a JSON object of the following form: /* We have to create the logged-in user explicitly; see comment for `createUser()` */
{ try {
"token":"47caa9df-ac89-45a8-8b22-9c0925d6aed0", await createUser();
"username":"tester", } catch (err) {
"first_name":"Fred", console.error("Failed to create user: " + err);
"last_name":"Feuerstein", }
"roles":[
"/user"
],
"claims":{
"groups":[
"/user"
]
}
}
*/
store.session = await response.json();
store.userName = store.session.first_name ? store.session.first_name : store.session.username;
/* redirect to home */ return {name: 'home'};
return { name: 'home' };
} }
}); });

View File

@ -11,11 +11,27 @@ export const store = reactive({
/** Username of the currently logged-in user */ /** Username of the currently logged-in user */
userName: "stranger", userName: "stranger",
/** The host that runs basebox and waits for GraphQL requests */ /** base URL of basebox broker host */
baseboxHost: "http://127.0.0.1:8080", baseboxHost: "http://127.0.0.1:8080",
/** basebox session data */ /** basebox session data */
session: null, session: {},
});
/**
* Return true if the user is logged in.
*/
export function loggedIn() {
return store.session && store.session.token;
}
/**
* Clear session data.
*/
export function clearSession() {
store.session = {};
}
})

View File

@ -5,8 +5,7 @@
* https://basebox.tech * https://basebox.tech
*/ */
import {store} from "../store"; import {clearSession, store} from "../store";
import router from "../router";
/** /**
* GqlError - custom error thrown by the gqlQuery function. * GqlError - custom error thrown by the gqlQuery function.
@ -114,7 +113,7 @@ export function gqlQuery(query)
console.info(fetchOpt); console.info(fetchOpt);
return fetch(store.getters.gqlUrl, fetchOpt).then( return fetch(`${store.baseboxHost}/graphql`, fetchOpt).then(
/* fetch success */ /* fetch success */
async response => { async response => {
if (response.ok) { if (response.ok) {
@ -128,27 +127,25 @@ export function gqlQuery(query)
} else { } else {
/* some errors... */ /* some errors... */
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const error = new GqlError(rspJson);
/* if this is a 401, we redirect to the login page */
if (error.is401()) {
router.push({
name: "Login",
query: {
next: router.currentRoute.fullPath,
}
});
}
reject(new GqlError(rspJson)); reject(new GqlError(rspJson));
}); });
} }
} else { } else {
/* low level/network error */ /* low level/network error */
/* if this is a 401, we clear the session data */
if (response.status === 401) {
console.error("Last request failed: unauthorized. Clearing session.");
clearSession();
return new Promise((resolve, reject) => reject("Unauthorized"));
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
reject(new GqlError(response.statusText)); reject(new GqlError(response.statusText));
}); });
} }
}, },
/* fetch failure */ /* fetch failure */
reason => { reason => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -44,23 +44,18 @@ import {gqlQuery} from "./net";
*/ */
export async function oauthCallbackHandler(queryString) { export async function oauthCallbackHandler(queryString) {
fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, { /* Pass query string to the broker to complete the login process. */
const response = await fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, {
method: "POST", method: "POST",
}).then(response => {
if (!response.ok) {
throw new Error("Failed to get session data: " + response.statusText);
}
/* Store session data */
response.json(
).then(rspJson => {
store.session = rspJson;
store.userName = store.session.first_name ? store.session.first_name : store.session.username;
});
}).catch(e => {
throw new Error("Failed to get session data: " + e.toString());
}); });
if (!response.ok) {
throw new Error("Failed to get session data: " + response.statusText);
}
/* Store session data */
const rspJson = await response.json();
store.session = { ...rspJson };
store.userName = store.session.first_name ? store.session.first_name : store.session.username;
} }
/** /**
@ -70,21 +65,17 @@ export async function oauthCallbackHandler(queryString) {
* make sure that out user is known in the app database (basebox broker). After successful * make sure that out user is known in the app database (basebox broker). After successful
* login, this function must be called to do that. * login, this function must be called to do that.
* *
* @param username unique username of new user * @throws Error if the user creation failed.
* @param firstName first name of new user
* @param lastName last name of new user
* @returns {Promise<void>}
*/ */
async function createUser(username, firstName, lastName) { export async function createUser() {
gqlQuery(`query { await gqlQuery(`mutation {
createUser( createUser(
username: "${username}", username: "${store.session.username}",
name: "${firstName} ${lastName}" name: "${store.session.first_name} ${store.session.last_name}"
)` ) {
).catch(err => { username
}
}); }`);
} }

View File

@ -7,6 +7,7 @@
<script setup> <script setup>
import { store } from "../store"; import { store } from "../store";
import {loggedIn} from "../store";
import TodoRoot from "../components/TodoRoot.vue"; import TodoRoot from "../components/TodoRoot.vue";
/** /**
@ -22,12 +23,12 @@ function login() {
<main> <main>
<!-- Force user to log in before he/she can see tasks. --> <!-- Force user to log in before he/she can see tasks. -->
<div v-if="!store.session" id="login-prompt"> <div v-if="!loggedIn()" id="login-prompt">
<p>Your are currently not logged in.</p> <p>Your are currently not logged in.</p>
<button class="btn btn-primary" @click="login" type="button">Login</button> <button class="btn btn-primary" @click="login" type="button">Login</button>
</div> </div>
<TodoRoot v-if="store.session" /> <TodoRoot v-if="loggedIn()" />
</main> </main>
</template> </template>