feat(sso): accept base-issued iframe login
Handle Base SSO tokens on initial requests so HRM can bootstrap PocketBase auth and redirect into the app without a separate login.
This commit is contained in:
@@ -3,6 +3,7 @@ import PocketBase from 'pocketbase';
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
const POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
const POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
||||||
|
const BASE_SSO_URL = process.env.BASE_SSO_URL ?? 'https://base.ccllc.pro';
|
||||||
|
|
||||||
const LOGIN_PATH = '/login';
|
const LOGIN_PATH = '/login';
|
||||||
const ACCESS_DENIED_PATH = '/access-denied';
|
const ACCESS_DENIED_PATH = '/access-denied';
|
||||||
@@ -16,6 +17,25 @@ function hasHrmAccess(user: { HRM?: boolean } | null): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
// SSO: if opened from Base with ?sso=token, exchange for session and redirect (no auth yet)
|
||||||
|
const ssoParam = event.url.searchParams.get('sso');
|
||||||
|
if (ssoParam) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_SSO_URL}/api/sso/exchange?token=${encodeURIComponent(ssoParam)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const auth = await res.json();
|
||||||
|
const cookieVal = encodeURIComponent(JSON.stringify({ token: auth.token, model: auth.model }));
|
||||||
|
const setCookie = `pb_auth=${cookieVal}; Path=/; SameSite=Lax; Max-Age=604800`;
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: { Location: event.url.pathname, 'Set-Cookie': setCookie }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to normal auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.locals.pb = new PocketBase(POCKETBASE_URL);
|
event.locals.pb = new PocketBase(POCKETBASE_URL);
|
||||||
|
|
||||||
// Load auth store from request cookie
|
// Load auth store from request cookie
|
||||||
|
|||||||
+29
-2
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import * as Table from '$lib/components/ui/table/index.js';
|
import * as Table from '$lib/components/ui/table/index.js';
|
||||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||||
@@ -33,8 +35,25 @@
|
|||||||
return typeFilter === 'all' ? '' : `&type=${encodeURIComponent(typeFilter)}`;
|
return typeFilter === 'all' ? '' : `&type=${encodeURIComponent(typeFilter)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listUrl(): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (typeFilter !== 'all') params.set('type', typeFilter);
|
||||||
|
const q = searchQuery.trim();
|
||||||
|
if (q) params.set('search', q);
|
||||||
|
const s = params.toString();
|
||||||
|
return s ? `/?${s}` : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncUrlToState() {
|
||||||
|
const params = get(page).url.searchParams;
|
||||||
|
const type = params.get('type');
|
||||||
|
typeFilter = type === 'Sub-Contractor' ? 'Sub-Contractor' : type === 'Employee' ? 'Employee' : 'all';
|
||||||
|
searchQuery = params.get('search') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function setTypeFilter(value: TypeFilterValue) {
|
function setTypeFilter(value: TypeFilterValue) {
|
||||||
typeFilter = value;
|
typeFilter = value;
|
||||||
|
goto(listUrl(), { replaceState: true });
|
||||||
loading = true;
|
loading = true;
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
searchEmployees(searchQuery);
|
searchEmployees(searchQuery);
|
||||||
@@ -109,13 +128,14 @@
|
|||||||
const q = searchQuery.trim();
|
const q = searchQuery.trim();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
searchDebounceId = null;
|
searchDebounceId = null;
|
||||||
// Clear search: reload first page of full list
|
goto(listUrl(), { replaceState: true });
|
||||||
loading = true;
|
loading = true;
|
||||||
loadEmployees();
|
loadEmployees();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
searchDebounceId = setTimeout(() => {
|
searchDebounceId = setTimeout(() => {
|
||||||
searchDebounceId = null;
|
searchDebounceId = null;
|
||||||
|
goto(listUrl(), { replaceState: true });
|
||||||
searchEmployees(searchQuery);
|
searchEmployees(searchQuery);
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
@@ -273,7 +293,14 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(loadEmployees);
|
onMount(() => {
|
||||||
|
syncUrlToState();
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
searchEmployees(searchQuery);
|
||||||
|
} else {
|
||||||
|
loadEmployees();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="flex flex-col gap-6 p-6 md:p-8">
|
<main class="flex flex-col gap-6 p-6 md:p-8">
|
||||||
|
|||||||
@@ -267,7 +267,18 @@
|
|||||||
|
|
||||||
<main class="flex flex-col gap-6 p-6 md:p-8">
|
<main class="flex flex-col gap-6 p-6 md:p-8">
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<Button variant="ghost" size="icon" href="/" class="shrink-0">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
href="/"
|
||||||
|
class="shrink-0"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (typeof window !== 'undefined' && window.history.length > 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ArrowLeftIcon class="size-4" />
|
<ArrowLeftIcon class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-2xl font-semibold tracking-tight shrink-0 flex items-center gap-2 flex-wrap">
|
<h1 class="text-2xl font-semibold tracking-tight shrink-0 flex items-center gap-2 flex-wrap">
|
||||||
|
|||||||
Reference in New Issue
Block a user