Add login logic

This commit is contained in:
2024-05-01 13:38:58 +02:00
parent 9f21a36406
commit d0af8f0250
12 changed files with 368 additions and 90 deletions

View File

@@ -8,7 +8,9 @@
"name": "frontend",
"version": "0.0.1",
"dependencies": {
"@dagrejs/dagre": "github:dagrejs/dagre",
"@xyflow/svelte": "^0.0.41",
"oidc-client-ts": "^3.0.1",
"svelte-eslint-parser": "^0.33.1"
},
"devDependencies": {
@@ -572,6 +574,22 @@
"node": ">=6.9.0"
}
},
"node_modules/@dagrejs/dagre": {
"version": "1.1.3-pre",
"resolved": "git+ssh://git@github.com/dagrejs/dagre.git#e6d4c7f6f95834fc794d82e6c803ead3aa3816d2",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "2.2.2"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.2.tgz",
"integrity": "sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==",
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -3205,6 +3223,14 @@
"node": ">=6"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3480,6 +3506,17 @@
"node": ">= 6"
}
},
"node_modules/oidc-client-ts": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz",
"integrity": "sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg==",
"dependencies": {
"jwt-decode": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",

View File

@@ -30,7 +30,9 @@
},
"type": "module",
"dependencies": {
"@dagrejs/dagre": "github:dagrejs/dagre",
"@xyflow/svelte": "^0.0.41",
"oidc-client-ts": "^3.0.1",
"svelte-eslint-parser": "^0.33.1"
}
}

View File

@@ -1,51 +1,79 @@
<!doctype html>
<html lang="en" style="width: 100vw; height: 100vh;">
<html lang="en" style="width: 100vw; height: 100vh">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" style="width: 100vw; height: 100vh;">
<div style="display: contents; width: 100vw; height: 100vh;" class="bg-base-100">%sveltekit.body%</div>
<div class="dropdown mb-72" style="position: absolute; left: auto; right: 3vw; top: 10px; bottom: auto">
<div tabindex="0" role="button" class="btn m-1">
Theme
<svg width="12px" height="12px" class="h-2 w-2 fill-current opacity-60 inline-block"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path>
</svg>
<body data-sveltekit-preload-data="hover" style="width: 100vw; height: 100vh">
<div style="display: contents; width: 100vw; height: 100vh" class="bg-base-100">
%sveltekit.body%
</div>
<ul tabindex="0" class="dropdown-content z-[1] p-2 shadow-2xl bg-base-300 rounded-box w-36">
<li>
<input type="radio" name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start" aria-label="Light"
value="light" />
</li>
<li>
<input type="radio" name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start" aria-label="Dark"
value="dark" />
</li>
<li>
<input type="radio" name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start" aria-label="Cyberpunk"
value="cyberpunk" />
</li>
<li>
<input type="radio" name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start" aria-label="Synthwave"
value="synthwave" />
</li>
<li>
<input type="radio" name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start" aria-label="Retro"
value="retro" />
</li>
</ul>
</div>
</body>
</html>
<div
class="dropdown mb-72"
style="position: absolute; left: auto; right: 3vw; top: 10px; bottom: auto"
>
<div tabindex="0" role="button" class="btn m-1">
Theme
<svg
width="12px"
height="12px"
class="h-2 w-2 fill-current opacity-60 inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2048 2048"
>
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path>
</svg>
</div>
<ul tabindex="0" class="dropdown-content z-[1] p-2 shadow-2xl bg-base-300 rounded-box w-36">
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Light"
value="light"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Dark"
value="dark"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Cyberpunk"
value="cyberpunk"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Synthwave"
value="synthwave"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Retro"
value="retro"
/>
</li>
</ul>
</div>
</body>
</html>

70
frontend/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,70 @@
import { UserManager, WebStorageStateStore, User } from 'oidc-client-ts';
import { isAuthenticated, user } from './stores';
import {
PUBLIC_ZITADEL_CLIENT_ID,
PUBLIC_ISSUER,
PUBLIC_LOGIN_REDIRECT_URI,
PUBLIC_LOGOUT_REDIRECT_URI
} from '$env/static/public';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let userManager: UserManager;
if (browser) {
const config = {
authority: PUBLIC_ISSUER, // At Zitadel Project Console > [Your project] > [Your application] > URLs - Issuer
client_id: PUBLIC_ZITADEL_CLIENT_ID, // At Zitadel Project Console > [Your project] > [Your application] > Configuration - Client ID
redirect_uri: PUBLIC_LOGIN_REDIRECT_URI+ '/callback', // At Zitadel Project Console > [Your project] > [Your application] > URLs - Login Redirect URI
response_type: 'code',
scope: 'openid profile email',
post_logout_redirect_uri: PUBLIC_LOGOUT_REDIRECT_URI,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: true,
silent_redirect_uri: PUBLIC_LOGIN_REDIRECT_URI + '/silent-refresh'
};
userManager = new UserManager(config);
userManager.events.addUserLoaded((loadedUser: User) => {
console.log('userManager.events.addUserLoaded');
user.set(loadedUser);
isAuthenticated.set(true);
});
userManager.events.addUserUnloaded(() => {
console.log('userManager.events.addUserUnloaded');
user.set(null);
isAuthenticated.set(false);
});
}
async function login(): Promise<void> {
console.log('UserManager.login()');
if (browser) {
await userManager.signinRedirect();
}
}
async function logout(): Promise<void> {
if (browser) {
await userManager.signoutRedirect();
}
}
async function handleCallback(): Promise<void> {
if (browser) {
await userManager.signinRedirectCallback();
goto('/');
}
}
async function handleSilentCallback(): Promise<void> {
if (browser) {
await userManager.signinSilentCallback();
goto('/');
}
}
export { login, logout, handleCallback, handleSilentCallback };

View File

@@ -2,12 +2,11 @@
import { Handle, Position } from '@xyflow/svelte';
export let person = {
ID : "",
Lastname : "Nem",
Firstname : "Ismert",
Middlename : "",
ProfilePicture : "https://daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg",
ID: '',
Lastname: 'Nem',
Firstname: 'Ismert',
Middlename: '',
ProfilePicture: 'https://www.flaticon.com/free-icons/user'
};
</script>
@@ -15,10 +14,14 @@
<div class="card-body items-center text-center w-30">
<div class="avatar">
<figure class="w-24 mask mask-squircle">
<img src="{person.ProfilePicture}" alt="Picture of {person.Lastname} {person.Firstname}" />
<img src={person.ProfilePicture} alt="Picture of {person.Lastname} {person.Firstname}" />
</figure>
</div>
<h2 class="card-title text-primary-content">{person.Lastname} {person.Firstname} {person.Middlename}</h2>
<h2 class="card-title text-primary-content">
{person.Lastname}
{person.Firstname}
{person.Middlename}
</h2>
</div>
</div>
<Handle

View File

@@ -0,0 +1,40 @@
import dagre from '@dagrejs/dagre';
import { Position, type Node, type Edge } from '@xyflow/svelte';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 36;
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
};
});
return { nodes, edges };
}
export { getLayoutedElements };

View File

@@ -0,0 +1,27 @@
import { PUBLIC_API_URL } from '$env/static/public';
import { user } from '$lib/stores';
let auth_token: string;
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
async function fetch_family_tree() {
const response = await fetch(
{ PUBLIC_API_URL } + '/familyTree?id=8a8b9b05bdc24550a5cc73e0b55e8d7d',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: auth_token
}
}
);
const data = await response.json();
return data;
}
export { fetch_family_tree };

View File

@@ -0,0 +1,5 @@
import { writable } from 'svelte/store';
import type { User } from 'oidc-client-ts';
export const isAuthenticated = writable<boolean>(false);
export const user = writable<User | null>(null);

View File

@@ -1,47 +1,78 @@
<script>
import PersonNode from './../lib/family_tree/PersonNode.svelte';
<script lang="ts">
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import { SvelteFlowProvider, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
import type { Node, Edge, NodeTypes } from '@xyflow/svelte';
import { isAuthenticated } from '../lib/stores';
import PersonNode from './../lib/family_tree/PersonNode.svelte';
import { login } from '../lib/auth';
import { fetch_family_tree } from '$lib/family_tree/getFamilyTree';
import { getLayoutedElements } from '$lib/family_tree/dagreLayout';
const nodes = writable([
{
id: '1',
data: { label: 'Hello' },
position: { x: 0, y: 0 }
},
{
id: '2',
data: { label: 'World' },
position: { x: 0, y: 150 }
},
{
id: '3',
data: { label: 'World' },
position: { x: 250, y: 150 },
type: 'custom'
let edges_data: {
id: string;
source: string;
target: string;
data: { type: string; verified: boolean };
}[] = [];
const nodes = writable<Node[]>([]);
const edges = writable<Edge[]>([]);
onMount(() => {
if (!$isAuthenticated) {
console.log('user authenticated:', $isAuthenticated);
login();
} else {
console.log('user authenticated:', $isAuthenticated);
console.log('fetching nodes');
fetch_family_tree().then((data) => {
let Position = { x: 0, y: 0 };
let nodes_data: Node[] = [];
function AddToNodesData(data: any, i: number) {
if (data[0].Values[i] != null) {
if (Object.prototype.toString.call(data[0].Values[i]) === '[object Array]') {
data[0].Values[i].forEach((person: { ElementId: string; Props: {} }) => {
nodes_data.push({
id: person.ElementId,
type: 'custom',
data: person.Props,
position: Position
});
});
} else {
nodes_data.push({
id: data[0].Values[i].ElementId,
type: 'custom',
data: data[0].Values[i].Props,
position: Position
});
}
}
}
AddToNodesData(data, 0);
AddToNodesData(data, 2);
AddToNodesData(data, 4);
AddToNodesData(data, 6);
AddToNodesData(data, 8);
edges_data = [];
const layoutedElements = getLayoutedElements(nodes_data, edges_data, 'TB');
$nodes = layoutedElements.nodes;
$edges = layoutedElements.edges;
});
}
]);
const edges = writable([
{
id: '1-2',
source: '1',
target: '2'
},
{
id: '1-3',
source: '1',
target: '3'
}
]);
const nodeTypes = {
});
const nodeTypes: NodeTypes = {
custom: PersonNode
};
</script>
<div style="height:100vh;">
<SvelteFlowProvider>
<SvelteFlow {nodes} {nodeTypes} {edges} class="bg-base-100" fitView=True onlyRenderVisibleElements=True>
<Controls class="bg-base-300 text-primary-content"/>
<SvelteFlow {nodes} {nodeTypes} {edges} class="bg-base-100" fitView onlyRenderVisibleElements>
<Controls class="bg-base-300 text-primary-content" />
<MiniMap />
</SvelteFlow>
</SvelteFlowProvider>

View File

@@ -0,0 +1,11 @@
<!-- src/routes/callback.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { handleCallback } from '../../lib/auth';
onMount(() => {
handleCallback();
});
</script>
<div>Logging in...</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { handleSilentCallback } from '../../lib/auth';
onMount(() => {
handleSilentCallback();
});
</script>
<div>Refreshing...</div>

14
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
},
"include": ["src/**/*", "src/node_modules", ".svelte-kit/ambient.d.ts"] // see last element
}