BBIO-46 WIP

This commit is contained in:
Markus Thielen 2023-11-01 03:36:55 +01:00
parent 2b7a127701
commit 3c30049dc2
11 changed files with 259 additions and 312 deletions

View File

@ -1,5 +1,5 @@
--
-- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-10-31 15:13:34+01:00
-- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00
--
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -1,5 +1,5 @@
#
# Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-10-31 15:13:34+01:00
# Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00
#
[resolvers.updateTask]
operation_name = "updateTask"
@ -36,6 +36,138 @@ column = "id"
condition_str = "= '$id'"
index = ""
[resolvers.getUser]
operation_name = "getUser"
[resolvers.getUser.resolver.QueryBuilder]
command_type = "SQLSelect"
[resolvers.getUser.resolver.QueryBuilder.command]
table = "User"
command_type = "SQLSelect"
columns = []
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.getUser.resolver.QueryBuilder.command.where_clauses]]
table = "User"
column = "username"
condition_str = "= '$username'"
index = ""
[resolvers.deleteTask]
operation_name = "deleteTask"
[resolvers.deleteTask.resolver.QueryBuilder]
command_type = "SQLDelete"
[resolvers.deleteTask.resolver.QueryBuilder.command]
table = "Task"
command_type = "SQLSelect"
columns = []
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.deleteTask.resolver.QueryBuilder.command.where_clauses]]
table = "Task"
column = "id"
condition_str = "= '$id'"
index = ""
[resolvers.createUser]
operation_name = "createUser"
[resolvers.createUser.resolver.QueryBuilder]
command_type = "SQLInsert"
[resolvers.createUser.resolver.QueryBuilder.command]
table = "User"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
where_clauses = []
aggregate_result = true
[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]]
column = "username"
value = "'$username'"
[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]]
column = "name"
value = "'$name'"
[resolvers.createList]
operation_name = "createList"
[resolvers.createList.resolver.QueryBuilder]
command_type = "SQLInsert"
[resolvers.createList.resolver.QueryBuilder.command]
table = "List"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
where_clauses = []
aggregate_result = true
[[resolvers.createList.resolver.QueryBuilder.command.modify_values]]
column = "title"
value = "'$title'"
[[resolvers.createList.resolver.QueryBuilder.command.modify_values]]
column = "user_username"
value = "'$user.$username'"
[resolvers.updateList]
operation_name = "updateList"
[resolvers.updateList.resolver.QueryBuilder]
command_type = "SQLUpdate"
[resolvers.updateList.resolver.QueryBuilder.command]
table = "List"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.updateList.resolver.QueryBuilder.command.modify_values]]
column = "title"
value = "'$title'"
[[resolvers.updateList.resolver.QueryBuilder.command.where_clauses]]
table = "List"
column = "id"
condition_str = "= '$id'"
index = ""
[resolvers._bb_user_User]
operation_name = "_bb_user_User"
[resolvers._bb_user_User.resolver.InternalQueryBuilder]
command_type = "SQLSelect"
[resolvers._bb_user_User.resolver.InternalQueryBuilder.command]
table = "User"
command_type = "SQLSelect"
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns]]
[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns.Column]
table = "User"
column = "username"
[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.where_clauses]]
table = "User"
column = ".ownerId"
condition_str = "= $1"
index = ""
[resolvers.createTask]
operation_name = "createTask"
@ -70,49 +202,6 @@ value = "'$list.$id'"
column = "user_username"
value = "'$user.$username'"
[resolvers.updateList]
operation_name = "updateList"
[resolvers.updateList.resolver.QueryBuilder]
command_type = "SQLUpdate"
[resolvers.updateList.resolver.QueryBuilder.command]
table = "List"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.updateList.resolver.QueryBuilder.command.modify_values]]
column = "title"
value = "'$title'"
[[resolvers.updateList.resolver.QueryBuilder.command.where_clauses]]
table = "List"
column = "id"
condition_str = "= '$id'"
index = ""
[resolvers.deleteTask]
operation_name = "deleteTask"
[resolvers.deleteTask.resolver.QueryBuilder]
command_type = "SQLDelete"
[resolvers.deleteTask.resolver.QueryBuilder.command]
table = "Task"
command_type = "SQLSelect"
columns = []
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.deleteTask.resolver.QueryBuilder.command.where_clauses]]
table = "Task"
column = "id"
condition_str = "= '$id'"
index = ""
[resolvers.deleteList]
operation_name = "deleteList"
@ -132,92 +221,3 @@ table = "List"
column = "id"
condition_str = "= '$id'"
index = ""
[resolvers.createUser]
operation_name = "createUser"
[resolvers.createUser.resolver.QueryBuilder]
command_type = "SQLInsert"
[resolvers.createUser.resolver.QueryBuilder.command]
table = "User"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
where_clauses = []
aggregate_result = true
[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]]
column = "username"
value = "'$username'"
[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]]
column = "name"
value = "'$name'"
[resolvers._bb_user_User]
operation_name = "_bb_user_User"
[resolvers._bb_user_User.resolver.InternalQueryBuilder]
command_type = "SQLSelect"
[resolvers._bb_user_User.resolver.InternalQueryBuilder.command]
table = "User"
command_type = "SQLSelect"
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns]]
[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns.Column]
table = "User"
column = "username"
[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.where_clauses]]
table = "User"
column = ".ownerId"
condition_str = "= $1"
index = ""
[resolvers.createList]
operation_name = "createList"
[resolvers.createList.resolver.QueryBuilder]
command_type = "SQLInsert"
[resolvers.createList.resolver.QueryBuilder.command]
table = "List"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
where_clauses = []
aggregate_result = true
[[resolvers.createList.resolver.QueryBuilder.command.modify_values]]
column = "title"
value = "'$title'"
[[resolvers.createList.resolver.QueryBuilder.command.modify_values]]
column = "user_username"
value = "'$user.$username'"
[resolvers.getUser]
operation_name = "getUser"
[resolvers.getUser.resolver.QueryBuilder]
command_type = "SQLSelect"
[resolvers.getUser.resolver.QueryBuilder.command]
table = "User"
command_type = "SQLSelect"
columns = []
modify_values = []
nested_modify_tables = []
aggregate_result = true
[[resolvers.getUser.resolver.QueryBuilder.command.where_clauses]]
table = "User"
column = "username"
condition_str = "= '$username'"
index = ""

View File

@ -1,6 +1,6 @@
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import { store } from './store.js'
import { store, loggedIn } from './store.js'
import {clearSession} from "./store.js";
import {Modal} from "bootstrap";
@ -33,7 +33,7 @@ function dismissError() {
</button>
<!-- navigation items are shown only if logged in -->
<div v-if="$oidc.isAuthenticated" class="collapse navbar-collapse" id="navbarSupportedContent">
<div v-if="loggedIn()" class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<RouterLink to="/" class="nav-link" active-class="active">Tasks</RouterLink>

View File

@ -0,0 +1,12 @@
<template>
</template>
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
console.log('Component is mounted!');
});
</script>

View File

View File

@ -1,80 +0,0 @@
/**
* Configure oidc-client library.
*
*/
import Oidc from 'oidc-client';
Oidc.Log.logger = console;
Oidc.Log.level = (process.env.NODE_ENV === 'production') ? Oidc.Log.ERROR : Oidc.Log.DEBUG;
// OIDC configuration
let oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com';
let clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg';
let scopes = "openid profile email"
let instance;
// OIDC Client
export const getOidcClient = () => {
if (instance) {
return instance;
}
instance = new Oidc.UserManager({
userStore: new Oidc.WebStorageStateStore(),
authority: oidcProviderDomain,
client_id: clientId,
redirect_uri: window.location.origin + '/callback',
response_type: 'code',
scope: scopes,
post_logout_redirect_uri: window.location.origin + '/home?action=logout',
accessTokenExpiringNotificationTime: 10,
automaticSilentRenew: false,
filterProtocolClaims: false,
loadUserInfo: true,
includeIdTokenInSilentRenew: false
});
instance.events.addAccessTokenExpiring(function() {
// eslint-disable-next-line no-console
console.log('access token expiring')
})
instance.events.addAccessTokenExpired(function() {
// eslint-disable-next-line no-console
console.log('access token expired')
})
instance.events.addSilentRenewError(function(err) {
// eslint-disable-next-line no-console
console.error('silent renew error', err)
})
instance.events.addUserLoaded(function(user) {
// eslint-disable-next-line no-console
console.log('user loaded', user)
})
instance.events.addUserSignedIn(function(user) {
// eslint-disable-next-line no-console
console.log('user signed in', user)
})
instance.events.addUserUnloaded(function() {
// eslint-disable-next-line no-console
console.log('user unloaded')
})
instance.events.addUserSignedOut(function() {
// eslint-disable-next-line no-console
console.log('user signed out')
})
instance.events.addUserSessionChanged(function() {
// eslint-disable-next-line no-console
console.log('user session changed')
})
return instance;
}

View File

@ -9,8 +9,7 @@ import {loggedIn, showError, store} from "../store";
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { objectToQueryString } from "../util/net";
import {oauthCallbackHandler} from "../util/oauth";
import {storeInit} from "../store";
import { callbackPath, getOidcUserManager } from "../util/oidc";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -38,24 +37,18 @@ const router = createRouter({
router.beforeEach(async (to, from) => {
// Handle OAuth callback request.
// This request is coming in after the user entered her/his credentials at the IdP
// login form; the IdP (e.g. Keycloak) redirects the browser to the URL of this route.
if (to.path.startsWith("/oauth-callback")) {
let queryString = objectToQueryString(to.query);
console.info(`Got /oauth-callback with query string '${queryString}`);
/* Pass the query string to the handler function */
/* catch OIDC callback */
if (to.path === callbackPath) {
const userMgr = getOidcUserManager();
try {
await oauthCallbackHandler(queryString);
const user = await userMgr.signinRedirectCallback();
console.log("OIDC: login complete, user: ", user);
storeInit(user);
/* redirect to home page */
return {name: 'home'};
} catch (err) {
const errorMsg = `Failed to get session info from basebox/finish OpenID Connect login: ${err}`;
console.error(errorMsg);
/* show error */
showError(errorMsg);
console.error("OIDC: login failed: ", err);
}
return {name: 'home'};
}
/* if no user is logged in, we redirect to the home page */

View File

@ -18,8 +18,8 @@ export const store = reactive({
/** base URL of basebox broker host */
baseboxHost: import.meta.env.BASEBOX_HOST || "http://127.0.0.1:8080",
/** basebox session data */
session: {},
/** User as returned from oidc-client */
user: null,
/** Fatal error message, if any */
fatalError: "",
@ -35,14 +35,15 @@ export const store = reactive({
* Return true if the user is logged in.
*/
export function loggedIn() {
return store.session && store.session.token;
return store.user !== null;
}
/**
* Clear session data.
*/
export function clearSession() {
store.session = {};
store.user = null;
store.userDisplayName = "stranger";
}
/**
@ -69,27 +70,17 @@ export function clearError() {
*
* Additionally, we also create the default list here and save everything in the store.
*
* @param session - the session info as returned by the OpenID Connect login.
* @param user - the user as returned from oidc-client.
*/
export async function storeInit(session) {
export async function storeInit(user) {
/* save user session in the store */
store.session = { ...session };
store.user = user;
/* The information in the session object depends on the OpenID Connect provider.
* The following fields are always present:
*
* `token` - basebox session token
* `subject` - unique user id (not a username, rather a number or UUID)
* `id_token_claims` - all fields found in the ID token.
*
* Since we assume Auth0 being the Open ID Connect provider, we use `id_token_claims.nickname`
* as the user name we display in the UI.
*/
store.userDisplayName = session.id_token_claims.nickname;
store.userDisplayName = user.profile.nickname || "Logged In Stranger";
/* The unique user ID is the 'sub' field from the ID token. */
store.userId = session.subject;
store.userId = user.profile.sub;
/* Create user and default list.
* We cannot run these requests in parallel, since the user record must exist in the database

View File

@ -1,58 +0,0 @@
/**
* OAuth specific functions.
*
* Part of the basebox sample Todo app.
* https://basebox.io
*/
import {store} from "../store";
import {storeInit} from "../store";
/**
* Handle OAuth callback and complete login process.
*
* After the user enters her/his credentials at the IdP (Keycloak) login form, she/he
* gets redirected to a client URL; this function handles this request.
*
* This function passes the query string received with the callback to the basebox broker,
* which responds with a session information struct (JSON) like this:
*
* {
* "token":"47caa9df-ac89-45a8-8b22-9c0925d6aed0",
* "username":"tester",
* "first_name":"Fred",
* "last_name":"Feuerstein",
* "roles":[
* "/user"
* ],
* "claims":{
* "groups":[
* "/user"
* ]
* }
* }
*
* The session is then stored in app state.
*
* Then, this function checks if the user is known in the database; since we use Keycloak/OpenID Connect
* for user management, the user might be available in Keycloak, but not in the database. So if the user
* is not there, it is created.
*
* @param queryString
* @returns {Promise<void>}
* @throws Error if the session data cannot be retrieved.
*/
export async function oauthCallbackHandler(queryString) {
/* Pass query string to the broker to complete the login process. */
const response = await fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, {
method: "POST",
});
if (!response.ok) {
throw new Error("Failed to get session data: " + response.statusText);
}
/* Store session data and initialize data store. */
const rspJson = await response.json();
await storeInit(rspJson);
}

74
src/util/oidc.js Normal file
View File

@ -0,0 +1,74 @@
/**
* Configure oidc-client-ts library and retrieve UserManager instance for authentication.
*
*/
import { Log, UserManager, WebStorageStateStore } from 'oidc-client-ts';
Log.logger = console;
Log.level = (process.env.NODE_ENV === 'production') ? Log.ERROR : Log.DEBUG;
// OIDC configuration
const oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com';
const clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg';
const scopes = "openid profile email";
export const callbackPath = "/auth/callback"
/* OIDC UserManager singleton */
let userMgr = null;
// OIDC Client
export const getOidcUserManager = () => {
if (userMgr) {
return userMgr;
}
userMgr = new UserManager({
userStore: new WebStorageStateStore(),
authority: oidcProviderDomain,
client_id: clientId,
redirect_uri: window.location.origin + callbackPath,
response_type: 'code',
scope: scopes,
post_logout_redirect_uri: window.location.origin + '/home?action=logout',
accessTokenExpiringNotificationTime: 10,
automaticSilentRenew: false,
filterProtocolClaims: false,
loadUserInfo: true,
includeIdTokenInSilentRenew: false
});
userMgr.events.addAccessTokenExpiring(function() {
console.info('OIDC: access token expiring')
})
userMgr.events.addAccessTokenExpired(function() {
console.info('OIDC: access token expired')
})
userMgr.events.addSilentRenewError(function(err) {
console.error('silent renew error', err)
})
userMgr.events.addUserLoaded(function(user) {
console.info('OIDC: user loaded', user)
})
userMgr.events.addUserSignedIn(function(user) {
console.info('OIDC: user signed in', user)
})
userMgr.events.addUserUnloaded(function() {
console.info('OIDC: user unloaded')
})
userMgr.events.addUserSignedOut(function() {
console.info('OIDC: user signed out')
})
userMgr.events.addUserSessionChanged(function() {
console.info('OIDC: user session changed')
})
return userMgr;
}

View File

@ -9,6 +9,21 @@
import { store } from "../store";
import {loggedIn} from "../store";
import TodoRoot from "../components/Todo.vue";
import { getOidcUserManager } from "../util/oidc";
/**
* Start login process.
*/
function doLogin() {
console.log("doLogin");
const userMgr = getOidcUserManager();
userMgr.signinRedirect().then(() => {
console.log("signinRedirect");
}).catch((err) => {
console.error("signinRedirect failed", err);
});
}
</script>
@ -16,11 +31,11 @@ import TodoRoot from "../components/Todo.vue";
<main>
<!-- Force user to log in before he/she can see tasks. -->
<div v-if="!$oidc.isAuthenticated" id="login-prompt">
<div v-if="!loggedIn()" id="login-prompt">
<h1>Welcome to basebox' TODO sample app!</h1>
<p class="mt-5">Your are currently not logged in.</p>
<button class="btn btn-primary" @click="$oidc.signIn()" type="button">Login</button>
<button class="btn btn-primary" @click="doLogin()" type="button">Login</button>
</div>
<TodoRoot v-if="loggedIn()" />