This commit is contained in:
Markus Thielen 2023-03-07 22:12:06 +01:00
parent ae400722ba
commit 896ca59ace
Signed by: markus
GPG Key ID: 3D4980D3EC9C8E26
15 changed files with 248 additions and 14 deletions

View File

@ -24,17 +24,17 @@ modify_table = ["Task", ""]
modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"]] modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"]]
aggregate_final_json_result = true aggregate_final_json_result = true
[resolvers.getUser] [resolvers.createUser]
operation_name = "getUser" operation_name = "createUser"
[resolvers.getUser.resolver] [resolvers.createUser.resolver]
command_type = "SQLSelect" command_type = "SQLInsert"
columns = [] columns = []
tables = [["User", ""]] tables = []
where_clauses = [["User", "username", "= '$username'"]] where_clauses = []
join_clauses = [] join_clauses = []
modify_table = ["", ""] modify_table = ["User", ""]
modify_values = [] modify_values = [["username", "'$username'"], ["name", "'$name'"]]
aggregate_final_json_result = true aggregate_final_json_result = true
[resolvers.createList] [resolvers.createList]
@ -62,3 +62,16 @@ join_clauses = []
modify_table = ["Task", ""] modify_table = ["Task", ""]
modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"], ["user_username", "'$user.$username'"]] modify_values = [["title", "'$title'"], ["description", "'$description'"], ["completed", "$completed"], ["list_id", "'$list.$id'"], ["user_username", "'$user.$username'"]]
aggregate_final_json_result = true aggregate_final_json_result = true
[resolvers.getUser]
operation_name = "getUser"
[resolvers.getUser.resolver]
command_type = "SQLSelect"
columns = []
tables = [["User", ""]]
where_clauses = [["User", "username", "= '$username'"]]
join_clauses = []
modify_table = ["", ""]
modify_values = []
aggregate_final_json_result = true

View File

@ -4,7 +4,7 @@ log_level = "trace"
[graphql] [graphql]
# path and file name to GraphQL schema file # path and file name to GraphQL schema file
schema_file = "todo5_schema.graphql" schema_file = "todo_schema.graphql"
[proxy] [proxy]
# host name or IP of basebox DB proxy # host name or IP of basebox DB proxy

3
bbconf/compile_schema.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
# Run basebox installer from the samples/toodo/bbconf directory
cargo run --manifest-path ../../../installer/Cargo.toml -- -c install-config.toml

View File

@ -20,7 +20,7 @@ acc_aud = "account"
[graphql] [graphql]
# path and file name to GraphQL schema file # path and file name to GraphQL schema file
schema_file = "todo5_schema.graphql" schema_file = "todo_schema.graphql"
# Path and file name of the resolver map file # Path and file name of the resolver map file
resolver_map_file = "bb_todo_resolvers.toml" resolver_map_file = "bb_todo_resolvers.toml"
# Path and file name of the type map file # Path and file name of the type map file

View File

@ -0,0 +1,18 @@
# Sample toml file; for testing only
[generic]
# the name of the project
project_name = "bb_todo"
# the folder where the generated files will be placed. If not specifed, the project name will be
# used to create a folder by that name in the current directory. Note that the folder must not exist
# yet in order to make ensure that an existing installation will not be overwritten by accident.
output = "output"
[log]
# log level; can be off, error, warn, info, debug, trace
log_level = "info"
[graphql]
schema = "todo_schema.graphql"
[database]

View File

@ -44,6 +44,11 @@ type Query {
type Mutation { type Mutation {
createUser(
username: String!,
name: String!
): User @bb_resolver(_type: insert, _object: User, _fields: { username: "$username", name: "$name" })
createList( createList(
title: String! title: String!
user: User! # username needs to be specified as it's non-nullable user: User! # username needs to be specified as it's non-nullable

View File

@ -1,3 +1,9 @@
<!--
Component that dispalys authentication related errors.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<template> <template>
<div id="error-message"> <div id="error-message">

View File

@ -1,6 +1,8 @@
<!-- <!--
Dummy component that handles OAuth login callback requests. Dummy component that handles OAuth login callback requests.
Part of the basebox sample Todo app.
https://basebox.tech
--> -->
<script setup> <script setup>

View File

@ -1,3 +1,9 @@
<!--
About component.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<script setup> <script setup>
import AboutItem from './AboutItem.vue' import AboutItem from './AboutItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue' import DocumentationIcon from './icons/IconDocumentation.vue'

View File

@ -0,0 +1,71 @@
<!--
Root component for the Todo app that is displayed if the user is logged in.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<template>
</template>
<script>
import {gqlQuery} from "../util/net";
import {store} from "../store";
export default {
name: "TodoRoot",
/** Current state of the component */
data() {
return {
/* array of todo lists currently known */
lists: [],
/* the name of the list currently being shown */
currentList: null,
/* tasks of the current list */
tasks: [],
}
},
/**
* mounted lifecycle hook.
*/
mounted() {
if (!store.session) {
console.error("TodoRoot component must not be loaded if user is not logged in.");
return;
}
/* Load user info and his/her lists and todos */
gqlQuery(`query {
getUser(username: "${store.session.userName}") {
name
lists {
id
title
}
tasks {
id
title
description
list {
id
}
}
}
}`).then(rspJson => {
}).catch(err => {
});
},
}
</script>
<style scoped>
</style>

View File

@ -1,3 +1,9 @@
<!--
Welcome component.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<script setup> <script setup>
import { store} from "../store"; import { store} from "../store";

View File

@ -55,7 +55,7 @@ router.beforeEach(async (to, from) => {
}); });
if (!response.ok) { if (!response.ok) {
/* redirect to error component */ /* redirect to error component */
console.error("Failed to get session token: " + response.statusText); console.error("Failed to complete login/get session data: " + response.statusText);
return { return {
name: 'oauth-error', name: 'oauth-error',
query: { query: {

View File

@ -1,7 +1,8 @@
/** /**
* Network related functions. * Network related functions.
* *
* markus.thielen@basebox.health * Part of the basebox sample Todo app.
* https://basebox.tech
*/ */
import {store} from "../store"; import {store} from "../store";
@ -158,7 +159,8 @@ export function gqlQuery(query)
} }
/** /**
* Encode object as a query string * Encode simple, unnested objects (dicts) as a query string
*
* @param obj - a JavaScript object to encode * @param obj - a JavaScript object to encode
* @returns {string} query string, e.g. "parm1=778&read=all" * @returns {string} query string, e.g. "parm1=778&read=all"
*/ */
@ -170,3 +172,4 @@ export function objectToQueryString(obj) {
} }
return str.join("&"); return str.join("&");
} }

90
src/util/oauth.js Normal file
View File

@ -0,0 +1,90 @@
/**
* OAuth specific functions.
*
* Part of the basebox sample Todo app.
* https://basebox.tech
*/
import {store} from "../store";
import {gqlQuery} from "./net";
/**
* 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) {
fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, {
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());
});
}
/**
* Create the user on the broker/todo database.
*
* Since user management is done by Keycloak, which uses its own user database, we must
* make sure that out user is known in the app database (basebox broker). After successful
* login, this function must be called to do that.
*
* @param username unique username of new user
* @param firstName first name of new user
* @param lastName last name of new user
* @returns {Promise<void>}
*/
async function createUser(username, firstName, lastName) {
gqlQuery(`query {
createUser(
username: "${username}",
name: "${firstName} ${lastName}"
)`
).catch(err => {
});
}

View File

@ -1,6 +1,13 @@
<!--
Home view.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<script setup> <script setup>
import { store } from "../store"; import { store } from "../store";
import TodoRoot from "../components/TodoRoot.vue";
/** /**
* Perform a login. * Perform a login.
@ -13,11 +20,15 @@ function login() {
<template> <template>
<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="!store.session" 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" />
</main> </main>
</template> </template>