feat(sso): accept base-issued iframe login
CI / build (push) Has been skipped
CI / deploy (push) Successful in 47s

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:
eewing
2026-02-19 09:36:08 -06:00
parent bbf185df50
commit 7c1d089035
3 changed files with 61 additions and 3 deletions
+20
View File
@@ -3,6 +3,7 @@ import PocketBase from 'pocketbase';
import type { Handle } from '@sveltejs/kit';
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 ACCESS_DENIED_PATH = '/access-denied';
@@ -16,6 +17,25 @@ function hasHrmAccess(user: { HRM?: boolean } | null): boolean {
}
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);
// Load auth store from request cookie
+29 -2
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import * as Table from '$lib/components/ui/table/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
@@ -33,8 +35,25 @@
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) {
typeFilter = value;
goto(listUrl(), { replaceState: true });
loading = true;
if (searchQuery.trim()) {
searchEmployees(searchQuery);
@@ -109,13 +128,14 @@
const q = searchQuery.trim();
if (!q) {
searchDebounceId = null;
// Clear search: reload first page of full list
goto(listUrl(), { replaceState: true });
loading = true;
loadEmployees();
return;
}
searchDebounceId = setTimeout(() => {
searchDebounceId = null;
goto(listUrl(), { replaceState: true });
searchEmployees(searchQuery);
}, 300);
}
@@ -273,7 +293,14 @@
};
}
onMount(loadEmployees);
onMount(() => {
syncUrlToState();
if (searchQuery.trim()) {
searchEmployees(searchQuery);
} else {
loadEmployees();
}
});
</script>
<main class="flex flex-col gap-6 p-6 md:p-8">
+12 -1
View File
@@ -267,7 +267,18 @@
<main class="flex flex-col gap-6 p-6 md:p-8">
<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" />
</Button>
<h1 class="text-2xl font-semibold tracking-tight shrink-0 flex items-center gap-2 flex-wrap">