diff --git a/package-lock.json b/package-lock.json index 057e9239..ef4d23e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "eslint-plugin-svelte": "^3.0.0", "globals": "^16.0.0", "hono": "^4.10.1", + "jose": "^6.1.3", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", @@ -6192,9 +6193,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -8206,6 +8207,16 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", diff --git a/package.json b/package.json index 23482113..26c0fedf 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "eslint-plugin-svelte": "^3.0.0", "globals": "^16.0.0", "hono": "^4.10.1", + "jose": "^6.1.3", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", @@ -56,5 +57,9 @@ "@prisma/client": "^6.15.0", "prisma": "^6.15.0", "s3-sync-client": "^4.3.1" + }, + "//cookie": "sveltekit depends on v0.6, which is insecure", + "overrides": { + "cookie": "^0.7.0" } } diff --git a/src/app.d.ts b/src/app.d.ts index 6fb018ae..e2b53922 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} interface Locals { clientId: number; + userEmail?: string; } // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index d20d3055..e7ef09cd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,28 +1,25 @@ import { type Handle, error } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { building } from '$app/environment'; +import { tryVerifyAPIToken, tryVerifyCookie } from '$lib/server/auth'; import { QueueConnected, getQueues } from '$lib/server/bullmq'; import { bullboardHandle } from '$lib/server/bullmq/BullBoard'; import { allWorkers } from '$lib/server/bullmq/BullMQ'; -import { DatabaseConnected, prisma } from '$lib/server/prisma'; -import { ErrorResponse } from '$lib/utils'; +import { DatabaseConnected } from '$lib/server/prisma'; const handleAPIRoute: Handle = async ({ event, resolve }) => { - if (event.route.id?.split('/')[1] === '(api)') { - if (event.request.headers.get('Content-Type') !== 'application/json') { - return ErrorResponse(400, 'Missing Header Content-Type: application/json'); - } - const access_token = event.request.headers.get('Authorization')?.replace('Bearer ', ''); - if (!access_token) { - return ErrorResponse(401, 'Missing Header Authorization: Bearer '); - } - const client = await prisma.client.findFirst({ where: { access_token } }); - if (!client) { - return ErrorResponse(403, 'Invalid Access Token'); - } - event.locals.clientId = client.id; - } else { - event.locals.clientId = 0; + const [success, res] = await tryVerifyAPIToken(event); + if (!success) { + return res; + } + event.locals.clientId = res.id; + return resolve(event); +}; + +const handleAuthRoute: Handle = async ({ event, resolve }) => { + event.locals.clientId = 0; + if (event.route.id?.split('/')?.[1] !== '(auth)') { + await tryVerifyCookie(event); } return resolve(event); }; @@ -62,5 +59,11 @@ export const handle: Handle = async ({ event, resolve }) => { return new Response('', { status: 404 }); } - return await sequence(heartbeat, handleAPIRoute, bullboardHandle)({ event, resolve }); + return await sequence( + heartbeat, + (h) => { + return event.route.id?.split('/')?.[1] === '(api)' ? handleAPIRoute(h) : handleAuthRoute(h); + }, + bullboardHandle + )({ event, resolve }); }; diff --git a/src/lib/icons/ScriptoriaIcon.svelte b/src/lib/icons/ScriptoriaIcon.svelte new file mode 100644 index 00000000..61a31370 --- /dev/null +++ b/src/lib/icons/ScriptoriaIcon.svelte @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 00000000..0496876a --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,92 @@ +import type { Prisma } from '@prisma/client'; +import { type RequestEvent, redirect } from '@sveltejs/kit'; +import { jwtDecrypt } from 'jose'; +import { createHash, randomUUID } from 'node:crypto'; +import { getAuthConnection } from './bullmq/queues'; +import { prisma } from './prisma'; +import { env as secrets } from '$env/dynamic/private'; +import { env } from '$env/dynamic/public'; +import { ErrorResponse } from '$lib/utils'; + +export async function tryVerifyCookie(event: RequestEvent, gotoLoginPage = true) { + const cookie = event.cookies.get('scriptoria.session-token'); + + let token = null; + try { + if (cookie) { + token = await jwtDecrypt(cookie, new TextEncoder().encode(secrets.AUTH0_SECRET)); + + event.locals.userEmail = token.payload.email as string; + } + } catch { + /* empty */ + } + + if (!cookie || !token) { + if (gotoLoginPage) { + const returnTo = event.url.pathname + event.url.search; + throw redirect(302, `/login?returnTo=${encodeURIComponent(returnTo)}`); + } else { + throw await initiateScriptoriaLogin(event); + } + } +} + +async function initiateScriptoriaLogin(event: RequestEvent) { + const verify = randomUUID(); + const requestId = randomUUID(); + + await getAuthConnection().set(`${requestId}`, verify, 'EX', 300); // 5 minute (300 s) TTL + + const hash = createHash('sha256'); + hash.update(verify); + const challenge = hash.digest('base64url').replace(/=+$/, ''); + + const returnTo = event.url.searchParams.get('returnTo'); + + throw redirect( + 302, + `${env.PUBLIC_SCRIPTORIA_URL}/api/auth/token?` + + `challenge=${challenge}&` + + `redirect_uri=${encodeURIComponent( + `${secrets.ORIGIN}/exchange?` + + (returnTo ? `returnTo=${returnTo}` : '') + + `&requestId=${requestId}` + )}&` + + `scope=admin` + ); +} + +export function returnTo(event: RequestEvent) { + let redirectUrl = decodeURIComponent(event.url.searchParams.get('returnTo') ?? ''); + while (redirectUrl?.startsWith('/login')) { + redirectUrl = decodeURIComponent(new URL(redirectUrl).searchParams.get('returnTo') ?? ''); + } + throw redirect( + 302, + redirectUrl && redirectUrl.startsWith('/') && !redirectUrl.startsWith('//') ? redirectUrl : '/' + ); +} + +export function invalidateLogin(event: RequestEvent) { + event.cookies.set('scriptoria.session-token', '', { path: '/' }); + throw redirect(302, '/login'); +} + +export async function tryVerifyAPIToken( + event: RequestEvent +): Promise<[true, Prisma.clientGetPayload] | [false, Response]> { + if (event.request.headers.get('Content-Type') !== 'application/json') { + return [false, ErrorResponse(400, 'Missing Header Content-Type: application/json')]; + } + const access_token = event.request.headers.get('Authorization')?.replace('Bearer ', ''); + if (!access_token) { + return [false, ErrorResponse(401, 'Missing Header Authorization: Bearer ')]; + } + const client = await prisma.client.findFirst({ where: { access_token } }); + if (!client) { + return [false, ErrorResponse(403, 'Invalid Access Token')]; + } + + return [true, client]; +} diff --git a/src/lib/server/bullmq/queues.ts b/src/lib/server/bullmq/queues.ts index 1ccfaac1..b38c3b8b 100644 --- a/src/lib/server/bullmq/queues.ts +++ b/src/lib/server/bullmq/queues.ts @@ -2,14 +2,16 @@ import { Queue } from 'bullmq'; import { Redis } from 'ioredis'; import type { BuildJob, PollJob, ProjectJob, PublishJob, S3Job, SystemJob } from './types'; import { QueueName } from './types'; +import { env } from '$env/dynamic/private'; class Connection { private conn: Redis; private connected: boolean; - constructor(isQueueConnection = false) { + constructor(isQueueConnection = false, keyPrefix?: string) { this.conn = new Redis({ host: process.env.NODE_ENV === 'development' ? 'localhost' : process.env.VALKEY_HOST, - maxRetriesPerRequest: isQueueConnection ? undefined : null + maxRetriesPerRequest: isQueueConnection ? undefined : null, + keyPrefix }); this.connected = false; this.conn.on('close', () => { @@ -55,14 +57,20 @@ class Connection { let _workerConnection: Connection | undefined = undefined; let _queueConnection: Connection | undefined = undefined; +let _authConnection: Connection | undefined = undefined; export const QueueConnected = () => _queueConnection?.IsConnected() ?? false; +export const getAuthConnection = () => { + if (!_authConnection) _authConnection = new Connection(false, env.APP_ENV + '_be_auth'); + return _authConnection.connection(); +}; + export const getWorkerConfig = () => { if (!_workerConnection) _workerConnection = new Connection(false); return { connection: _workerConnection!.connection(), - prefix: 'build-engine' + prefix: env.APP_ENV + '_build-engine' } as const; }; @@ -70,7 +78,7 @@ export const getQueueConfig = () => { if (!_queueConnection) _queues = createQueues(); return { connection: _queueConnection!.connection(), - prefix: 'build-engine' + prefix: env.APP_ENV + '_build-engine' } as const; }; let _queues: ReturnType | undefined = undefined; diff --git a/src/routes/(api)/system/check/+server.ts b/src/routes/(api)/system/check/+server.ts index 51c4128d..33637f81 100644 --- a/src/routes/(api)/system/check/+server.ts +++ b/src/routes/(api)/system/check/+server.ts @@ -2,5 +2,5 @@ import type { RequestHandler } from './$types'; // GET system/check export const GET: RequestHandler = () => { - return new Response(JSON.stringify({})); + return new Response(JSON.stringify({ versions: {}, imageHash: '' })); }; diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 00000000..9c52bde4 --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,16 @@ + + +
+ {@render children?.()} +
diff --git a/src/routes/(auth)/exchange/+server.ts b/src/routes/(auth)/exchange/+server.ts new file mode 100644 index 00000000..1ef04f6e --- /dev/null +++ b/src/routes/(auth)/exchange/+server.ts @@ -0,0 +1,60 @@ +import { error } from 'console'; +import { EncryptJWT, jwtVerify } from 'jose'; +import type { RequestHandler } from './$types'; +import { env as secrets } from '$env/dynamic/private'; +import { env } from '$env/dynamic/public'; +import { returnTo } from '$lib/server/auth'; +import { QueueConnected } from '$lib/server/bullmq'; +import { getAuthConnection } from '$lib/server/bullmq/queues'; + +// GET system/check +export const GET: RequestHandler = async (event) => { + if (QueueConnected()) { + const requestId = event.url.searchParams.get('requestId'); + const code = event.url.searchParams.get('code'); + if (!requestId || !code) { + throw error(400, 'Missing URL Search Params'); + } + + const verify = await getAuthConnection().get(requestId); + if (!verify) { + throw error(400, 'Invalid or expired code'); + } + + try { + //immediately invalidate + await getAuthConnection().del(requestId); + } catch { + /* empty */ + } + + const res: { id_token?: string } = await fetch( + `${env.PUBLIC_SCRIPTORIA_URL}/api/auth/exchange`, + { + method: 'POST', + body: JSON.stringify({ + code, + verify + }) + } + ).then((r) => r.json()); + + if (!res.id_token) { + throw error(401, 'Authentication failed'); + } + + const key = new TextEncoder().encode(secrets.AUTH0_SECRET); + + const token = await jwtVerify(res.id_token, key); + + const encryptedToken = await new EncryptJWT(token.payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256CBC-HS512' }) + .encrypt(key); + + event.cookies.set('scriptoria.session-token', encryptedToken, { path: '/' }); + + throw returnTo(event); + } else { + throw error(503, 'Service Unavailable'); + } +}; diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts new file mode 100644 index 00000000..1ea6f99b --- /dev/null +++ b/src/routes/(auth)/login/+page.server.ts @@ -0,0 +1,15 @@ +import type { Actions, PageServerLoad } from './$types'; +import { returnTo, tryVerifyCookie } from '$lib/server/auth'; +import { QueueConnected } from '$lib/server/bullmq'; + +export const load: PageServerLoad = async (event) => { + return { + serviceAvailable: QueueConnected() + }; +}; +export const actions: Actions = { + async login(event) { + await tryVerifyCookie(event, false); + throw returnTo(event); + } +}; diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte new file mode 100644 index 00000000..4480c664 --- /dev/null +++ b/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,65 @@ + + +
+
+
+ +
+

Welcome to BuildEngine

+
+

+ BuildEngine serves as an interface between Scriptoria and the AWS resources it uses. +

+ {#if data.serviceAvailable} +
+ +
+ {:else} +

+ BuildEngine is currently unavailable +

+ {/if} +
+
+ +
+ + Like to use our service? + + + Visit Scriptoria + +
diff --git a/src/routes/(auth)/signout/+page.server.ts b/src/routes/(auth)/signout/+page.server.ts new file mode 100644 index 00000000..cc21277e --- /dev/null +++ b/src/routes/(auth)/signout/+page.server.ts @@ -0,0 +1,6 @@ +import type { PageServerLoad } from './$types'; +import { invalidateLogin } from '$lib/server/auth'; + +export const load: PageServerLoad = async (event) => { + throw invalidateLogin(event); +}; diff --git a/src/routes/(ui)/+layout.server.ts b/src/routes/(ui)/+layout.server.ts new file mode 100644 index 00000000..e5cf9daa --- /dev/null +++ b/src/routes/(ui)/+layout.server.ts @@ -0,0 +1,5 @@ +import type { LayoutServerLoad } from './$types'; + +export const load = (async ({ locals }) => { + return { userEmail: locals.userEmail }; +}) satisfies LayoutServerLoad; diff --git a/src/routes/(ui)/+layout.svelte b/src/routes/(ui)/+layout.svelte new file mode 100644 index 00000000..7a9e5be5 --- /dev/null +++ b/src/routes/(ui)/+layout.svelte @@ -0,0 +1,81 @@ + + +
+ +
+
+
+ {@render children?.()} +
+
+
+
© SIL Global {new Date().getFullYear()}
+
+ Powered by  + SvelteKit +
+
+ + diff --git a/src/routes/+page.svelte b/src/routes/(ui)/+page.svelte similarity index 100% rename from src/routes/+page.svelte rename to src/routes/(ui)/+page.svelte diff --git a/src/routes/about/+page.svelte b/src/routes/(ui)/about/+page.svelte similarity index 100% rename from src/routes/about/+page.svelte rename to src/routes/(ui)/about/+page.svelte diff --git a/src/routes/build-admin/+page.server.ts b/src/routes/(ui)/build-admin/+page.server.ts similarity index 100% rename from src/routes/build-admin/+page.server.ts rename to src/routes/(ui)/build-admin/+page.server.ts diff --git a/src/routes/build-admin/+page.svelte b/src/routes/(ui)/build-admin/+page.svelte similarity index 100% rename from src/routes/build-admin/+page.svelte rename to src/routes/(ui)/build-admin/+page.svelte diff --git a/src/routes/build-admin/update/+page.server.ts b/src/routes/(ui)/build-admin/update/+page.server.ts similarity index 100% rename from src/routes/build-admin/update/+page.server.ts rename to src/routes/(ui)/build-admin/update/+page.server.ts diff --git a/src/routes/build-admin/update/+page.svelte b/src/routes/(ui)/build-admin/update/+page.svelte similarity index 100% rename from src/routes/build-admin/update/+page.svelte rename to src/routes/(ui)/build-admin/update/+page.svelte diff --git a/src/routes/build-admin/view/+page.server.ts b/src/routes/(ui)/build-admin/view/+page.server.ts similarity index 100% rename from src/routes/build-admin/view/+page.server.ts rename to src/routes/(ui)/build-admin/view/+page.server.ts diff --git a/src/routes/build-admin/view/+page.svelte b/src/routes/(ui)/build-admin/view/+page.svelte similarity index 100% rename from src/routes/build-admin/view/+page.svelte rename to src/routes/(ui)/build-admin/view/+page.svelte diff --git a/src/routes/client-admin/+page.server.ts b/src/routes/(ui)/client-admin/+page.server.ts similarity index 100% rename from src/routes/client-admin/+page.server.ts rename to src/routes/(ui)/client-admin/+page.server.ts diff --git a/src/routes/client-admin/+page.svelte b/src/routes/(ui)/client-admin/+page.svelte similarity index 100% rename from src/routes/client-admin/+page.svelte rename to src/routes/(ui)/client-admin/+page.svelte diff --git a/src/routes/client-admin/create/+page.server.ts b/src/routes/(ui)/client-admin/create/+page.server.ts similarity index 100% rename from src/routes/client-admin/create/+page.server.ts rename to src/routes/(ui)/client-admin/create/+page.server.ts diff --git a/src/routes/client-admin/create/+page.svelte b/src/routes/(ui)/client-admin/create/+page.svelte similarity index 100% rename from src/routes/client-admin/create/+page.svelte rename to src/routes/(ui)/client-admin/create/+page.svelte diff --git a/src/routes/client-admin/update/+page.server.ts b/src/routes/(ui)/client-admin/update/+page.server.ts similarity index 100% rename from src/routes/client-admin/update/+page.server.ts rename to src/routes/(ui)/client-admin/update/+page.server.ts diff --git a/src/routes/client-admin/update/+page.svelte b/src/routes/(ui)/client-admin/update/+page.svelte similarity index 100% rename from src/routes/client-admin/update/+page.svelte rename to src/routes/(ui)/client-admin/update/+page.svelte diff --git a/src/routes/client-admin/valibot.ts b/src/routes/(ui)/client-admin/valibot.ts similarity index 100% rename from src/routes/client-admin/valibot.ts rename to src/routes/(ui)/client-admin/valibot.ts diff --git a/src/routes/client-admin/view/+page.server.ts b/src/routes/(ui)/client-admin/view/+page.server.ts similarity index 100% rename from src/routes/client-admin/view/+page.server.ts rename to src/routes/(ui)/client-admin/view/+page.server.ts diff --git a/src/routes/client-admin/view/+page.svelte b/src/routes/(ui)/client-admin/view/+page.svelte similarity index 100% rename from src/routes/client-admin/view/+page.svelte rename to src/routes/(ui)/client-admin/view/+page.svelte diff --git a/src/routes/job-admin/+page.server.ts b/src/routes/(ui)/job-admin/+page.server.ts similarity index 100% rename from src/routes/job-admin/+page.server.ts rename to src/routes/(ui)/job-admin/+page.server.ts diff --git a/src/routes/job-admin/+page.svelte b/src/routes/(ui)/job-admin/+page.svelte similarity index 100% rename from src/routes/job-admin/+page.svelte rename to src/routes/(ui)/job-admin/+page.svelte diff --git a/src/routes/job-admin/update/+page.server.ts b/src/routes/(ui)/job-admin/update/+page.server.ts similarity index 100% rename from src/routes/job-admin/update/+page.server.ts rename to src/routes/(ui)/job-admin/update/+page.server.ts diff --git a/src/routes/job-admin/update/+page.svelte b/src/routes/(ui)/job-admin/update/+page.svelte similarity index 100% rename from src/routes/job-admin/update/+page.svelte rename to src/routes/(ui)/job-admin/update/+page.svelte diff --git a/src/routes/job-admin/view/+page.server.ts b/src/routes/(ui)/job-admin/view/+page.server.ts similarity index 100% rename from src/routes/job-admin/view/+page.server.ts rename to src/routes/(ui)/job-admin/view/+page.server.ts diff --git a/src/routes/job-admin/view/+page.svelte b/src/routes/(ui)/job-admin/view/+page.svelte similarity index 100% rename from src/routes/job-admin/view/+page.svelte rename to src/routes/(ui)/job-admin/view/+page.svelte diff --git a/src/routes/project-admin/+page.server.ts b/src/routes/(ui)/project-admin/+page.server.ts similarity index 100% rename from src/routes/project-admin/+page.server.ts rename to src/routes/(ui)/project-admin/+page.server.ts diff --git a/src/routes/project-admin/+page.svelte b/src/routes/(ui)/project-admin/+page.svelte similarity index 100% rename from src/routes/project-admin/+page.svelte rename to src/routes/(ui)/project-admin/+page.svelte diff --git a/src/routes/project-admin/update/+page.server.ts b/src/routes/(ui)/project-admin/update/+page.server.ts similarity index 100% rename from src/routes/project-admin/update/+page.server.ts rename to src/routes/(ui)/project-admin/update/+page.server.ts diff --git a/src/routes/project-admin/update/+page.svelte b/src/routes/(ui)/project-admin/update/+page.svelte similarity index 100% rename from src/routes/project-admin/update/+page.svelte rename to src/routes/(ui)/project-admin/update/+page.svelte diff --git a/src/routes/project-admin/view/+page.server.ts b/src/routes/(ui)/project-admin/view/+page.server.ts similarity index 100% rename from src/routes/project-admin/view/+page.server.ts rename to src/routes/(ui)/project-admin/view/+page.server.ts diff --git a/src/routes/project-admin/view/+page.svelte b/src/routes/(ui)/project-admin/view/+page.svelte similarity index 100% rename from src/routes/project-admin/view/+page.svelte rename to src/routes/(ui)/project-admin/view/+page.svelte diff --git a/src/routes/queue-admin/[...rest]/+page.svelte b/src/routes/(ui)/queue-admin/[...rest]/+page.svelte similarity index 100% rename from src/routes/queue-admin/[...rest]/+page.svelte rename to src/routes/(ui)/queue-admin/[...rest]/+page.svelte diff --git a/src/routes/release-admin/+page.server.ts b/src/routes/(ui)/release-admin/+page.server.ts similarity index 100% rename from src/routes/release-admin/+page.server.ts rename to src/routes/(ui)/release-admin/+page.server.ts diff --git a/src/routes/release-admin/+page.svelte b/src/routes/(ui)/release-admin/+page.svelte similarity index 100% rename from src/routes/release-admin/+page.svelte rename to src/routes/(ui)/release-admin/+page.svelte diff --git a/src/routes/release-admin/update/+page.server.ts b/src/routes/(ui)/release-admin/update/+page.server.ts similarity index 100% rename from src/routes/release-admin/update/+page.server.ts rename to src/routes/(ui)/release-admin/update/+page.server.ts diff --git a/src/routes/release-admin/update/+page.svelte b/src/routes/(ui)/release-admin/update/+page.svelte similarity index 100% rename from src/routes/release-admin/update/+page.svelte rename to src/routes/(ui)/release-admin/update/+page.svelte diff --git a/src/routes/release-admin/view/+page.server.ts b/src/routes/(ui)/release-admin/view/+page.server.ts similarity index 100% rename from src/routes/release-admin/view/+page.server.ts rename to src/routes/(ui)/release-admin/view/+page.server.ts diff --git a/src/routes/release-admin/view/+page.svelte b/src/routes/(ui)/release-admin/view/+page.svelte similarity index 100% rename from src/routes/release-admin/view/+page.svelte rename to src/routes/(ui)/release-admin/view/+page.svelte diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d80902ff..2546d7fb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,15 +1,10 @@ @@ -17,37 +12,4 @@ {dev ? '[DEV] ' : ''}{$title || 'SIL AppBuilder Administration'} -
- -
-
-
- {@render children?.()} -
-
-
-
© SIL Global {new Date().getFullYear()}
-
- Powered by  - SvelteKit -
-
- - +{@render children()} \ No newline at end of file