BBIO-46 WIP

This commit is contained in:
Markus Thielen 2023-11-01 11:44:19 +01:00
parent be1e94ee06
commit b387643068
11 changed files with 169 additions and 143 deletions

View File

@ -1,5 +1,5 @@
-- --
-- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00 -- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-11-01 10:35:59+01:00
-- --
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
@ -23,6 +23,12 @@ CREATE TABLE "User" (
"name" VARCHAR "name" VARCHAR
); );
ALTER TABLE "List" ADD COLUMN ".ownerId" VARCHAR NOT NULL;
ALTER TABLE "Task" ADD COLUMN ".ownerId" VARCHAR NOT NULL;
ALTER TABLE "User" ADD COLUMN ".ownerId" VARCHAR NOT NULL;
ALTER TABLE "List" ADD PRIMARY KEY ("id"); ALTER TABLE "List" ADD PRIMARY KEY ("id");
ALTER TABLE "Task" ADD PRIMARY KEY ("id"); ALTER TABLE "Task" ADD PRIMARY KEY ("id");
@ -37,9 +43,3 @@ ALTER TABLE "Task" ADD CONSTRAINT fk_task_3 FOREIGN KEY ("list_id") REFERENCES "
ALTER TABLE "User" ADD CONSTRAINT uq_user_4 UNIQUE (".ownerId"); ALTER TABLE "User" ADD CONSTRAINT uq_user_4 UNIQUE (".ownerId");
ALTER TABLE "List" ADD COLUMN ".ownerId" VARCHAR NOT NULL;
ALTER TABLE "Task" ADD COLUMN ".ownerId" VARCHAR NOT NULL;
ALTER TABLE "User" ADD COLUMN ".ownerId" VARCHAR NOT NULL;

View File

@ -1,6 +1,31 @@
# #
# Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00 # Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-11-01 10:35:59+01:00
# #
[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.updateTask] [resolvers.updateTask]
operation_name = "updateTask" operation_name = "updateTask"
@ -36,45 +61,39 @@ column = "id"
condition_str = "= '$id'" condition_str = "= '$id'"
index = "" index = ""
[resolvers.getUser] [resolvers.createTask]
operation_name = "getUser" operation_name = "createTask"
[resolvers.getUser.resolver.QueryBuilder] [resolvers.createTask.resolver.QueryBuilder]
command_type = "SQLSelect" command_type = "SQLInsert"
[resolvers.getUser.resolver.QueryBuilder.command] [resolvers.createTask.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" table = "Task"
command_type = "SQLSelect" command_type = "SQLSelect"
columns = [] columns = []
modify_values = []
nested_modify_tables = [] nested_modify_tables = []
where_clauses = []
aggregate_result = true aggregate_result = true
[[resolvers.deleteTask.resolver.QueryBuilder.command.where_clauses]] [[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
table = "Task" column = "title"
column = "id" value = "'$title'"
condition_str = "= '$id'"
index = "" [[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "description"
value = "'$description'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "completed"
value = "'$completed'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "list_id"
value = "'$list.$id'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "user_username"
value = "'$user.$username'"
[resolvers.createUser] [resolvers.createUser]
operation_name = "createUser" operation_name = "createUser"
@ -98,6 +117,46 @@ value = "'$username'"
column = "name" column = "name"
value = "'$name'" value = "'$name'"
[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.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.createList] [resolvers.createList]
operation_name = "createList" operation_name = "createList"
@ -143,65 +202,6 @@ column = "id"
condition_str = "= '$id'" condition_str = "= '$id'"
index = "" 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"
[resolvers.createTask.resolver.QueryBuilder]
command_type = "SQLInsert"
[resolvers.createTask.resolver.QueryBuilder.command]
table = "Task"
command_type = "SQLSelect"
columns = []
nested_modify_tables = []
where_clauses = []
aggregate_result = true
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "title"
value = "'$title'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "description"
value = "'$description'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "completed"
value = "'$completed'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "list_id"
value = "'$list.$id'"
[[resolvers.createTask.resolver.QueryBuilder.command.modify_values]]
column = "user_username"
value = "'$user.$username'"
[resolvers.deleteList] [resolvers.deleteList]
operation_name = "deleteList" operation_name = "deleteList"

View File

@ -3,13 +3,14 @@ import { RouterLink, RouterView } from 'vue-router'
import { store, loggedIn } from './store.js' import { store, loggedIn } from './store.js'
import {clearSession} from "./store.js"; import {clearSession} from "./store.js";
import {Modal} from "bootstrap"; import {Modal} from "bootstrap";
import { getOidcUserManager } from './util/oidc';
/** /**
* Logout the user. * Logout the user.
*/ */
function logOut() { function logOut() {
clearSession(); clearSession();
location.href = "/"; getOidcUserManager().signoutRedirect();
} }
/** /**
@ -48,7 +49,7 @@ function dismissError() {
<ul class="navbar-nav mb-2 mb-lg-0"> <ul class="navbar-nav mb-2 mb-lg-0">
<li class="navbar-text">Hello, {{ store.userDisplayName }}!</li> <li class="navbar-text">Hello, {{ store.userDisplayName }}!</li>
<li class="nav-item ms-2"> <li class="nav-item ms-2">
<button class="nav-link btn btn-sm btn-secondary" @click="$oidc.signOut()">Logout</button> <button class="nav-link btn btn-sm btn-secondary" @click="logOut()">Logout</button>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -53,9 +53,9 @@
</template> </template>
<script> <script>
import {gqlQuery} from "../util/net"; import { gqlRequest } from "../util/net";
import {showError, store} from "../store"; import { showError, store } from "../store";
import {showModal} from "../util/misc"; import { showModal } from "../util/misc";
const NEW_LIST_ID = "new-list-id"; const NEW_LIST_ID = "new-list-id";
@ -96,12 +96,17 @@ export default {
}`; }`;
} }
/* send query */ /* send query */
gqlQuery( gqlRequest(
gql gql
).then(data => { ).then(data => {
/* Save the list's id in case it was just created */ /* Save the list's id in case it was just created */
if (!list.id) { if (list.id === NEW_LIST_ID) {
list.id = data.createList.id; list.id = data.createList.id;
/* Update list id in data store */
const idx = store.lists.findIndex(item => item.id === NEW_LIST_ID);
if (idx !== -1) {
store.lists[idx].id = list.id;
}
} }
}).catch(e => { }).catch(e => {
const errMsg = `Failed to save list: ${e}`; const errMsg = `Failed to save list: ${e}`;
@ -122,7 +127,7 @@ export default {
return; return;
} }
/* List must be also deleted from the server */ /* List must be also deleted from the server */
gqlQuery(`mutation { gqlRequest(`mutation {
deleteList(id: "${list.id}") { deleteList(id: "${list.id}") {
id id
} }

View File

@ -73,9 +73,9 @@
</template> </template>
<script> <script>
import {gqlQuery} from "../util/net"; import { gqlRequest } from "../util/net";
import {showError, store} from "../store"; import { removeTask, showError, store } from "../store";
import {showModal} from "../util/misc"; import { showModal } from "../util/misc";
const NEW_TASK_ID = "new-task-id"; const NEW_TASK_ID = "new-task-id";
@ -137,7 +137,7 @@ export default {
} }
/* send query */ /* send query */
gqlQuery( gqlRequest(
request request
).then(data => { ).then(data => {
/* Save the task's id in case it was just created */ /* Save the task's id in case it was just created */
@ -158,18 +158,24 @@ export default {
deleteTask(task) { deleteTask(task) {
if (task.id !== NEW_TASK_ID) { if (task.id !== NEW_TASK_ID) {
/* Task must be also deleted from the server */ /* Task must be also deleted from the server */
gqlQuery(`mutation { gqlRequest(`mutation {
deleteTask(id: "${task.id}") { deleteTask(id: "${task.id}") {
id id
} }
}`); }`).then(data => {
console.info(`Deleted task '${task.title}' with id ${task.id}`);
/* Also remove task from the store */
removeTask(task);
}).catch(e => {
const errMsg = `Failed to delete task: ${e}`;
console.error(errMsg);
showError(errMsg);
});
} else {
/* Just delete task from the store */
removeTask(task);
} }
/* delete task from the store. */
const idx = store.tasks.findIndex(item => item.id === task.id);
if (idx !== -1) {
store.tasks.splice(idx, 1);
}
}, },

View File

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

View File

@ -5,7 +5,7 @@
* https://basebox.io * https://basebox.io
*/ */
import {loggedIn, showError, store} from "../store"; import {loggedIn, showError, storeInit } from "../store";
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import { objectToQueryString } from "../util/net"; import { objectToQueryString } from "../util/net";
@ -39,7 +39,7 @@ router.beforeEach(async (to, from) => {
/* catch OIDC callback */ /* catch OIDC callback */
if (to.path === callbackPath) { if (to.path === callbackPath) {
console.info("OIDC: got signin callback request to ", to); console.info("OIDC: got OIDC callback request to ", to);
const userMgr = getOidcUserManager(); const userMgr = getOidcUserManager();
try { try {
const user = await userMgr.signinRedirectCallback(location.origin + to.fullPath); const user = await userMgr.signinRedirectCallback(location.origin + to.fullPath);
@ -47,6 +47,7 @@ router.beforeEach(async (to, from) => {
storeInit(user); storeInit(user);
} catch (err) { } catch (err) {
console.error("OIDC: Error handling signin callback: ", err); console.error("OIDC: Error handling signin callback: ", err);
showError("The login process failed: " + err);
} }
/* redirect to home page */ /* redirect to home page */
return {name: 'home'}; return {name: 'home'};

View File

@ -5,7 +5,7 @@
* https://basebox.io * https://basebox.io
*/ */
import { reactive } from 'vue' import { reactive } from 'vue'
import {gqlQuery} from "./util/net"; import {gqlRequest} from "./util/net";
export const store = reactive({ export const store = reactive({
@ -60,6 +60,18 @@ export function clearError() {
store.fatalError = ""; store.fatalError = "";
} }
/**
* Remove a task from the store.
*
* This does not remove it from the database/server!
*/
export function removeTask(task) {
const idx = store.tasks.findIndex(item => item.id === task.id);
if (idx !== -1) {
store.tasks.splice(idx, 1);
}
}
/** /**
* Initialize data store and database. * Initialize data store and database.
* *
@ -88,7 +100,7 @@ export async function storeInit(user) {
*/ */
try { try {
console.info("NOTE: If next request fails, it is probably because the user already exists. In this case, the error is ignored."); console.info("NOTE: If next request fails, it is probably because the user already exists. In this case, the error is ignored.");
await gqlQuery(`mutation { await gqlRequest(`mutation {
createUser( createUser(
username: "${store.userId}", username: "${store.userId}",
name: "${store.userDisplayName}" name: "${store.userDisplayName}"
@ -108,7 +120,7 @@ export async function storeInit(user) {
} }
/* Load user info, lists and todos. */ /* Load user info, lists and todos. */
gqlQuery(`query { gqlRequest(`query {
getUser(username: "${store.userId}") { getUser(username: "${store.userId}") {
name name
lists { lists {
@ -131,14 +143,18 @@ export async function storeInit(user) {
/* create default list if necessary */ /* create default list if necessary */
if (store.lists.length === 0) { if (store.lists.length === 0) {
console.info("No lists found for current user; creating default list."); console.info("No lists found for current user; creating default list.");
gqlQuery(`mutation { gqlRequest(`mutation {
createList( createList(
title: "Default", title: "Default",
user: { username: "${store.userId}" } user: { username: "${store.userId}" }
) { ) {
title title
id
} }
}`); }`).then(data => {
/* Save new default list in store */
store.lists.push(data.createList);
});
} }
/* fix for basebox bug: task.list is returned as an array of List objects, where we expect just a single object */ /* fix for basebox bug: task.list is returned as an array of List objects, where we expect just a single object */

View File

@ -8,7 +8,7 @@
import {clearSession, store} from "../store"; import {clearSession, store} from "../store";
/** /**
* GqlError - custom error thrown by the gqlQuery function. * GqlError - custom error thrown by the gqlRequest function.
* *
* Since the GraphQL server might through multiple errors at once, * Since the GraphQL server might through multiple errors at once,
* this error class uses a message array. * this error class uses a message array.
@ -102,7 +102,7 @@ class GqlError extends Error {
* Error with error message array on failure * Error with error message array on failure
*/ */
export function gqlQuery(query) export function gqlRequest(query)
{ {
/* prepare fetch options */ /* prepare fetch options */
const fetchOpt = { const fetchOpt = {
@ -113,9 +113,9 @@ export function gqlQuery(query)
body: JSON.stringify({ query: query }), body: JSON.stringify({ query: query }),
}; };
/* if we're logged in, we add the authorization header */ /* if we're logged in, we add the authorization header with the access token */
if (store.session) { if (store.user) {
fetchOpt.headers["Authorization"] = `Bearer ${store.session.token}`; fetchOpt.headers["Authorization"] = `Bearer ${store.user.access_token}`;
} }
console.info(`Sending request:\n${query}`); console.info(`Sending request:\n${query}`);

View File

@ -1,6 +1,9 @@
/** /**
* Configure oidc-client-ts library and retrieve UserManager instance for authentication. * Configure oidc-client-ts library and retrieve UserManager instance for authentication.
* *
* The documentation of oidc-client-ts is very...thin; this might be helpful:
* <https://gist.github.com/davidamidon/24c8a6980e116e62f781be4d6239d10d>
*
*/ */
import { Log, UserManager, WebStorageStateStore } from 'oidc-client-ts'; import { Log, UserManager, WebStorageStateStore } from 'oidc-client-ts';
@ -11,13 +14,18 @@ Log.level = (process.env.NODE_ENV === 'production') ? Log.ERROR : Log.DEBUG;
// OIDC configuration // OIDC configuration
const oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com'; const oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com';
const clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg'; const clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg';
const clientSecret = 'QlHMvIffKLRviCcSu_bPQcf8e4dc6WeS3BwZE1r1F-9R30AFoeYEwaOazAuFenI5';
const scopes = "openid profile email name nickname"; const scopes = "openid profile email name nickname";
export const callbackPath = "/auth/callback" export const callbackPath = "/auth/callback"
/* OIDC UserManager singleton */ /* OIDC UserManager singleton */
let userMgr = null; let userMgr = null;
// OIDC Client /**
* Get OIDC UserManager singleton.
*
* @returns OIDC UserManager singleton.
*/
export const getOidcUserManager = () => { export const getOidcUserManager = () => {
if (userMgr) { if (userMgr) {
return userMgr; return userMgr;
@ -27,11 +35,12 @@ export const getOidcUserManager = () => {
userStore: new WebStorageStateStore(), userStore: new WebStorageStateStore(),
authority: oidcProviderDomain, authority: oidcProviderDomain,
client_id: clientId, client_id: clientId,
client_secret: clientSecret,
redirect_uri: window.location.origin + callbackPath, redirect_uri: window.location.origin + callbackPath,
response_type: 'code', response_type: 'code',
response_mode: 'query', response_mode: 'query',
scope: scopes, scope: scopes,
post_logout_redirect_uri: window.location.origin + '/home?action=logout', post_logout_redirect_uri: window.location.origin,
accessTokenExpiringNotificationTime: 10, accessTokenExpiringNotificationTime: 10,
automaticSilentRenew: false, automaticSilentRenew: false,
filterProtocolClaims: false, filterProtocolClaims: false,