style updates and re-added auth with a new method (thad to confirm)

This commit is contained in:
Zach Harding
2026-04-08 07:50:27 -04:00
parent d2ad949c2e
commit d5dbb7718d
4 changed files with 81 additions and 46 deletions

7
.env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
canAddInventory: boolean;
}
}

View File

@@ -1,17 +1,64 @@
// src/middleware.ts import { clerkMiddleware, createRouteMatcher, clerkClient } from '@clerk/astro/server';
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';
import type { AstroMiddlewareRequest, AstroMiddlewareResponse } from 'astro';
const isProtectedRoute = createRouteMatcher([ const isProtectedRoute = createRouteMatcher(['/pokemon']);
'/pokemon', const isAdminRoute = createRouteMatcher(['/admin']);
]);
export const onRequest = clerkMiddleware((auth, context) => { const TARGET_ORG_ID = "org_3Baav9czkRLLlC7g89oJWqRRulK";
const { isAuthenticated, redirectToSignIn } = auth()
export const onRequest = clerkMiddleware(async (auth, context) => {
const { isAuthenticated, userId, redirectToSignIn, has } = auth();
if (!isAuthenticated && isProtectedRoute(context.request)) { if (!isAuthenticated && isProtectedRoute(context.request)) {
// Add custom logic to run before redirecting return redirectToSignIn();
}
return redirectToSignIn() // ── Inventory visibility check ──────────────────────────────────────────────
// Resolves to true if the user belongs to the target org OR has the feature
const canAddInventory =
isAuthenticated &&
userId &&
(
has({ permission: "org:feature:inventory_add" }) || // Clerk feature flag
(await getUserOrgIds(context, userId)).includes(TARGET_ORG_ID)
);
// Expose the flag to your Astro pages via locals
context.locals.canAddInventory = canAddInventory ?? false;
// ── Admin route guard (unchanged) ───────────────────────────────────────────
if (isAdminRoute(context.request)) {
if (!isAuthenticated || !userId) {
return redirectToSignIn();
}
try {
const client = await clerkClient(context);
const memberships = await client.organizations.getOrganizationMembershipList({
organizationId: TARGET_ORG_ID,
});
const userMembership = memberships.data.find(
(m) => m.publicUserData?.userId === userId
);
if (!userMembership || userMembership.role !== "org:admin") {
return new Response(null, { status: 404 });
}
} catch (e) {
console.error("Clerk membership check failed:", e);
return context.redirect("/");
}
} }
}); });
// ── Helper: fetch all org IDs the current user belongs to ───────────────────
async function getUserOrgIds(context: any, userId: string): Promise<string[]> {
try {
const client = await clerkClient(context);
const memberships = await client.users.getOrganizationMembershipList({ userId });
return memberships.data.map((m) => m.organization.id);
} catch (e) {
console.error("Failed to fetch user org memberships:", e);
return [];
}
}

View File

@@ -10,17 +10,8 @@ import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
import { clerkClient } from '@clerk/astro/server'; // auth check for inventory management features
const { canAddInventory } = Astro.locals;
const { userId, has } = Astro.locals.auth();
const TARGET_ORG_ID = 'org_3Baav9czkRLLlC7g89oJWqRRulK';
let hasAccess = has({ feature: 'inventory_add' });
if (!hasAccess && userId) {
const memberships = await clerkClient(Astro).users.getOrganizationMembershipList({ userId });
hasAccess = memberships.data.some(m => m.organization.id === TARGET_ORG_ID);
}
export const partial = true; export const partial = true;
export const prerender = false; export const prerender = false;
@@ -239,8 +230,8 @@ const altSearchUrl = (card: any) => {
<span class="rarity-icon-large position-absolute bottom-0 end-0 d-inline"><RarityIcon rarity={card?.rarityName} /></span> <span class="rarity-icon-large position-absolute bottom-0 end-0 d-inline"><RarityIcon rarity={card?.rarityName} /></span>
</div> </div>
<div class="d-flex flex-column flex-lg-row justify-content-between mt-2"> <div class="d-flex flex-column flex-lg-row justify-content-between mt-2">
<div class="text-secondary">{card?.set?.setCode}</div> <div class="text-secondary"><span class="d-flex d-xxl-none">{card?.set?.setCode}</span><span class="d-none d-xxl-flex">{card?.set?.setName}</span></div>
<div class="text-secondary">Illus<span class="d-none d-lg-inline">trator</span>: {card?.artist}</div> <div class="text-secondary">Illus<span class="d-none d-xxl-inline">trator</span>: {card?.artist}</div>
</div> </div>
</div> </div>
@@ -272,13 +263,13 @@ const altSearchUrl = (card: any) => {
<span class="d-none">Damaged</span><span class="d-inline">DMG</span> <span class="d-none">Damaged</span><span class="d-inline">DMG</span>
</button> </button>
</li> </li>
{/* {hasAccess && ( */} {canAddInventory && (
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link vendor" id="vendor-tab" data-bs-toggle="tab" data-bs-target="#nav-vendor" type="button" role="tab" aria-controls="nav-vendor" aria-selected="false"> <button class="nav-link vendor" id="vendor-tab" data-bs-toggle="tab" data-bs-target="#nav-vendor" type="button" role="tab" aria-controls="nav-vendor" aria-selected="false">
<span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span> <span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span>
</button> </button>
</li> </li>
{/* )} */} )}
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
@@ -361,15 +352,15 @@ const altSearchUrl = (card: any) => {
</div> </div>
); );
})} })}
{/* {hasAccess && ( */} {canAddInventory && (
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0"> <div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
<div class="row g-4"> <div class="row g-5">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<h6 class="mt-1 mb-2">Add {card?.productName} to inventory</h6> <h6 class="mt-1 mb-2">Add {card?.productName} to inventory</h6>
<form id="inventoryForm" data-inventory-form novalidate> <form id="inventoryForm" data-inventory-form novalidate>
<div class="row gx-3 gy-1"> <div class="row gx-3 gy-1">
<div class="col-3"> <div class="col-12 col-lg-3">
<label for="quantity" class="form-label">Quantity</label> <label for="quantity" class="form-label">Quantity</label>
<input <input
type="number" type="number"
@@ -384,7 +375,7 @@ const altSearchUrl = (card: any) => {
<div class="invalid-feedback">Required.</div> <div class="invalid-feedback">Required.</div>
</div> </div>
<div class="col-9"> <div class="col-12 col-lg-9">
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap"> <div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
<label for="purchasePrice" class="form-label"> <label for="purchasePrice" class="form-label">
Purchase price Purchase price
@@ -436,7 +427,7 @@ const altSearchUrl = (card: any) => {
<div class="col-12"> <div class="col-12">
<label class="form-label">Condition</label> <label class="form-label">Condition</label>
<div class="btn-group condition-input w-100" role="group" aria-label="Condition"> <div class="btn-group btn-group-sm condition-input w-100 col-12" role="group" aria-label="Condition">
<input <input
type="radio" type="radio"
class="btn-check" class="btn-check"
@@ -521,7 +512,7 @@ const altSearchUrl = (card: any) => {
class="form-control" class="form-control"
id="note" id="note"
name="note" name="note"
rows="2" rows="3"
maxlength="255" maxlength="255"
placeholder="e.g. bought at local shop, gift, graded copy…" placeholder="e.g. bought at local shop, gift, graded copy…"
></textarea> ></textarea>
@@ -554,7 +545,7 @@ const altSearchUrl = (card: any) => {
</div> </div>
</div> </div>
</div> </div>
{/* )} */} )}
</div> </div>
<!-- Chart lives permanently outside tab-content so Bootstrap never hides it. --> <!-- Chart lives permanently outside tab-content so Bootstrap never hides it. -->

View File

@@ -1,23 +1,13 @@
--- ---
import { client } from '../../db/typesense'; import { client } from '../../db/typesense';
import { clerkClient } from '@clerk/astro/server';
const { userId, has } = Astro.locals.auth();
const TARGET_ORG_ID = 'org_3Baav9czkRLLlC7g89oJWqRRulK';
let hasAccess = has({ feature: 'inventory_add' });
if (!hasAccess && userId) {
const memberships = await clerkClient(Astro).users.getOrganizationMembershipList({ userId });
hasAccess = memberships.data.some(m => m.organization.id === TARGET_ORG_ID);
}
import RarityIcon from '../../components/RarityIcon.astro'; import RarityIcon from '../../components/RarityIcon.astro';
import FirstEditionIcon from "../../components/FirstEditionIcon.astro"; import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
export const prerender = false; export const prerender = false;
import * as util from 'util'; import * as util from 'util';
// auth check for inventory management features
const { canAddInventory } = Astro.locals;
// all the facet fields we want to use for filtering // all the facet fields we want to use for filtering
const facetFields:any = { const facetFields:any = {
@@ -296,11 +286,11 @@ const facets = searchResults.results.slice(1).map((result: any) => {
{pokemon.map((card:any) => ( {pokemon.map((card:any) => (
<div class="col"> <div class="col">
{/* {hasAccess && ( */} {canAddInventory && (
<button type="button" class="btn btn-sm inventory-button position-relative float-end shadow-filter text-center p-2" data-card-id={card.cardId} hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="event.stopPropagation(); sessionStorage.setItem('openModalTab', 'nav-vendor');"> <button type="button" class="btn btn-sm inventory-button position-relative float-end shadow-filter text-center p-2" data-card-id={card.cardId} hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="event.stopPropagation(); sessionStorage.setItem('openModalTab', 'nav-vendor');">
<b>+/</b> <b>+/</b>
</button> </button>
{/* )} */} )}
<div class="card-trigger position-relative" data-card-id={card.cardId} hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="const cardTitle = this.querySelector('#cardImage').getAttribute('alt'); dataLayer.push({'event': 'virtualPageview', 'pageUrl': this.getAttribute('hx-get'), 'pageTitle': cardTitle, 'previousUrl': '/pokemon'});"> <div class="card-trigger position-relative" data-card-id={card.cardId} hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="const cardTitle = this.querySelector('#cardImage').getAttribute('alt'); dataLayer.push({'event': 'virtualPageview', 'pageUrl': this.getAttribute('hx-get'), 'pageTitle': cardTitle, 'previousUrl': '/pokemon'});">
<div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/static/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span> <div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/static/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span>
<div class="holo-shine"></div> <div class="holo-shine"></div>