Company listing, authentication, and page error handling are all working

This commit is contained in:
2026-02-17 17:29:17 -06:00
parent 6d046e90ed
commit 8e225aa254
23 changed files with 2086 additions and 342 deletions
+77 -37
View File
@@ -1,27 +1,19 @@
import { api, user } from "$lib";
// src/hooks.server.ts
import { optima } from "$lib";
import { redirect, type Handle } from "@sveltejs/kit";
import { access } from "fs";
import { a } from "vitest/dist/chunks/suite.d.FvehnV49.js";
export const handle: Handle = async ({ event, resolve }) => {
const accessToken = event.cookies.get("access_token");
const refreshToken = event.cookies.get("refresh_token");
event.locals.session = {
accessToken: accessToken || "",
refreshToken: refreshToken || "",
};
const accessToken = event.cookies.get("accessToken") || null;
const refreshToken = event.cookies.get("refreshToken") || null;
if (event.url.pathname === "/logout") {
event.cookies.delete("access_token", { path: "/" });
event.cookies.delete("refresh_token", { path: "/" });
event.cookies.delete("accessToken", { path: "/" });
event.cookies.delete("refreshToken", { path: "/" });
redirect(303, "/login");
return resolve(event);
return redirect(303, "/login");
}
if (event.url.pathname.startsWith("/login") && user.isLoggedIn()) {
if (event.url.pathname.startsWith("/login") && optima.user.isLoggedIn()) {
return redirect(303, "/");
}
@@ -29,31 +21,79 @@ export const handle: Handle = async ({ event, resolve }) => {
return await resolve(event);
}
if (!accessToken || !refreshToken) {
user.logout(event);
return resolve(event);
if (!accessToken && !refreshToken) {
optima.user.logout(event);
redirect(303, "/login");
}
try {
if (accessToken && refreshToken) {
const newSession = await user.refreshSession(refreshToken);
// Check if the access token is expired or near expiry and refresh if needed
let currentAccessToken = accessToken;
let currentRefreshToken = refreshToken;
console.log(newSession);
if (currentAccessToken) {
try {
const [, payload] = currentAccessToken.split(".");
const decoded = JSON.parse(
Buffer.from(payload, "base64url").toString("utf8"),
);
const nowSec = Math.floor(Date.now() / 1000);
const thresholdSec = 60; // refresh if < 60s remaining
event.cookies.set("access_token", newSession.accessToken, {
httpOnly: true,
path: "/",
});
event.cookies.set("refresh_token", newSession.refreshToken, {
httpOnly: true,
path: "/",
});
if (!decoded?.exp || decoded.exp - nowSec < thresholdSec) {
// Token is expired or about to expire — try to refresh
if (currentRefreshToken) {
const refreshed =
await optima.user.refreshSession(currentRefreshToken);
currentAccessToken = refreshed.accessToken;
currentRefreshToken = refreshed.refreshToken ?? currentRefreshToken;
} else {
// No refresh token available, force re-login
optima.user.logout(event);
return redirect(303, "/login");
}
}
} catch {
// Token is malformed or refresh failed — try refresh as fallback
if (currentRefreshToken) {
try {
const refreshed =
await optima.user.refreshSession(currentRefreshToken);
currentAccessToken = refreshed.accessToken;
currentRefreshToken = refreshed.refreshToken ?? currentRefreshToken;
} catch {
// Refresh also failed, force re-login
optima.user.logout(event);
return redirect(303, "/login");
}
} else {
optima.user.logout(event);
return redirect(303, "/login");
}
}
} else if (currentRefreshToken) {
// No access token but have a refresh token — try to get a new one
try {
const refreshed = await optima.user.refreshSession(currentRefreshToken);
currentAccessToken = refreshed.accessToken;
currentRefreshToken = refreshed.refreshToken ?? currentRefreshToken;
} catch {
optima.user.logout(event);
return redirect(303, "/login");
}
} catch (err) {
console.trace(err);
user.logout(event);
} finally {
return await resolve(event);
}
const setTokens = async (accessToken: string, refreshToken: string) => {
event.cookies.set("accessToken", accessToken, { path: "/" });
event.cookies.set("refreshToken", refreshToken, { path: "/" });
event.locals.session = { accessToken, refreshToken, set: setTokens };
return;
};
// Persist any refreshed tokens into cookies
await setTokens(currentAccessToken!, currentRefreshToken!);
const response = await resolve(event);
return response;
};
+8 -2
View File
@@ -9,8 +9,13 @@ export const company = {
});
return company.data;
},
async fetchMany(accessToken: string, page: number = 1, search?: string) {
const params: Record<string, unknown> = { page };
async fetchMany(
accessToken: string,
page: number = 1,
search?: string,
rpp: number = 30,
) {
const params: Record<string, unknown> = { page, rpp };
if (search && search.length > 0) params.search = search;
const companies = await api.get("/v1/company/companies", {
@@ -19,6 +24,7 @@ export const company = {
Authorization: `Bearer ${accessToken}`,
},
});
return companies.data;
},
async fetchConfigurations(accessToken: string, id: string) {
+25 -3
View File
@@ -2,12 +2,13 @@ import { getRequestEvent } from "$app/server";
import { PUBLIC_API_URL } from "$env/static/public";
import { redirect, RequestEvent } from "@sveltejs/kit";
import axios from "axios";
import api from "../axios";
import { io } from "socket.io-client";
export const user = {
isLoggedIn(): boolean {
const event = getRequestEvent();
const authToken = event.cookies.get("authToken");
const authToken = event.cookies.get("accessToken");
return !!authToken;
},
@@ -28,18 +29,39 @@ export const user = {
return refreshedTokens;
},
fetchInfo() {},
async fetchInfo(accessToken: string) {
const response = await api.get("/v1/user/@me", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
},
logout(event: RequestEvent) {
if (!event) return;
// Clear authentication cookies
event.cookies.delete("authToken", { path: "/" });
event.cookies.delete("accessToken", { path: "/" });
event.cookies.delete("refreshToken", { path: "/" });
return redirect(303, "/login");
},
async checkPermissions(accessToken: string, permissions: string[]) {
const response = await api.post(
"/v1/user/@me/check-permission",
{ permissions },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
/**
* @todo Get communication with server working and setup a key system so that the frontend can listen for a specific key from the backend so that nobody can poach off of login events.
*
+1
View File
@@ -0,0 +1 @@
export const ssr = true;
+1
View File
@@ -0,0 +1 @@
<slot />
+2 -2
View File
@@ -9,11 +9,11 @@ export const actions: Actions = {
data.get("callbackKey") as string,
);
event.cookies.set("access_token", tokens.accessToken, {
event.cookies.set("accessToken", tokens.accessToken, {
httpOnly: true,
path: "/",
});
event.cookies.set("refresh_token", tokens.refreshToken, {
event.cookies.set("refreshToken", tokens.refreshToken, {
httpOnly: true,
path: "/",
});
+1 -2
View File
@@ -2,11 +2,10 @@
import { optima } from "$lib";
import { PUBLIC_API_URL } from "$env/static/public";
import { enhance } from "$app/forms";
import { goto } from "$app/navigation";
import LoadingSpinner from "../../../components/LoadingSpinner.svelte";
import { writable } from "svelte/store";
const uriData = await fetchAuthRedirectUri(PUBLIC_API_URL);
const uriData = await optima.auth.fetchAuthRedirectUri(PUBLIC_API_URL);
let loading = writable(false);
function handleSubmit(e: SubmitEvent) {
-80
View File
@@ -1,80 +0,0 @@
// src/routes/(secure)/+layout.server.ts
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
type Session = {
accessToken: string | null;
refreshToken: string | null;
};
type JwtPayload = {
exp?: number; // seconds since epoch
[key: string]: unknown;
};
const parseJwt = (token: string): JwtPayload | null => {
try {
const [, payload] = token.split(".");
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
} catch {
return null;
}
};
export const load: LayoutServerLoad = async (event) => {
const { locals, url } = event;
const session = locals.session as Session | undefined;
const accessToken = session?.accessToken ?? null;
const refreshToken = session?.refreshToken ?? null;
if (!accessToken && !refreshToken) {
throw redirect(302, `/login?redirectTo=${url.pathname}`);
}
// Decide if access token is expired/near expiry from its exp claim
let needsRefresh = false;
if (accessToken) {
const payload = parseJwt(accessToken);
if (!payload?.exp) {
needsRefresh = true; // malformed or no exp, play safe
} else {
const nowSec = Math.floor(Date.now() / 1000);
const thresholdSec = 60; // refresh if < 60s remaining
if (payload.exp - nowSec < thresholdSec) {
needsRefresh = true;
}
}
} else if (refreshToken) {
// No access token but we have a refresh token
needsRefresh = true;
}
if (needsRefresh && refreshToken) {
const refreshed = await refreshTokens(refreshToken);
if (!refreshed) {
throw redirect(302, `/login?redirectTo=${url.pathname}`);
}
await locals.session!.set(
refreshed.accessToken,
refreshed.refreshToken ?? refreshToken,
);
}
// Expose only what secure pages need; they can decode again or call backend
return {
accessToken: locals.session!.accessToken,
};
};
// Your backend-specific implementation
async function refreshTokens(refreshToken: string): Promise<{
accessToken: string;
refreshToken?: string;
} | null> {
// e.g. call /auth/refresh and return new tokens
return null;
}
+69 -152
View File
@@ -1,9 +1,10 @@
<script>
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import "../styles/errorpage.css";
function signOut() {
goto("/logout");
}
$: status = $page.status || 500;
$: message = $page.error?.message || "Something went wrong";
function goBack() {
history.back();
@@ -11,159 +12,75 @@
</script>
<svelte:head>
<title>Error — App</title>
<title>Error {status} — Project Optima</title>
</svelte:head>
<header class="header container">
<h1>Error</h1>
<nav>
<a href="/">Home</a>
<button on:click={signOut}>Sign out</button>
</nav>
</header>
<div class="error-page">
<div class="error-pane">
<!-- Pane header -->
<div class="error-pane-header">
<h2 class="error-pane-title">Error</h2>
<span class="error-status-badge">{status}</span>
</div>
<main class="container">
<section class="error-section">
<div class="error-box">
<h2>Oops! Something went wrong</h2>
<p class="error-message">
We encountered an error while processing your request. Please try again
or contact support if the problem persists.
<!-- Pane body -->
<div class="error-pane-body">
<div class="error-illustration">
<svg viewBox="0 0 120 120" width="120" height="120" aria-hidden="true">
<circle
cx="60"
cy="60"
r="56"
fill="#fef2f2"
stroke="#fecaca"
stroke-width="2"
/>
<circle cx="60" cy="60" r="40" fill="#fee2e2" />
<path
d="M60 35v30"
stroke="#dc2626"
stroke-width="5"
stroke-linecap="round"
/>
<circle cx="60" cy="78" r="4" fill="#dc2626" />
</svg>
</div>
<h3 class="error-heading">Oops! Something went wrong</h3>
<p class="error-message">{message}</p>
<p class="error-hint">
Please try again or contact support if the problem persists.
</p>
<div class="error-actions">
<button class="btn btn-primary" on:click={goBack}>Go Back</button>
<a href="/" class="btn btn-secondary">Go Home</a>
<button class="btn btn-primary" on:click={goBack}>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
width="16"
height="16"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Go Back
</button>
<a href="/" class="btn btn-secondary">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
width="16"
height="16"
>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
Go Home
</a>
</div>
</div>
</section>
</main>
<footer class="container">
<small>© {new Date().getFullYear()} Your App</small>
</footer>
<style>
:global(body) {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial;
background: #f7f7f8;
color: #111;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e7eb;
background: #fff;
}
nav {
display: flex;
gap: 0.5rem;
align-items: center;
}
nav a,
nav button {
padding: 0.5rem 1rem;
text-decoration: none;
color: #0066cc;
border: none;
background: none;
cursor: pointer;
font-size: 1rem;
}
nav a:hover,
nav button:hover {
text-decoration: underline;
}
main {
padding: 2rem 1rem;
}
.error-section {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.error-box {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.error-box h2 {
margin: 0 0 1rem;
color: #d32f2f;
font-size: 1.5rem;
}
.error-message {
color: #666;
margin: 1rem 0 2rem;
line-height: 1.6;
}
.error-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.btn-primary {
background: #0066cc;
color: white;
}
.btn-primary:hover {
background: #0052a3;
}
.btn-secondary {
background: #e5e7eb;
color: #111;
}
.btn-secondary:hover {
background: #d1d5db;
}
footer {
text-align: center;
padding: 2rem 1rem;
border-top: 1px solid #e5e7eb;
color: #666;
}
</style>
</div>
</div>
+24 -1
View File
@@ -1 +1,24 @@
// Layout server code removed for fresh start
import { optima } from "$lib";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ locals }) => {
const accessToken = locals.session?.accessToken ?? null;
// Only check permissions if the user is authenticated
if (!accessToken) {
return { canViewAdmin: false };
}
let canViewAdmin = false;
try {
const permResult = await optima.user.checkPermissions(accessToken, [
"ui.navigation.admin.view",
]);
canViewAdmin = permResult?.data?.results?.[0]?.hasPermission === true;
} catch (err) {
console.error("Admin permission check failed:", err);
canViewAdmin = false;
}
return { canViewAdmin };
};
+102
View File
@@ -0,0 +1,102 @@
<script lang="ts">
import { optima } from "$lib";
import { page } from "$app/stores";
const navItems = [
{
href: "/",
label: "Home",
icon: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline>',
exact: true,
},
{
href: "/companies",
label: "Companies",
icon: '<path d="M3 21h18"></path><path d="M5 21V7l8-4v18"></path><path d="M19 21V11l-6-4"></path><path d="M9 9h1"></path><path d="M9 13h1"></path><path d="M9 17h1"></path>',
},
];
const adminNavItem = {
href: "/admin",
label: "Admin",
icon: '<path d="M12 15v2m-6 4h12a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2zm10-10V7a4 4 0 0 0-8 0v4h8z"></path>',
};
$: canViewAdmin = $page.data?.canViewAdmin === true;
function isActive(pathname: string, item: (typeof navItems)[0]) {
return item.exact ? pathname === item.href : pathname.startsWith(item.href);
}
</script>
{#if $page.route.id?.startsWith("/(auth)")}
<slot />
{:else}
<div class="layout-container">
<header class="header">
<div class="header-content">
<h1>Project Optima</h1>
</div>
</header>
<div class="layout-wrapper">
<aside class="sidebar">
<nav class="sidebar-nav">
{#each navItems as item}
<a
href={item.href}
class="nav-item {isActive($page.url.pathname, item)
? 'active'
: ''}"
title={item.label}
>
<svg
class="nav-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
{@html item.icon}
</svg>
<span class="nav-label">{item.label}</span>
</a>
{/each}
{#if canViewAdmin}
<hr class="nav-divider" />
<a
href={adminNavItem.href}
class="nav-item {$page.url.pathname.startsWith('/admin')
? 'active'
: ''}"
title={adminNavItem.label}
>
<svg
class="nav-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
{@html adminNavItem.icon}
</svg>
<span class="nav-label">{adminNavItem.label}</span>
</a>
{/if}
</nav>
</aside>
<div class="content-area">
<div class="accent-bar"></div>
<main class="main-content">
<slot />
</main>
</div>
</div>
<footer class="footer">
<small>&copy; {new Date().getFullYear()} Total Tech Solutions, LLC</small>
</footer>
</div>
{/if}
+3 -1
View File
@@ -1 +1,3 @@
import "..styles/app.css";
import "../styles/app.css";
import "../styles/layout.css";
import "../styles/errorpage.css";
+1 -2
View File
@@ -7,6 +7,5 @@
</svelte:head>
<main>
<h1>Welcome</h1>
<p>Your new landing page. Ready to build.</p>
<h1>Home Page</h1>
</main>
+34
View File
@@ -0,0 +1,34 @@
import { optima } from "$lib";
import { handleApiError } from "$lib/optima-api/errorHandler";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals, url }) => {
const accessToken = locals.session?.accessToken;
if (!accessToken) {
return {
companies: [],
totalPages: 1,
currentPage: 1,
totalRecords: 0,
search: "",
};
}
const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
const search = url.searchParams.get("search") || "";
try {
const result = await optima.company.fetchMany(accessToken, page, search);
return {
companies: result?.data ?? [],
totalPages: result?.meta?.pagination?.totalPages ?? 1,
currentPage: result?.meta?.pagination?.currentPage ?? page,
totalRecords:
result?.meta?.pagination?.totalRecords ?? result?.data?.length ?? 0,
search,
};
} catch (err) {
handleApiError(err);
}
};
+355
View File
@@ -0,0 +1,355 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { afterNavigate } from "$app/navigation";
import NoResultsMonkey from "../../components/NoResultsMonkey.svelte";
import "../../styles/companies/companylist.css";
export let data: {
companies: Array<{
id: string;
name: string;
status?: string;
type?: string;
createdAt?: string;
identifier?: string;
contactEmail?: string;
[key: string]: unknown;
}>;
totalPages: number;
currentPage: number;
totalRecords: number;
search: string;
};
let searchInput = data.search;
let debounceTimer: ReturnType<typeof setTimeout>;
let isSearching = false;
let searchInputEl: HTMLInputElement;
// When navigation completes (results loaded), clear loading & refocus
afterNavigate(() => {
isSearching = false;
if (searchInputEl && document.activeElement !== searchInputEl) {
// Use tick to ensure DOM is settled
requestAnimationFrame(() => searchInputEl?.focus());
}
});
$: currentPage = data.currentPage;
$: totalPages = data.totalPages;
$: totalRecords = data.totalRecords;
$: companies = data.companies;
function navigateToPage(p: number) {
const params = new URLSearchParams();
params.set("page", String(p));
if (searchInput) params.set("search", searchInput);
goto(`/companies?${params.toString()}`);
}
function handleSearch() {
isSearching = true;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const params = new URLSearchParams();
params.set("page", "1");
if (searchInput) params.set("search", searchInput);
goto(`/companies?${params.toString()}`);
}, 300);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter") {
isSearching = true;
clearTimeout(debounceTimer);
const params = new URLSearchParams();
params.set("page", "1");
if (searchInput) params.set("search", searchInput);
goto(`/companies?${params.toString()}`);
}
}
function formatDate(dateStr?: string): string {
if (!dateStr) return "";
try {
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} catch {
return "";
}
}
function statusClass(status?: string): string {
if (!status) return "neutral";
const s = status.toLowerCase();
if (s === "active") return "active";
if (s === "inactive" || s === "disabled") return "inactive";
if (s === "pending") return "pending";
return "neutral";
}
function companyInitials(name: string): string {
return name
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0])
.join("")
.toUpperCase();
}
// Generate visible page numbers with ellipsis
function getPageNumbers(current: number, total: number): (number | "...")[] {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages: (number | "...")[] = [];
pages.push(1);
if (current > 3) pages.push("...");
const start = Math.max(2, current - 1);
const end = Math.min(total - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < total - 2) pages.push("...");
pages.push(total);
return pages;
}
$: pageNumbers = getPageNumbers(currentPage, totalPages);
</script>
<svelte:head>
<title>Companies — Project Optima</title>
</svelte:head>
<div class="companies-page">
<div class="companies-pane">
<!-- Pane header -->
<div class="pane-header">
<div class="pane-header-left">
<h2 class="page-title">Companies</h2>
{#if totalRecords > 0}
<span class="result-count"
>{totalRecords} record{totalRecords === 1 ? "" : "s"}</span
>
{/if}
</div>
<div class="search-bar">
<svg
class="search-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
</svg>
<input
type="text"
placeholder="Search companies…"
bind:this={searchInputEl}
bind:value={searchInput}
on:input={handleSearch}
on:keydown={handleKeydown}
/>
{#if searchInput}
<button
class="search-clear"
on:click={() => {
searchInput = "";
handleSearch();
}}
aria-label="Clear search"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
width="14"
height="14"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
{/if}
</div>
</div>
<!-- Pane body -->
<div class="pane-body">
{#if isSearching}
<div class="search-loading-overlay">
<div class="search-spinner"></div>
</div>
{/if}
{#if companies.length === 0}
<div class="empty-state">
<NoResultsMonkey
message={searchInput
? "No companies match your search"
: "No companies found"}
/>
</div>
{:else}
<div class="card-grid">
{#each companies as company (company.id)}
<button
class="company-card"
on:click={() => goto(`/companies/${company.id}`)}
on:keydown={(e) => {
if (e.key === "Enter") goto(`/companies/${company.id}`);
}}
>
<!-- Card header: avatar + status -->
<div class="card-top">
<div class="card-avatar">
<span class="avatar-initials"
>{companyInitials(company.name)}</span
>
</div>
<span
class="status-dot {statusClass(company.status)}"
title={company.status || "Unknown"}
></span>
</div>
<!-- Card body -->
<div class="card-body">
<h3 class="card-name">{company.name}</h3>
{#if company.contactEmail}
<span class="card-email">{company.contactEmail}</span>
{/if}
</div>
<!-- Card meta -->
<div class="card-meta">
{#if company.type}
<div class="meta-item">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="meta-icon"
>
<rect x="2" y="7" width="20" height="14" rx="2" ry="2" />
<path d="M16 3h-8l-2 4h12z" />
</svg>
<span>{company.type}</span>
</div>
{/if}
{#if company.identifier || company.id}
<div class="meta-item">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="meta-icon"
>
<path d="M4 9h16M4 15h16M10 3L8 21M16 3l-2 18" />
</svg>
<span class="mono"
>{company.identifier || company.id?.slice(0, 8)}</span
>
</div>
{/if}
</div>
<!-- Card footer -->
<div class="card-footer">
{#if company.status}
<span class="status-label {statusClass(company.status)}"
>{company.status}</span
>
{/if}
{#if formatDate(company.createdAt)}
<span class="card-date">{formatDate(company.createdAt)}</span>
{/if}
</div>
<!-- Hover arrow -->
<svg
class="card-arrow"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
{/each}
</div>
{/if}
</div>
<!-- Pane footer / Pagination -->
{#if totalPages > 1}
<div class="pane-footer">
<span class="page-info">
Page {currentPage} of {totalPages}
</span>
<nav class="pagination" aria-label="Pagination">
<button
class="page-btn"
disabled={currentPage <= 1}
on:click={() => navigateToPage(currentPage - 1)}
aria-label="Previous page"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
width="16"
height="16"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
{#each pageNumbers as p}
{#if p === "..."}
<span class="page-ellipsis"></span>
{:else}
<button
class="page-btn"
class:active={p === currentPage}
on:click={() => navigateToPage(p)}
aria-current={p === currentPage ? "page" : undefined}
>
{p}
</button>
{/if}
{/each}
<button
class="page-btn"
disabled={currentPage >= totalPages}
on:click={() => navigateToPage(currentPage + 1)}
aria-label="Next page"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
width="16"
height="16"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</nav>
</div>
{/if}
</div>
</div>
-21
View File
@@ -1,21 +0,0 @@
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
const accessToken = event.cookies.get("acceessToken") || null;
const refreshToken = event.cookies.get("refreshToken") || null;
const setTokens = async (accessToken: string, refreshToken: string) => {
event.cookies.set("accessToken", accessToken, {} as any);
event.cookies.set("refreshToken", refreshToken, {} as any);
event.locals.session = { accessToken, refreshToken, set: setTokens };
return;
};
event.locals.session = { accessToken, refreshToken, set: setTokens };
const response = await resolve(event);
return response;
};
+8
View File
@@ -1,3 +1,11 @@
@import "tailwindcss";
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
}
+615
View File
@@ -0,0 +1,615 @@
/* ═══════════════════════════════════════════════════
Companies Page — Pane + Card Grid Layout
═══════════════════════════════════════════════════ */
/* Page container */
.companies-page {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
width: 100%;
}
/* ── Pane container ── */
.companies-pane {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background: #ffffff;
border-radius: 12px;
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.08),
0 1px 4px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
/* ── Pane header ── */
.pane-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 20px 24px 16px;
border-bottom: 1px solid #eef0f3;
flex-shrink: 0;
}
.pane-header-left {
display: flex;
align-items: baseline;
gap: 12px;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #2c3e50;
}
.result-count {
font-size: 13px;
font-weight: 500;
color: #8492a6;
}
.search-bar {
position: relative;
width: 280px;
}
.search-bar input {
width: 100%;
padding: 9px 34px 9px 38px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
background: #f9fafb;
color: #374151;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.search-bar input::placeholder {
color: #9ca3af;
}
.search-bar input:focus {
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.12);
background: #fff;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: #9ca3af;
pointer-events: none;
}
.search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition:
color 0.15s,
background 0.15s;
}
.search-clear:hover {
color: #374151;
background: #f3f4f6;
}
/* ── Pane body ── */
.pane-body {
position: relative;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 20px 24px;
}
/* Search loading overlay */
.search-loading-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
backdrop-filter: blur(2px);
}
.search-spinner {
width: 36px;
height: 36px;
border-radius: 50%;
border: 4px solid #eef0f3;
border-top-color: #3498db;
animation: search-spin 0.7s linear infinite;
}
@keyframes search-spin {
to {
transform: rotate(360deg);
}
}
/* ── Empty state ── */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
padding: 48px 16px;
min-height: 200px;
}
/* ═══════════════════════════════════════════════════
Card Grid
═══════════════════════════════════════════════════ */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
/* ── Individual Card ── */
.company-card {
position: relative;
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
background: #f8f9fb;
border-radius: 12px;
border: 1px solid #eef0f3;
box-shadow: none;
cursor: pointer;
transition:
transform 0.18s,
box-shadow 0.18s,
border-color 0.18s,
background 0.18s;
text-align: left;
font: inherit;
color: inherit;
overflow: hidden;
width: 100%;
}
.company-card:hover {
transform: translateY(-3px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.08),
0 2px 8px rgba(0, 0, 0, 0.04);
border-color: #d0d9e8;
}
.company-card:active {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.company-card:focus-visible {
outline: 2px solid #3498db;
outline-offset: 2px;
}
/* Card top: avatar + status dot */
.card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.card-avatar {
width: 44px;
height: 44px;
border-radius: 12px;
background: linear-gradient(135deg, #3498db, #2980b9);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-initials {
font-size: 16px;
font-weight: 700;
color: #fff;
letter-spacing: 0.5px;
line-height: 1;
}
/* Status dot */
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 4px;
}
.status-dot.active {
background: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
}
.status-dot.inactive {
background: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
}
.status-dot.pending {
background: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
}
.status-dot.neutral {
background: #d1d5db;
box-shadow: 0 0 0 3px rgba(209, 213, 219, 0.3);
}
/* Card body */
.card-body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.card-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.card-email {
font-size: 13px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Card meta */
.card-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: #64748b;
}
.meta-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
color: #94a3b8;
}
.meta-item .mono {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
font-size: 11px;
letter-spacing: -0.02em;
}
/* Card footer */
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding-top: 10px;
border-top: 1px solid #f1f5f9;
margin-top: auto;
}
.status-label {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
text-transform: capitalize;
letter-spacing: 0.02em;
}
.status-label.active {
background: #dcfce7;
color: #15803d;
}
.status-label.inactive {
background: #fee2e2;
color: #b91c1c;
}
.status-label.pending {
background: #fef3c7;
color: #a16207;
}
.status-label.neutral {
background: #f1f5f9;
color: #64748b;
}
.card-date {
font-size: 12px;
color: #94a3b8;
margin-left: auto;
}
/* Hover arrow */
.card-arrow {
position: absolute;
top: 20px;
right: 16px;
width: 18px;
height: 18px;
color: #cbd5e1;
opacity: 0;
transform: translateX(-4px);
transition:
opacity 0.18s,
transform 0.18s;
}
.company-card:hover .card-arrow {
opacity: 1;
transform: translateX(0);
}
/* ═══════════════════════════════════════════════════
Pagination Footer
═══════════════════════════════════════════════════ */
.pane-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 24px;
border-top: 1px solid #eef0f3;
background: #f8f9fb;
flex-shrink: 0;
}
.page-info {
font-size: 13px;
color: #8492a6;
}
.pagination {
display: flex;
align-items: center;
gap: 4px;
}
.page-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
padding: 0 6px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
color: #374151;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.page-btn:hover:not(:disabled):not(.active) {
background: #f3f4f6;
border-color: #9ca3af;
}
.page-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-btn.active {
background: #3498db;
border-color: #3498db;
color: #fff;
font-weight: 600;
}
.page-ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: #9ca3af;
font-size: 13px;
user-select: none;
}
/* ═══════════════════════════════════════════════════
Responsive
═══════════════════════════════════════════════════ */
/* Tablets — 2 columns */
@media (max-width: 1024px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
}
/* Mobile — single column */
@media (max-width: 768px) {
.companies-pane {
border-radius: 10px;
}
.pane-header {
flex-direction: column;
align-items: stretch;
gap: 10px;
padding: 16px 16px 12px;
}
.pane-header-left {
justify-content: space-between;
width: 100%;
}
.page-title {
font-size: 18px;
}
.search-bar {
width: 100%;
}
.search-bar input {
font-size: 16px; /* prevents iOS zoom */
padding: 11px 34px 11px 40px;
}
.pane-body {
padding: 12px;
}
.card-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.company-card {
padding: 14px;
border-radius: 10px;
gap: 12px;
}
.card-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
}
.avatar-initials {
font-size: 15px;
}
.card-name {
font-size: 15px;
}
.card-arrow {
display: none;
}
.pane-footer {
flex-direction: column;
gap: 8px;
padding: 12px 16px;
}
.page-btn {
min-width: 36px;
height: 36px;
}
}
/* Small phones */
@media (max-width: 480px) {
.companies-pane {
border-radius: 8px;
}
.pane-header {
padding: 12px 12px 10px;
}
.page-title {
font-size: 16px;
}
.pane-body {
padding: 8px;
}
.company-card {
padding: 12px;
gap: 10px;
}
.card-avatar {
width: 36px;
height: 36px;
}
.avatar-initials {
font-size: 14px;
}
.card-name {
font-size: 14px;
}
.card-email {
font-size: 12px;
}
.status-label {
font-size: 10px;
padding: 2px 8px;
}
.card-date {
font-size: 11px;
}
.pane-footer {
padding: 10px 12px;
}
.page-btn {
min-width: 32px;
height: 32px;
font-size: 13px;
}
}
+233
View File
@@ -0,0 +1,233 @@
/* ═══════════════════════════════════════════════════
Error Page — Pane Layout
═══════════════════════════════════════════════════ */
/* Page container */
.error-page {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
width: 100%;
}
/* ── Pane container ── */
.error-pane {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background: #ffffff;
border-radius: 12px;
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.08),
0 1px 4px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
/* ── Pane header ── */
.error-pane-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 24px 16px;
border-bottom: 1px solid #eef0f3;
flex-shrink: 0;
}
.error-pane-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #2c3e50;
}
.error-status-badge {
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
background: #fee2e2;
color: #b91c1c;
letter-spacing: 0.02em;
}
/* ── Pane body ── */
.error-pane-body {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
gap: 16px;
text-align: center;
overflow-y: auto;
}
/* Illustration */
.error-illustration {
margin-bottom: 8px;
}
/* Heading */
.error-heading {
margin: 0;
font-size: 22px;
font-weight: 700;
color: #1e293b;
line-height: 1.3;
}
/* Message */
.error-message {
margin: 0;
font-size: 15px;
color: #64748b;
line-height: 1.6;
max-width: 420px;
}
.error-hint {
margin: 0;
font-size: 13px;
color: #94a3b8;
line-height: 1.5;
max-width: 420px;
}
/* Actions */
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-top: 8px;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: all 0.18s;
line-height: 1;
}
.btn-primary {
background: #3498db;
color: #fff;
}
.btn-primary:hover {
background: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.25);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(52, 152, 219, 0.2);
}
.btn-secondary {
background: #f1f5f9;
color: #475569;
border: 1px solid #e2e8f0;
}
.btn-secondary:hover {
background: #e2e8f0;
border-color: #cbd5e1;
transform: translateY(-1px);
}
.btn-secondary:active {
transform: translateY(0);
}
/* ═══════════════════════════════════════════════════
Responsive
═══════════════════════════════════════════════════ */
@media (max-width: 768px) {
.error-pane {
border-radius: 10px;
}
.error-pane-header {
padding: 16px 16px 12px;
}
.error-pane-title {
font-size: 18px;
}
.error-pane-body {
padding: 32px 16px;
gap: 14px;
}
.error-heading {
font-size: 20px;
}
.error-message {
font-size: 14px;
}
.btn {
padding: 10px 18px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.error-pane {
border-radius: 8px;
}
.error-pane-header {
padding: 12px 12px 10px;
}
.error-pane-title {
font-size: 16px;
}
.error-pane-body {
padding: 24px 12px;
gap: 12px;
}
.error-illustration svg {
width: 96px;
height: 96px;
}
.error-heading {
font-size: 18px;
}
.error-message {
font-size: 13px;
}
.error-actions {
flex-direction: column;
width: 100%;
}
.btn {
justify-content: center;
width: 100%;
}
}
+328
View File
@@ -0,0 +1,328 @@
/* Layout Container */
.layout-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
/* Header */
.header {
height: 60px;
background-color: #ffffff;
color: #2c3e50;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
z-index: 100;
}
.header-content {
display: flex;
align-items: center;
width: 100%;
}
.header-content h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
letter-spacing: 1px;
}
/* Layout Wrapper */
.layout-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 72px;
background-color: #ffffff;
border-right: 1px solid #e0e0e0;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
overflow-y: auto;
overflow-x: hidden;
z-index: 99;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0;
align-items: center;
width: 100%;
}
/* Navigation Items */
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 14px 0;
color: #666666;
text-decoration: none;
transition: all 0.2s ease;
position: relative;
cursor: pointer;
gap: 4px;
box-sizing: border-box;
border-left: 3px solid transparent;
}
.nav-item:hover {
background-color: #f0f4ff;
color: #2c3e50;
}
.nav-item:active {
background-color: #e0eaff;
color: #3498db;
}
/* Active page indicator */
.nav-item.active {
color: #3498db;
background-color: #eef4ff;
border-left: 3px solid #3498db;
}
.nav-item.active .nav-icon {
stroke: #3498db;
}
.nav-item.active .nav-label {
color: #3498db;
font-weight: 600;
}
/* Navigation Icon */
.nav-icon {
width: 24px;
height: 24px;
stroke: currentColor;
flex-shrink: 0;
}
/* Navigation Label */
.nav-label {
font-size: 11px;
font-weight: 500;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60px;
}
/* Main Content */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
background:
linear-gradient(135deg, #3498db, #2980b9) top / 100% 220px no-repeat,
#f0f0f0;
}
.accent-bar {
height: 0;
background: linear-gradient(135deg, #3498db, #2980b9);
flex-shrink: 0;
}
.main-content {
flex: 1;
overflow: hidden;
background-color: transparent;
padding: 20px;
padding-top: 80px;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* Scrollbar Styling */
.sidebar::-webkit-scrollbar,
.main-content::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track,
.main-content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb,
.main-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.main-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Footer */
.footer {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
font-size: 12px;
color: #999;
flex-shrink: 0;
}
/* Nav Divider */
.nav-divider {
width: calc(100% - 1.5rem);
border: none;
border-top: 1px solid #d4dae3;
margin: 0.5rem auto;
padding: 0;
box-sizing: border-box;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.header {
height: 48px;
padding: 0 12px;
}
.header-content h1 {
font-size: 18px;
}
.layout-wrapper {
flex-direction: column;
/* Account for fixed bottom nav */
padding-bottom: 56px;
}
.sidebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 56px;
border-right: none;
border-top: 1px solid #e0e0e0;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.08);
padding: 0;
z-index: 100;
}
.sidebar-nav {
flex-direction: row;
justify-content: space-around;
height: 100%;
}
.nav-item {
padding: 6px 0;
min-width: 64px;
border-left: none;
border-bottom: 3px solid transparent;
}
.nav-item.active {
border-left: none;
border-bottom: 3px solid #3498db;
}
.nav-icon {
width: 22px;
height: 22px;
}
.nav-label {
font-size: 10px;
}
.main-content {
padding: 12px;
padding-top: 60px;
}
.content-area {
background:
linear-gradient(135deg, #3498db, #2980b9) top / 100% 190px no-repeat,
#f0f0f0;
}
.footer {
/* Hide footer on mobile since bottom nav takes that space */
display: none;
}
.nav-divider {
width: 1px;
height: calc(100% - 1rem);
border-top: none;
border-left: 1px solid #d4dae3;
margin: auto 0;
align-self: center;
}
}
/* Small phones */
@media (max-width: 480px) {
.header {
height: 44px;
padding: 0 8px;
}
.header-content h1 {
font-size: 16px;
}
.main-content {
padding: 8px;
padding-top: 40px;
}
.content-area {
background:
linear-gradient(135deg, #3498db, #2980b9) top / 100% 160px no-repeat,
#f0f0f0;
}
.nav-item {
min-width: 48px;
}
}
/* Safe area insets for notched devices */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
@media (max-width: 768px) {
.sidebar {
padding-bottom: env(safe-area-inset-bottom);
height: calc(56px + env(safe-area-inset-bottom));
}
.layout-wrapper {
padding-bottom: calc(56px + env(safe-area-inset-bottom));
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import { optima } from "$lib";
import { redirect, type Handle } from "@sveltejs/kit";
import { access } from "fs";
import { a } from "vitest/dist/chunks/suite.d.FvehnV49.js";
export const handle: Handle = async ({ event, resolve }) => {
const accessToken = event.cookies.get("access_token");
const refreshToken = event.cookies.get("refresh_token");
event.locals.ession = {
accessToken: accessToken || "",
refreshToken: refreshToken || "",
};
if (event.url.pathname === "/logout") {
event.cookies.delete("access_token", { path: "/" });
event.cookies.delete("refresh_token", { path: "/" });
redirect(303, "/login");
return resolve(event);
}
if (event.url.pathname.startsWith("/login") && optima.user.isLoggedIn()) {
return redirect(303, "/");
}
if (event.url.pathname.startsWith("/login")) {
return await resolve(event);
}
if (!accessToken || !refreshToken) {
optima.user.logout(event);
return resolve(event);
}
try {
if (accessToken && refreshToken) {
const newSession = await optima.user.refreshSession(refreshToken);
console.log(newSession);
event.cookies.set("access_token", newSession.accessToken, {
httpOnly: true,
path: "/",
});
event.cookies.set("refresh_token", newSession.refreshToken, {
httpOnly: true,
path: "/",
});
}
} catch (err) {
console.trace(err);
optima.user.logout(event);
} finally {
return await resolve(event);
}
};