Compare commits
No commits in common. "22b74987e53038dd1a9623584545b49e6a0da787" and "d9167dcd470825e6101af9fd3609257690546193" have entirely different histories.
22b74987e5
...
d9167dcd47
30 changed files with 400 additions and 1254 deletions
|
@ -15,14 +15,6 @@ npm run dev
|
||||||
npm run dev -- --open
|
npm run dev -- --open
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handle the prisma ORM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./run/db_up.sh # Start the docker container, create a new database and run migrations + insert test data
|
|
||||||
npx prisma migrate dev --name my_migration --create-only # Create a new migration
|
|
||||||
npx prisma migrate dev # Apply migrations to the database
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
To create a production version of your app:
|
To create a production version of your app:
|
||||||
|
|
40
package.json
40
package.json
|
@ -18,53 +18,49 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.27.0",
|
"@auth/core": "^0.27.0",
|
||||||
|
"@auth/sveltekit": "^0.13.0",
|
||||||
"@floating-ui/core": "^1.6.0",
|
"@floating-ui/core": "^1.6.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.9.1",
|
||||||
"diff": "^5.2.0",
|
"isomorphic-dompurify": "^2.3.0",
|
||||||
"isomorphic-dompurify": "^2.4.0",
|
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
"set-cookie-parser": "^2.6.0",
|
|
||||||
"svelte-floating-ui": "^1.5.8",
|
"svelte-floating-ui": "^1.5.8",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-form-data": "^2.0.2"
|
"zod-form-data": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@playwright/test": "^1.42.0",
|
"@playwright/test": "^1.41.2",
|
||||||
"@sveltejs/adapter-node": "^4.0.1",
|
"@sveltejs/adapter-node": "^4.0.1",
|
||||||
"@sveltejs/kit": "^2.5.2",
|
"@sveltejs/kit": "^2.5.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@trpc/client": "^10.45.1",
|
"@trpc/client": "^10.45.1",
|
||||||
"@trpc/server": "^10.45.1",
|
"@trpc/server": "^10.45.1",
|
||||||
"@types/diff": "^5.0.9",
|
"@types/node": "^20.11.19",
|
||||||
"@types/node": "^20.11.21",
|
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||||
"@types/set-cookie-parser": "^2.4.7",
|
"@typescript-eslint/parser": "^7.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"daisyui": "^4.7.2",
|
"daisyui": "^4.7.2",
|
||||||
"dotenv": "^16.4.5",
|
"eslint": "^8.56.0",
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-no-relative-import-paths": "^1.5.3",
|
"eslint-plugin-no-relative-import-paths": "^1.5.3",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
"postcss-import": "^16.0.1",
|
"postcss-import": "^16.0.1",
|
||||||
"postcss-nesting": "^12.0.4",
|
"postcss-nesting": "^12.0.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.2.2",
|
"prettier-plugin-svelte": "^3.2.1",
|
||||||
"prisma": "^5.10.2",
|
"prisma": "^5.9.1",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.11",
|
||||||
"svelte-check": "^3.6.5",
|
"svelte-check": "^3.6.4",
|
||||||
"sveltekit-superforms": "^2.6.2",
|
"sveltekit-superforms": "^2.3.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"trpc-sveltekit": "^3.5.27",
|
"trpc-sveltekit": "^3.5.26",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.3",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.3.0"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|
897
pnpm-lock.yaml
897
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -33,5 +33,5 @@ EXECUTE PROCEDURE update_entry_tsvec ();
|
||||||
ALTER TABLE patients
|
ALTER TABLE patients
|
||||||
ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
|
ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
|
||||||
|
|
||||||
CREATE INDEX entries_tsvec_idx ON entries USING GIN (tsvec);
|
CREATE INDEX entries_tsvec ON entries USING GIN (tsvec);
|
||||||
CREATE INDEX patients_full_name ON patients USING gin (full_name gin_trgm_ops);
|
CREATE INDEX patients_full_name ON patients USING gin (full_name gin_trgm_ops);
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE COLLATION NATURAL_CI (provider = icu, locale = 'en-u-kn-true');
|
|
||||||
|
|
||||||
ALTER TABLE stations ALTER COLUMN NAME TYPE TEXT COLLATE NATURAL_CI;
|
|
||||||
ALTER TABLE rooms ALTER COLUMN NAME TYPE TEXT COLLATE NATURAL_CI;
|
|
||||||
ALTER TABLE categories ALTER COLUMN NAME TYPE TEXT COLLATE NATURAL_CI;
|
|
|
@ -108,7 +108,6 @@ model Entry {
|
||||||
tsvec Unsupported("tsvector")?
|
tsvec Unsupported("tsvector")?
|
||||||
|
|
||||||
@@map("entries")
|
@@map("entries")
|
||||||
@@index([tsvec], type: Gin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model EntryVersion {
|
model EntryVersion {
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
cd "$DIR"
|
|
||||||
|
|
||||||
docker compose up -d
|
docker-compose up -d
|
||||||
echo 'Waiting for database to be ready...'
|
echo 'Waiting for database to be ready...'
|
||||||
"$DIR/wait-for-it.sh" "localhost:5432" -- echo 'Database is ready!'
|
"$DIR/wait-for-it.sh" "localhost:5432" -- echo 'Database is ready!'
|
||||||
|
|
||||||
# Create temporary test database
|
# Create temporary test database
|
||||||
docker compose exec -u 999:999 db sh -e -c 'dropdb -f --if-exists test; createdb test'
|
docker-compose exec -u 999:999 db sh -e -c 'dropdb -f --if-exists test; createdb test'
|
||||||
cd "$DIR/../"
|
cd "$DIR/../"
|
||||||
DATABASE_URL="postgresql://postgres:1234@localhost:5432/test?schema=public" npx prisma migrate reset --force
|
DATABASE_URL="postgresql://postgres:1234@localhost:5432/test?schema=public" npx prisma migrate reset --force
|
||||||
|
|
||||||
|
|
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
|
@ -1,13 +1,9 @@
|
||||||
import type { Session } from "@auth/core/types";
|
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
// interface Locals {}
|
||||||
session: Session;
|
|
||||||
}
|
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
33
src/app.pcss
33
src/app.pcss
|
@ -17,10 +17,6 @@ button {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-1 {
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-form-field > input {
|
.v-form-field > input {
|
||||||
@apply input input-bordered w-full max-w-xs;
|
@apply input input-bordered w-full max-w-xs;
|
||||||
}
|
}
|
||||||
|
@ -29,32 +25,3 @@ button {
|
||||||
.v-form-field > [aria-invalid="true"] {
|
.v-form-field > [aria-invalid="true"] {
|
||||||
@apply input-error;
|
@apply input-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card2 {
|
|
||||||
@apply bg-base-200;
|
|
||||||
@apply rounded-xl;
|
|
||||||
@apply mb-8;
|
|
||||||
@apply flex flex-col;
|
|
||||||
@apply border-solid border-base-content/30 border-[1px];
|
|
||||||
|
|
||||||
.row {
|
|
||||||
@apply flex flex-row p-2;
|
|
||||||
@apply border-solid border-base-content/30 border-t-[1px];
|
|
||||||
}
|
|
||||||
|
|
||||||
.row:first-child {
|
|
||||||
@apply rounded-t-xl border-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row:last-child {
|
|
||||||
@apply rounded-b-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-light {
|
|
||||||
@apply bg-base-content/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-primary {
|
|
||||||
@apply bg-primary text-primary-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,10 +13,9 @@ import { skAuthHandle } from "$lib/server/auth";
|
||||||
* auth mechanism)
|
* auth mechanism)
|
||||||
*/
|
*/
|
||||||
const authorization: Handle = async ({ event, resolve }) => {
|
const authorization: Handle = async ({ event, resolve }) => {
|
||||||
// Allowed pages without login: TRPC API (has its own auth hook), Auth.js internal
|
|
||||||
// pages and the login site
|
|
||||||
if (!/^\/(login|trpc)/.test(event.url.pathname)) {
|
if (!/^\/(login|trpc)/.test(event.url.pathname)) {
|
||||||
if (!event.locals.session) {
|
const session = await event.locals.getSession();
|
||||||
|
if (!session) {
|
||||||
const params = new URLSearchParams({ returnURL: event.url.pathname });
|
const params = new URLSearchParams({ returnURL: event.url.pathname });
|
||||||
redirect(303, "/login?" + params.toString());
|
redirect(303, "/login?" + params.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,10 @@
|
||||||
import { URL_ENTRIES } from "$lib/shared/constants";
|
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||||
import type { Category } from "$lib/shared/model";
|
import type { Category } from "$lib/shared/model";
|
||||||
import { gotoEntityQuery } from "$lib/shared/util";
|
import { gotoEntityQuery } from "$lib/shared/util";
|
||||||
import { getTextColor, colorToHex, hexToColor } from "$lib/shared/util/colors";
|
|
||||||
|
|
||||||
export let category: Category;
|
export let category: Category;
|
||||||
export let baseUrl = URL_ENTRIES;
|
export let baseUrl = URL_ENTRIES;
|
||||||
|
|
||||||
$: textColor = category.color
|
|
||||||
? colorToHex(getTextColor(hexToColor(category.color)))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
function onClick(e: MouseEvent) {
|
function onClick(e: MouseEvent) {
|
||||||
gotoEntityQuery(
|
gotoEntityQuery(
|
||||||
{
|
{
|
||||||
|
@ -27,8 +22,6 @@
|
||||||
<button
|
<button
|
||||||
class="badge ellipsis"
|
class="badge ellipsis"
|
||||||
class:badge-neutral={!category.color}
|
class:badge-neutral={!category.color}
|
||||||
style={category.color
|
style={category.color ? `background-color: ${category.color};` : undefined}
|
||||||
? `color: ${textColor}; background-color: #${category.color};`
|
|
||||||
: undefined}
|
|
||||||
on:click={onClick}>{category.name}</button
|
on:click={onClick}>{category.name}</button
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { mdiChevronLeft } from "@mdi/js";
|
|
||||||
|
|
||||||
import Icon from "./Icon.svelte";
|
|
||||||
|
|
||||||
export let title = "";
|
|
||||||
export let backHref: string | undefined = undefined;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mb-4 flex flex-row">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
{#if backHref}
|
|
||||||
<a href={backHref} class="btn btn-sm btn-circle btn-ghost">
|
|
||||||
<Icon path={mdiChevronLeft} size={1.8} />
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<h1 class="heading">{title}</h1>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
<slot name="rightBtn" />
|
|
||||||
</div>
|
|
|
@ -1,21 +1,12 @@
|
||||||
import {
|
import { SvelteKitAuth, type SvelteKitAuthConfig } from "@auth/sveltekit";
|
||||||
Auth,
|
import { Auth, raw, skipCSRFCheck } from "@auth/core";
|
||||||
isAuthAction,
|
|
||||||
raw,
|
|
||||||
setEnvDefaults,
|
|
||||||
skipCSRFCheck,
|
|
||||||
type AuthConfig,
|
|
||||||
} from "@auth/core";
|
|
||||||
import Keycloak from "@auth/core/providers/keycloak";
|
import Keycloak from "@auth/core/providers/keycloak";
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
import { PrismaAdapter } from "$lib/server/authAdapter";
|
import { PrismaAdapter } from "$lib/server/authAdapter";
|
||||||
import { env } from "$env/dynamic/private";
|
import { env } from "$env/dynamic/private";
|
||||||
import { redirect, type Handle, type RequestEvent } from "@sveltejs/kit";
|
import { redirect, type RequestEvent } from "@sveltejs/kit";
|
||||||
import { parse } from "set-cookie-parser";
|
|
||||||
|
|
||||||
const AUTH_BASE_PATH: string = "/auth";
|
export const AUTH_CFG: SvelteKitAuthConfig = {
|
||||||
|
|
||||||
export const AUTH_CFG: AuthConfig = {
|
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
providers: [
|
providers: [
|
||||||
Keycloak({
|
Keycloak({
|
||||||
|
@ -45,26 +36,10 @@ export const AUTH_CFG: AuthConfig = {
|
||||||
return opt.session;
|
return opt.session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
basePath: AUTH_BASE_PATH,
|
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
Info: the @auth/sveltekit library has currently an issue with building request URLs.
|
|
||||||
If the application is run in production mode, it always creates HTTPS URLs
|
|
||||||
for the internal auth.js requests which fails if the application is not running
|
|
||||||
with HTTPS. This is the reason for not using the library and implementing the
|
|
||||||
auth handler function myself.
|
|
||||||
|
|
||||||
Original source: https://github.com/nextauthjs/next-auth/blob/main/packages/frameworks-sveltekit/src/lib/actions.ts
|
|
||||||
ISC License
|
|
||||||
*/
|
|
||||||
|
|
||||||
function authjsUrl(event: RequestEvent, authjsEndpoint: string): string {
|
|
||||||
const url = event.url;
|
|
||||||
return url.protocol + "//" + url.host + AUTH_BASE_PATH + "/" + authjsEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function makeAuthjsRequest(
|
export async function makeAuthjsRequest(
|
||||||
event: RequestEvent,
|
event: RequestEvent,
|
||||||
authjsEndpoint: string,
|
authjsEndpoint: string,
|
||||||
|
@ -73,8 +48,11 @@ export async function makeAuthjsRequest(
|
||||||
const headers = new Headers(event.request.headers);
|
const headers = new Headers(event.request.headers);
|
||||||
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
const url = event.url;
|
||||||
|
const baseUrl = url.protocol + "//" + url.host;
|
||||||
|
|
||||||
const body = new URLSearchParams(params);
|
const body = new URLSearchParams(params);
|
||||||
const req = new Request(authjsUrl(event, authjsEndpoint), {
|
const req = new Request(baseUrl + authjsEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
|
@ -86,38 +64,4 @@ export async function makeAuthjsRequest(
|
||||||
return redirect(302, res.redirect ?? "");
|
return redirect(302, res.redirect ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function auth(event: RequestEvent) {
|
export const skAuthHandle = SvelteKitAuth(AUTH_CFG).handle;
|
||||||
const { request: req } = event;
|
|
||||||
setEnvDefaults(env, AUTH_CFG);
|
|
||||||
|
|
||||||
const sessionUrl = authjsUrl(event, "session");
|
|
||||||
const request = new Request(sessionUrl, {
|
|
||||||
headers: { cookie: req.headers.get("cookie") ?? "" },
|
|
||||||
});
|
|
||||||
const response = await Auth(request, AUTH_CFG);
|
|
||||||
const authCookies = parse(response.headers.getSetCookie());
|
|
||||||
for (const cookie of authCookies) {
|
|
||||||
const { name, value, ...options } = cookie;
|
|
||||||
// @ts-expect-error - Review: SvelteKit and set-cookie-parser are mismatching
|
|
||||||
event.cookies.set(name, value, { path: "/", ...options });
|
|
||||||
}
|
|
||||||
const { status = 200 } = response;
|
|
||||||
const data = await response.json();
|
|
||||||
if (!data || !Object.keys(data).length) return null;
|
|
||||||
if (status === 200) return data;
|
|
||||||
throw new Error(data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const skAuthHandle: Handle = async ({ event, resolve }) => {
|
|
||||||
const { url, request } = event;
|
|
||||||
|
|
||||||
if (
|
|
||||||
url.pathname.startsWith(AUTH_BASE_PATH + "/") &&
|
|
||||||
isAuthAction(url.pathname.slice(AUTH_BASE_PATH.length + 1).split("/")[0])
|
|
||||||
) {
|
|
||||||
return Auth(request, AUTH_CFG);
|
|
||||||
} else {
|
|
||||||
event.locals.session = await auth(event);
|
|
||||||
}
|
|
||||||
return resolve(event);
|
|
||||||
};
|
|
||||||
|
|
|
@ -6,11 +6,13 @@ import { type inferAsyncReturnType, TRPCError } from "@trpc/server";
|
||||||
// hence the eslint-disable rule
|
// hence the eslint-disable rule
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export async function createContext(event: RequestEvent) {
|
export async function createContext(event: RequestEvent) {
|
||||||
if (!event.locals.session?.user) {
|
const session = await event.locals.getSession();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "not logged in" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "not logged in" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = ZUser.parse(event.locals.session?.user);
|
const user = ZUser.parse(session?.user);
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
newEntryExecution,
|
newEntryExecution,
|
||||||
newEntryVersion,
|
newEntryVersion,
|
||||||
} from "$lib/server/query";
|
} from "$lib/server/query";
|
||||||
import { versionsDiff } from "$lib/shared/util/diff";
|
|
||||||
|
|
||||||
const ZEntityId = fields.EntityId();
|
const ZEntityId = fields.EntityId();
|
||||||
|
|
||||||
|
@ -39,12 +38,6 @@ export const entryRouter = t.router({
|
||||||
versions: t.procedure
|
versions: t.procedure
|
||||||
.input(ZEntityId)
|
.input(ZEntityId)
|
||||||
.query(async (opts) => trpcWrap(async () => getEntryVersions(opts.input))),
|
.query(async (opts) => trpcWrap(async () => getEntryVersions(opts.input))),
|
||||||
versionsDiff: t.procedure.input(ZEntityId).query(async (opts) =>
|
|
||||||
trpcWrap(async () => {
|
|
||||||
const versions = await getEntryVersions(opts.input);
|
|
||||||
return versionsDiff(versions);
|
|
||||||
})
|
|
||||||
),
|
|
||||||
executions: t.procedure
|
executions: t.procedure
|
||||||
.input(ZEntityId)
|
.input(ZEntityId)
|
||||||
.query(async (opts) => trpcWrap(async () => getEntryExecutions(opts.input))),
|
.query(async (opts) => trpcWrap(async () => getEntryExecutions(opts.input))),
|
||||||
|
|
12
src/lib/server/util.ts
Normal file
12
src/lib/server/util.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function userId(event: RequestEvent): Promise<number> {
|
||||||
|
const sess = await event.locals.getSession();
|
||||||
|
const id = Number(sess?.user?.id);
|
||||||
|
if (id) {
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
// This should never happen, since unauthorized requests are caught by hooks.server.ts
|
||||||
|
throw new Error("no user id");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { hexToColor, colorToHex } from "./colors";
|
|
||||||
|
|
||||||
describe.each([
|
|
||||||
{
|
|
||||||
hex: "#ffffff",
|
|
||||||
color: { r: 255, g: 255, b: 255 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#000000",
|
|
||||||
color: { r: 0, g: 0, b: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#ff0000",
|
|
||||||
color: { r: 255, g: 0, b: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#00ff00",
|
|
||||||
color: { r: 0, g: 255, b: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#0000ff",
|
|
||||||
color: { r: 0, g: 0, b: 255 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#8ECAE6",
|
|
||||||
color: { r: 142, g: 202, b: 230 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#219EBC",
|
|
||||||
color: { r: 33, g: 158, b: 188 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#023047",
|
|
||||||
color: { r: 2, g: 48, b: 71 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#FFB703",
|
|
||||||
color: { r: 255, g: 183, b: 3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#FB8500",
|
|
||||||
color: { r: 251, g: 133, b: 0 },
|
|
||||||
},
|
|
||||||
])("color conversion", ({ hex, color }) => {
|
|
||||||
it("colorToHex", () => {
|
|
||||||
expect(colorToHex(color)).toBe(hex.toLowerCase());
|
|
||||||
});
|
|
||||||
it("hexToColor", () => {
|
|
||||||
expect(hexToColor(hex)).toStrictEqual(color);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,72 +0,0 @@
|
||||||
// Source: https://css-tricks.com/nailing-the-perfect-contrast-between-light-text-and-a-background-image/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RGB color value (0-255)
|
|
||||||
*/
|
|
||||||
export type Color = {
|
|
||||||
r: number;
|
|
||||||
g: number;
|
|
||||||
b: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WHITE: Color = { r: 255, g: 255, b: 255 };
|
|
||||||
const BLACK: Color = { r: 0, g: 0, b: 0 };
|
|
||||||
|
|
||||||
export function colorToHex(c: Color): string {
|
|
||||||
return "#" + ((1 << 24) | (c.r << 16) | (c.g << 8) | c.b).toString(16).slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToColor(s: string): Color {
|
|
||||||
const hexStr = s[0] === "#" ? s.substring(1) : s;
|
|
||||||
const c = parseInt(hexStr, 16);
|
|
||||||
return {
|
|
||||||
r: c >> 16,
|
|
||||||
g: (c >> 8) & 255,
|
|
||||||
b: c & 255,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContrast(color1: Color, color2: Color): number {
|
|
||||||
const color1_luminance = getLuminance(color1);
|
|
||||||
const color2_luminance = getLuminance(color2);
|
|
||||||
const lighterColorLuminance = Math.max(color1_luminance, color2_luminance);
|
|
||||||
const darkerColorLuminance = Math.min(color1_luminance, color2_luminance);
|
|
||||||
const contrast = (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05);
|
|
||||||
return contrast;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLuminance(c: Color): number {
|
|
||||||
return (
|
|
||||||
0.2126 * getLinearRGB(c.r) + 0.7152 * getLinearRGB(c.g) + 0.0722 * getLinearRGB(c.b)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convert_8bit_RGB_to_standard_RGB(primaryColor_8bit: number): number {
|
|
||||||
return primaryColor_8bit / 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convert_standard_RGB_to_linear_RGB(primaryColor_sRGB: number): number {
|
|
||||||
const primaryColor_linear =
|
|
||||||
primaryColor_sRGB < 0.03928
|
|
||||||
? primaryColor_sRGB / 12.92
|
|
||||||
: Math.pow((primaryColor_sRGB + 0.055) / 1.055, 2.4);
|
|
||||||
return primaryColor_linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinearRGB(primaryColor_8bit: number): number {
|
|
||||||
// First convert from 8-bit rbg (0-255) to standard RGB (0-1)
|
|
||||||
const primaryColor_sRGB = convert_8bit_RGB_to_standard_RGB(primaryColor_8bit);
|
|
||||||
|
|
||||||
// Then convert from sRGB to linear RGB so we can use it to calculate luminance
|
|
||||||
const primaryColor_RGB_linear = convert_standard_RGB_to_linear_RGB(primaryColor_sRGB);
|
|
||||||
return primaryColor_RGB_linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the preferred text color for the given background color
|
|
||||||
*/
|
|
||||||
export function getTextColor(bgColor: Color): Color {
|
|
||||||
const cWhite = getContrast(bgColor, WHITE);
|
|
||||||
const cBlack = getContrast(bgColor, BLACK);
|
|
||||||
return cWhite > cBlack ? WHITE : BLACK;
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { test, expect } from "vitest";
|
|
||||||
import type { EntryVersion } from "$lib/shared/model";
|
|
||||||
import { CATEGORIES, U1, U2 } from "$tests/helpers/testdata";
|
|
||||||
import { versionsDiff } from "./diff";
|
|
||||||
|
|
||||||
test("versions diff", () => {
|
|
||||||
const versions: EntryVersion[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
author: U1,
|
|
||||||
category: CATEGORIES[0],
|
|
||||||
created_at: new Date(Date.UTC(2024, 1, 10, 10, 30)),
|
|
||||||
date: "2024-01-11",
|
|
||||||
priority: false,
|
|
||||||
text: "10ml Blut abnehmen.\n\nDas Blut muss auf XYZ getestet werden.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
author: U2,
|
|
||||||
category: CATEGORIES[0],
|
|
||||||
created_at: new Date(Date.UTC(2024, 1, 10, 11, 30)),
|
|
||||||
date: "2024-01-12",
|
|
||||||
priority: true,
|
|
||||||
text: "10ml Blut abnehmen.\n\nDas Blut muss auf XYZ-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
author: U1,
|
|
||||||
category: CATEGORIES[0],
|
|
||||||
created_at: new Date(Date.UTC(2024, 1, 10, 11, 31)),
|
|
||||||
date: "2024-01-12",
|
|
||||||
priority: true,
|
|
||||||
text: "10ml Blut abnehmen.\n\nDas Blut muss auf Lambda-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const diff = versionsDiff(versions);
|
|
||||||
expect(diff).toStrictEqual([
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
author: {
|
|
||||||
id: 1,
|
|
||||||
name: "Sven Schulz",
|
|
||||||
email: "sven.schulz@example.com",
|
|
||||||
},
|
|
||||||
category: {
|
|
||||||
id: 1,
|
|
||||||
name: "Laborabnahme",
|
|
||||||
description: "Blutabnahme zur Untersuchung im Labor",
|
|
||||||
color: "FF0000",
|
|
||||||
},
|
|
||||||
created_at: new Date("2024-02-10T11:31:00.000Z"),
|
|
||||||
date: "2024-01-12",
|
|
||||||
priority: true,
|
|
||||||
text: [
|
|
||||||
{
|
|
||||||
count: 38,
|
|
||||||
added: true,
|
|
||||||
removed: undefined,
|
|
||||||
value:
|
|
||||||
"10ml Blut abnehmen.\n\nDas Blut muss auf Lambda-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
author: {
|
|
||||||
id: 2,
|
|
||||||
name: "Sabrina Loewe",
|
|
||||||
email: "sabrina.loewe@example.com",
|
|
||||||
},
|
|
||||||
created_at: new Date("2024-02-10T11:30:00.000Z"),
|
|
||||||
category: undefined,
|
|
||||||
date: undefined,
|
|
||||||
priority: undefined,
|
|
||||||
text: [
|
|
||||||
{ count: 16, value: "10ml Blut abnehmen.\n\nDas Blut muss auf " },
|
|
||||||
{ count: 1, added: undefined, removed: true, value: "Lambda" },
|
|
||||||
{ count: 1, added: true, removed: undefined, value: "XYZ" },
|
|
||||||
{
|
|
||||||
count: 21,
|
|
||||||
value: "-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
author: {
|
|
||||||
id: 1,
|
|
||||||
name: "Sven Schulz",
|
|
||||||
email: "sven.schulz@example.com",
|
|
||||||
},
|
|
||||||
created_at: new Date("2024-02-10T10:30:00.000Z"),
|
|
||||||
category: undefined,
|
|
||||||
text: [
|
|
||||||
{ count: 17, value: "10ml Blut abnehmen.\n\nDas Blut muss auf XYZ" },
|
|
||||||
{ count: 2, added: undefined, removed: true, value: "-Erreger" },
|
|
||||||
{ count: 5, value: " getestet werden." },
|
|
||||||
{
|
|
||||||
count: 14,
|
|
||||||
added: undefined,
|
|
||||||
removed: true,
|
|
||||||
value: "\n\nHierfür ist das Labor Meier zuständig.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
date: "2024-01-11",
|
|
||||||
priority: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { diffWords } from "diff";
|
|
||||||
import type { EntryVersion, Category, Option, UserTag } from "$lib/shared/model";
|
|
||||||
|
|
||||||
export type EntryVersionChange = {
|
|
||||||
id: number;
|
|
||||||
author: UserTag;
|
|
||||||
created_at: Date;
|
|
||||||
|
|
||||||
text: Diff.Change[];
|
|
||||||
date?: string;
|
|
||||||
category?: Option<Category>;
|
|
||||||
priority?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function newOrUndef<T>(o: T, n: T): T | undefined {
|
|
||||||
return o === n ? undefined : n;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function versionsDiff(versions: EntryVersion[]): EntryVersionChange[] {
|
|
||||||
let prev = versions[versions.length - 1];
|
|
||||||
const changes: EntryVersionChange[] = [
|
|
||||||
{
|
|
||||||
...prev,
|
|
||||||
text: diffWords("", prev.text),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = versions.length - 2; i >= 0; i--) {
|
|
||||||
const v = versions[i];
|
|
||||||
const text = diffWords(prev.text, v.text);
|
|
||||||
|
|
||||||
changes.push({
|
|
||||||
id: v.id,
|
|
||||||
author: v.author,
|
|
||||||
created_at: v.created_at,
|
|
||||||
text,
|
|
||||||
date: newOrUndef(prev.date, v.date),
|
|
||||||
category: prev.category?.id === v.category?.id ? undefined : v.category,
|
|
||||||
priority: newOrUndef(prev.priority, v.priority),
|
|
||||||
});
|
|
||||||
|
|
||||||
prev = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes;
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ import { goto } from "$app/navigation";
|
||||||
import type { EntityQuery } from "$lib/shared/model";
|
import type { EntityQuery } from "$lib/shared/model";
|
||||||
import { TRPCClientError } from "@trpc/client";
|
import { TRPCClientError } from "@trpc/client";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { ZodError } from "zod";
|
|
||||||
|
|
||||||
export function formatDate(date: Date | string, time = false): string {
|
export function formatDate(date: Date | string, time = false): string {
|
||||||
let dt = date;
|
let dt = date;
|
||||||
|
@ -26,10 +25,6 @@ export function formatDate(date: Date | string, time = false): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBool(val: boolean): string {
|
|
||||||
return val ? "Ja" : "Nein";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQueryUrl(q: EntityQuery, basePath: string): string {
|
export function getQueryUrl(q: EntityQuery, basePath: string): string {
|
||||||
if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath;
|
if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath;
|
||||||
return basePath + "/" + JSON.stringify(q);
|
return basePath + "/" + JSON.stringify(q);
|
||||||
|
@ -61,8 +56,6 @@ export async function loadWrap<T>(f: () => Promise<T>) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof TRPCClientError) {
|
if (e instanceof TRPCClientError) {
|
||||||
error(e.data?.httpStatus ?? 500, e.message);
|
error(e.data?.httpStatus ?? 500, e.message);
|
||||||
} else if (e instanceof ZodError) {
|
|
||||||
error(400, e.message);
|
|
||||||
} else if (e instanceof Error) {
|
} else if (e instanceof Error) {
|
||||||
error(500, e.message);
|
error(500, e.message);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import NavLink from "$lib/components/ui/NavLink.svelte";
|
import NavLink from "$lib/components/ui/NavLink.svelte";
|
||||||
|
import { signOut } from "@auth/sveltekit/client";
|
||||||
import { mdiAccount, mdiHome } from "@mdi/js";
|
import { mdiAccount, mdiHome } from "@mdi/js";
|
||||||
|
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
import RoomField from "$lib/components/table/RoomField.svelte";
|
import RoomField from "$lib/components/table/RoomField.svelte";
|
||||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||||
import Markdown from "$lib/components/ui/Markdown.svelte";
|
import Markdown from "$lib/components/ui/Markdown.svelte";
|
||||||
import Header from "$lib/components/ui/Header.svelte";
|
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
|
||||||
import { mdiHistory } from "@mdi/js";
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,57 +13,50 @@
|
||||||
<title>Eintrag #{data.entry.id}</title>
|
<title>Eintrag #{data.entry.id}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Header title="Eintrag #{data.entry.id}">
|
<h1 class="heading">
|
||||||
|
Eintrag #{data.entry.id}
|
||||||
{#if data.entry.current_version.category}
|
{#if data.entry.current_version.category}
|
||||||
<CategoryField category={data.entry.current_version.category} />
|
<CategoryField category={data.entry.current_version.category} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if data.entry.current_version.priority}
|
</h1>
|
||||||
<div class="badge ellipsis badge-warning">Priorität</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<a
|
<div class="box">
|
||||||
href="/entry/{data.entry.id}/versions"
|
<p>
|
||||||
class="btn btn-sm btn-primary ml-auto"
|
|
||||||
slot="rightBtn"
|
|
||||||
>
|
|
||||||
<Icon path={mdiHistory} />
|
|
||||||
<span class="hidden sm:inline">Versionen</span>
|
|
||||||
</a>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<div class="card2">
|
|
||||||
<div class="row c-light text-sm">Patient</div>
|
|
||||||
<div class="row items-center gap-2">
|
|
||||||
{#if data.entry.patient.room}
|
{#if data.entry.patient.room}
|
||||||
<RoomField room={data.entry.patient.room} />
|
<RoomField room={data.entry.patient.room} />
|
||||||
{/if}
|
{/if}
|
||||||
{data.entry.patient.first_name}
|
{data.entry.patient.first_name}
|
||||||
{data.entry.patient.last_name}
|
{data.entry.patient.last_name}
|
||||||
({data.entry.patient.age})
|
({data.entry.patient.age})
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card2">
|
<div class="box">
|
||||||
<div class="row c-light text-sm">Beschreibung</div>
|
|
||||||
<div class="row">
|
|
||||||
<p class="prose">
|
<p class="prose">
|
||||||
<Markdown src={data.entry.current_version.text} />
|
<Markdown src={data.entry.current_version.text} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if data.entry.execution}
|
{#if data.entry.execution}
|
||||||
<div class="card2">
|
<div class="box">
|
||||||
<div class="row c-light text-sm">
|
|
||||||
<p>
|
<p>
|
||||||
Erledigt am {formatDate(data.entry.execution.created_at, true)} von
|
Erledigt am {formatDate(data.entry.execution.created_at, true)} von
|
||||||
<UserField user={data.entry.execution.author} filterName="executor" />:
|
<UserField user={data.entry.execution.author} filterName="executor" />:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<p class="prose">
|
<p class="prose">
|
||||||
<Markdown src={data.entry.execution?.text} />
|
<Markdown src={data.entry.execution?.text} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
p {
|
||||||
|
@apply my-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
@apply rounded-xl border-solid border-2 border-base-content/40;
|
||||||
|
@apply px-2 my-4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -4,10 +4,8 @@ import { loadWrap } from "$lib/shared/util";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
const entry = await loadWrap(async () => {
|
|
||||||
const id = ZUrlEntityId.parse(event.params.id);
|
const id = ZUrlEntityId.parse(event.params.id);
|
||||||
return trpc(event).entry.get.query(id);
|
const entry = await loadWrap(async () => trpc(event).entry.get.query(id));
|
||||||
});
|
|
||||||
|
|
||||||
return { entry };
|
return { entry };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
|
||||||
import UserField from "$lib/components/table/UserField.svelte";
|
|
||||||
import { formatBool, formatDate } from "$lib/shared/util";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
import Header from "$lib/components/ui/Header.svelte";
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
$: entryId = $page.params.id;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Eintrag #{entryId} - Versionen</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<Header title="Eintrag #{entryId} - Versionen" backHref="/entry/{entryId}" />
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
{#each data.versions as version}
|
|
||||||
<div class="card2">
|
|
||||||
<div class="row c-light text-sm">
|
|
||||||
<UserField user={version.author} />, {formatDate(version.created_at, true)}
|
|
||||||
</div>
|
|
||||||
{#if version.category}
|
|
||||||
<div class="row">
|
|
||||||
<div>Kategeorie</div>
|
|
||||||
<div><CategoryField category={version.category} /></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.text.length > 0}
|
|
||||||
<div class="row">
|
|
||||||
<div>Text</div>
|
|
||||||
<div class="whitespace-pre-wrap">
|
|
||||||
{#each version.text as change}
|
|
||||||
<span class:added={change.added} class:removed={change.removed}>
|
|
||||||
{change.value}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.date !== undefined}
|
|
||||||
<div class="row">
|
|
||||||
<div>Datum</div>
|
|
||||||
<div>{formatDate(version.date)}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version.priority !== undefined}
|
|
||||||
<div class="row">
|
|
||||||
<div>Priorität</div>
|
|
||||||
<div>{formatBool(version.priority)}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.row > div:first-child {
|
|
||||||
min-width: 6.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.added {
|
|
||||||
@apply bg-success/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.removed {
|
|
||||||
@apply bg-error/20;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
|
||||||
import { trpc } from "$lib/shared/trpc";
|
|
||||||
import { loadWrap } from "$lib/shared/util";
|
|
||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageLoad = async (event) => {
|
|
||||||
const id = ZUrlEntityId.parse(event.params.id);
|
|
||||||
const versions = await loadWrap(async () => trpc(event).entry.versionsDiff.query(id));
|
|
||||||
|
|
||||||
return { versions };
|
|
||||||
};
|
|
|
@ -6,6 +6,6 @@ export const actions: Actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const callbackUrl = baseUrl(event.url) + "/login?noAuto=1";
|
const callbackUrl = baseUrl(event.url) + "/login?noAuto=1";
|
||||||
|
|
||||||
return makeAuthjsRequest(event, "signout", { callbackUrl });
|
return makeAuthjsRequest(event, "/auth/signout", { callbackUrl });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,6 @@ import type { LayoutServerLoad } from "./$types";
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async (event) => {
|
export const load: LayoutServerLoad = async (event) => {
|
||||||
return {
|
return {
|
||||||
session: event.locals.session,
|
session: await event.locals.getSession(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,40 +1,11 @@
|
||||||
import type { Actions, RequestEvent } from "./$types";
|
import type { Actions } from "./$types";
|
||||||
import { makeAuthjsRequest } from "$lib/server/auth";
|
import { makeAuthjsRequest } from "$lib/server/auth";
|
||||||
import { baseUrl } from "$lib/shared/util";
|
import { baseUrl } from "$lib/shared/util";
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This cookie stores the timestamp of the last automatic login.
|
|
||||||
* It is used to prevent infinite redirect loops in case the authentication fails.
|
|
||||||
*/
|
|
||||||
const COOKIE_NAME = "autoLoginTs";
|
|
||||||
|
|
||||||
async function doLogin(event: RequestEvent) {
|
|
||||||
const callbackUrl = event.url.searchParams.get("returnURL") ?? baseUrl(event.url);
|
|
||||||
|
|
||||||
return makeAuthjsRequest(event, "signin/keycloak", { callbackUrl });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to login the user using OIDC
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
|
||||||
// the noAuto URL parameter disables auto-login
|
|
||||||
if (event.url.searchParams.get("noAuto")) return;
|
|
||||||
|
|
||||||
let autoLoginTs = null;
|
|
||||||
const autoLoginTsStr = event.cookies.get(COOKIE_NAME);
|
|
||||||
if (autoLoginTsStr) {
|
|
||||||
autoLoginTs = parseInt(autoLoginTsStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (!autoLoginTs || autoLoginTs + 15000 < now) {
|
|
||||||
event.cookies.set(COOKIE_NAME, now.toString(), { path: "/login" });
|
|
||||||
return doLogin(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
return doLogin(event);
|
const callbackUrl = event.url.searchParams.get("returnURL") ?? baseUrl(event.url);
|
||||||
|
|
||||||
|
return makeAuthjsRequest(event, "/auth/signin/keycloak", { callbackUrl });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,37 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { signIn } from "@auth/sveltekit/client";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "autoLoginTs";
|
||||||
|
|
||||||
|
let callbackUrl: string;
|
||||||
|
$: if ($page.url) {
|
||||||
|
let u = new URL($page.url.protocol + "//" + $page.url.host);
|
||||||
|
u.pathname = $page.url.searchParams.get("returnURL") ?? "/";
|
||||||
|
callbackUrl = u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client side auto-login / redirect
|
||||||
|
onMount(() => {
|
||||||
|
if (!$page.url.searchParams.get("noAuto")) {
|
||||||
|
// Keep track of last login time to avoid redirect loop if the login fails
|
||||||
|
const lastUpdateStr = sessionStorage.getItem(STORAGE_KEY);
|
||||||
|
const lastUpdate = lastUpdateStr ? parseInt(lastUpdateStr) : null;
|
||||||
|
const now = Date.now();
|
||||||
|
if (!lastUpdate || lastUpdate + 10000 < now) {
|
||||||
|
sessionStorage.setItem(STORAGE_KEY, now.toString());
|
||||||
|
|
||||||
|
if (!$page.data.session?.user) {
|
||||||
|
signIn("keycloak", { callbackUrl });
|
||||||
|
} else {
|
||||||
|
goto(callbackUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-[100vw] px-6 pb-16 xl:pr-2 text-center">
|
<div class="max-w-[100vw] px-6 pb-16 xl:pr-2 text-center">
|
||||||
|
|
Loading…
Reference in a new issue