mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
security fixes
This commit is contained in:
parent
84dffa29f0
commit
a0486e9a27
@ -1,7 +1,7 @@
|
||||
import { getConnection } from "@/lib/ai/mcp-logger";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
@ -28,9 +28,11 @@ export const POST = createSmartRouteHandler({
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ body }, fullReq) => {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
if (getNodeEnvironment() !== "development") {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getConnection } from "@/lib/ai/mcp-logger";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
@ -25,9 +25,11 @@ export const POST = createSmartRouteHandler({
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ body }, fullReq) => {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
if (getNodeEnvironment() !== "development") {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getConnection } from "@/lib/ai/mcp-logger";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
@ -26,9 +26,11 @@ export const POST = createSmartRouteHandler({
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ body }, fullReq) => {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
if (getNodeEnvironment() !== "development") {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getConnection } from "@/lib/ai/mcp-logger";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
@ -29,9 +29,11 @@ export const POST = createSmartRouteHandler({
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ body }, fullReq) => {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
if (getNodeEnvironment() !== "development") {
|
||||
const metadata = fullReq.auth?.user?.client_read_only_metadata;
|
||||
if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) {
|
||||
throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations.");
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
|
||||
@ -15,7 +15,7 @@ function getDocsToolsBaseUrl(): string {
|
||||
}
|
||||
if (getNodeEnvironment() === "development") {
|
||||
const portPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
|
||||
return `http://localhost:${portPrefix}04`;
|
||||
return `http://localhost:${portPrefix}26`;
|
||||
}
|
||||
return "https://mcp.stack-auth.com";
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
# Stack Auth
|
||||
NEXT_PUBLIC_STACK_API_URL=
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=
|
||||
NEXT_PUBLIC_STACK_HOSTED_COMPONENTS_URL=
|
||||
NEXT_PUBLIC_STACK_INTERNAL_TOOL_URL=
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=
|
||||
NEXT_PUBLIC_STACK_API_URL=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=REPLACE_ME
|
||||
STACK_SECRET_SERVER_KEY=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=REPLACE_ME
|
||||
# SpacetimeDB
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=
|
||||
NEXT_PUBLIC_SPACETIMEDB_DB_NAME=
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=REPLACE_ME
|
||||
NEXT_PUBLIC_SPACETIMEDB_DB_NAME=REPLACE_ME
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:8102
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_STACK_HOSTED_COMPONENTS_URL=http://internal.localhost:8109
|
||||
NEXT_PUBLIC_STACK_INTERNAL_TOOL_URL=http://localhost:8141
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=ws://localhost:8139
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=ws://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}39
|
||||
NEXT_PUBLIC_SPACETIMEDB_DB_NAME=stack-auth-llm
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { useUser } from "@stackframe/react";
|
||||
import { useState } from "react";
|
||||
import { clsx } from "clsx";
|
||||
import { CallLogList } from "../components/CallLogList";
|
||||
import { CallLogDetail } from "../components/CallLogDetail";
|
||||
import { useState } from "react";
|
||||
import { AddManualQa } from "../components/AddManualQa";
|
||||
import { KnowledgeBase } from "../components/KnowledgeBase";
|
||||
import { Analytics } from "../components/Analytics";
|
||||
import { CallLogDetail } from "../components/CallLogDetail";
|
||||
import { CallLogList } from "../components/CallLogList";
|
||||
import { KnowledgeBase } from "../components/KnowledgeBase";
|
||||
import { useMcpCallLogs } from "../hooks/useSpacetimeDB";
|
||||
import { mcpReviewApi } from "../lib/mcp-review-api";
|
||||
import { makeMcpReviewApi } from "../lib/mcp-review-api";
|
||||
import type { McpCallLogRow } from "../types";
|
||||
|
||||
type Tab = "calls" | "knowledge" | "analytics";
|
||||
|
||||
export default function App() {
|
||||
const user = useUser();
|
||||
const user = useUser({ or: process.env.NODE_ENV === "development" ? "redirect" : "return-null" });
|
||||
const [selectedRow, setSelectedRow] = useState<McpCallLogRow | null>(null);
|
||||
const [showAddQa, setShowAddQa] = useState(false);
|
||||
const [tab, setTab] = useState<Tab>("calls");
|
||||
@ -52,9 +52,6 @@ export default function App() {
|
||||
<p className="text-sm text-gray-500 mb-1">
|
||||
You are signed in as {user.displayName ?? user.primaryEmail}, but your account is not approved.
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
An admin needs to set <code className="bg-gray-100 px-1 rounded">approved: true</code> in your client read-only metadata.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -65,7 +62,16 @@ export default function App() {
|
||||
? rows.find(r => r.id === selectedRow.id) ?? selectedRow
|
||||
: null;
|
||||
|
||||
const reviewedBy = user.displayName ?? user.primaryEmail ?? "unknown";
|
||||
const currentUser = user;
|
||||
const reviewedBy = currentUser.displayName ?? currentUser.primaryEmail ?? "unknown";
|
||||
|
||||
async function getApi() {
|
||||
const { accessToken, refreshToken } = await currentUser.getAuthJson();
|
||||
const authHeaders: Record<string, string> = {};
|
||||
if (accessToken) authHeaders["x-stack-access-token"] = accessToken;
|
||||
if (refreshToken) authHeaders["x-stack-refresh-token"] = refreshToken;
|
||||
return makeMcpReviewApi(authHeaders);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
@ -127,12 +133,9 @@ export default function App() {
|
||||
<AddManualQa
|
||||
onClose={() => setShowAddQa(false)}
|
||||
onSave={(question, answer, publish) => {
|
||||
mcpReviewApi.addManual({
|
||||
question,
|
||||
answer,
|
||||
publish,
|
||||
reviewedBy,
|
||||
}).catch(() => { /* errors are surfaced by UI state */ });
|
||||
getApi()
|
||||
.then(api => api.addManual({ question, answer, publish, reviewedBy }))
|
||||
.catch(() => { /* errors are surfaced by UI state */ });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -154,13 +157,14 @@ export default function App() {
|
||||
allRows={rows}
|
||||
onClose={() => setSelectedRow(null)}
|
||||
onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => {
|
||||
mcpReviewApi.updateCorrection({
|
||||
correlationId,
|
||||
correctedQuestion,
|
||||
correctedAnswer,
|
||||
publish,
|
||||
reviewedBy,
|
||||
}).catch(() => { /* errors are surfaced by UI state */ });
|
||||
getApi()
|
||||
.then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish, reviewedBy }))
|
||||
.catch(() => { /* errors are surfaced by UI state */ });
|
||||
}}
|
||||
onMarkReviewed={(correlationId) => {
|
||||
getApi()
|
||||
.then(api => api.markReviewed({ correlationId, reviewedBy }))
|
||||
.catch(() => { /* errors are surfaced by UI state */ });
|
||||
}}
|
||||
/>
|
||||
</aside>
|
||||
@ -173,18 +177,14 @@ export default function App() {
|
||||
<KnowledgeBase
|
||||
rows={rows}
|
||||
onSave={(correlationId, question, answer, publish) => {
|
||||
mcpReviewApi.updateCorrection({
|
||||
correlationId,
|
||||
correctedQuestion: question,
|
||||
correctedAnswer: answer,
|
||||
publish,
|
||||
reviewedBy,
|
||||
}).catch(() => { /* errors are surfaced by UI state */ });
|
||||
getApi()
|
||||
.then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish, reviewedBy }))
|
||||
.catch(() => { /* errors are surfaced by UI state */ });
|
||||
}}
|
||||
onDelete={(correlationId) => {
|
||||
mcpReviewApi.delete({
|
||||
correlationId,
|
||||
}).catch(() => { /* errors are surfaced by UI state */ });
|
||||
getApi()
|
||||
.then(api => api.delete({ correlationId }))
|
||||
.catch(() => { /* errors are surfaced by UI state */ });
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
|
||||
11
apps/internal-tool/src/app/handler/[...stack]/page.tsx
Normal file
11
apps/internal-tool/src/app/handler/[...stack]/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { StackHandler } from "@stackframe/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Handler() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) return null;
|
||||
return <StackHandler fullPage />;
|
||||
}
|
||||
@ -61,9 +61,6 @@ export default function QuestionsPage() {
|
||||
{row.publishedAt && (
|
||||
<span>{format(toDate(row.publishedAt), "MMM d, yyyy")}</span>
|
||||
)}
|
||||
{row.humanReviewedBy && (
|
||||
<span>Reviewed by {row.humanReviewedBy}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
@ -28,13 +28,15 @@ function CopyButton({ text }: { text: string }) {
|
||||
|
||||
// ─── Main Component ────────────────────────────────────
|
||||
|
||||
export function CallLogDetail({ row, allRows, onClose, onSaveCorrection }: {
|
||||
export function CallLogDetail({ row, allRows, onClose, onSaveCorrection, onMarkReviewed }: {
|
||||
row: McpCallLogRow;
|
||||
allRows: McpCallLogRow[];
|
||||
onClose: () => void;
|
||||
onSaveCorrection?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => void;
|
||||
onMarkReviewed?: (correlationId: string) => void;
|
||||
}) {
|
||||
const [showReplay, setShowReplay] = useState(false);
|
||||
const isReviewed = row.humanReviewedAt != null;
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
@ -44,8 +46,26 @@ export function CallLogDetail({ row, allRows, onClose, onSaveCorrection }: {
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-900">Call Detail</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-gray-900">Call Detail</h2>
|
||||
{isReviewed && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 text-green-800"
|
||||
title={`Reviewed ${row.humanReviewedAt ? format(toDate(row.humanReviewedAt), "PPpp") : ""}${row.humanReviewedBy ? ` by ${row.humanReviewedBy}` : ""}`}
|
||||
>
|
||||
✓ Reviewed{row.humanReviewedBy ? ` by ${row.humanReviewedBy}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isReviewed && onMarkReviewed && (
|
||||
<button
|
||||
onClick={() => onMarkReviewed(row.correlationId)}
|
||||
className="px-2.5 py-1 text-xs font-medium text-green-700 bg-green-50 rounded-md hover:bg-green-100 border border-green-200"
|
||||
>
|
||||
Mark as reviewed
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowReplay(true)}
|
||||
className="px-2.5 py-1 text-xs font-medium text-purple-600 bg-purple-50 rounded-md hover:bg-purple-100"
|
||||
|
||||
@ -2,8 +2,12 @@ import { useEffect, useState, useRef } from "react";
|
||||
import { DbConnection, type EventContext, type SubscriptionEventContext } from "../module_bindings";
|
||||
import type { McpCallLogRow } from "../types";
|
||||
|
||||
const HOST = process.env.NEXT_PUBLIC_SPACETIMEDB_HOST ?? "";
|
||||
const DB_NAME = process.env.NEXT_PUBLIC_SPACETIMEDB_DB_NAME ?? "";
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PLACEHOLDER = "REPLACE_ME";
|
||||
const rawHost = process.env.NEXT_PUBLIC_SPACETIMEDB_HOST;
|
||||
const rawDbName = process.env.NEXT_PUBLIC_SPACETIMEDB_DB_NAME;
|
||||
const HOST = (!rawHost || rawHost === PLACEHOLDER) ? (IS_DEV ? "ws://localhost:8139" : "") : rawHost;
|
||||
const DB_NAME = (!rawDbName || rawDbName === PLACEHOLDER) ? (IS_DEV ? "stack-auth-llm" : "") : rawDbName;
|
||||
const TOKEN_KEY = `spacetimedb_${HOST}/${DB_NAME}/auth_token`;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
@ -1,16 +1,30 @@
|
||||
const API_URL = process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_STACK_PROJECT_ID;
|
||||
const PUBLISHABLE_CLIENT_KEY = process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY;
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PLACEHOLDER = "REPLACE_ME";
|
||||
|
||||
async function post(path: string, body: unknown): Promise<void> {
|
||||
function envOrDevDefault(value: string | undefined, devDefault: string): string {
|
||||
if (!value || value === PLACEHOLDER) {
|
||||
return IS_DEV ? devDefault : "";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const PORT_PREFIX = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
|
||||
const API_URL = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${PORT_PREFIX}02`);
|
||||
const PROJECT_ID = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal");
|
||||
const PUBLISHABLE_CLIENT_KEY = envOrDevDefault(
|
||||
process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
"this-publishable-client-key-is-for-local-development-only",
|
||||
);
|
||||
|
||||
async function post(path: string, body: unknown, authHeaders: Record<string, string>): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/api/latest/internal/mcp-review/${path}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-stack-access-type": "client",
|
||||
"x-stack-project-id": PROJECT_ID ?? "",
|
||||
"x-stack-publishable-client-key": PUBLISHABLE_CLIENT_KEY ?? "",
|
||||
"x-stack-project-id": PROJECT_ID,
|
||||
"x-stack-publishable-client-key": PUBLISHABLE_CLIENT_KEY,
|
||||
...authHeaders,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
@ -20,25 +34,27 @@ async function post(path: string, body: unknown): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpReviewApi = {
|
||||
markReviewed: (body: { correlationId: string; reviewedBy: string }) =>
|
||||
post("mark-reviewed", body),
|
||||
export function makeMcpReviewApi(authHeaders: Record<string, string>) {
|
||||
return {
|
||||
markReviewed: (body: { correlationId: string; reviewedBy: string }) =>
|
||||
post("mark-reviewed", body, authHeaders),
|
||||
|
||||
updateCorrection: (body: {
|
||||
correlationId: string;
|
||||
correctedQuestion: string;
|
||||
correctedAnswer: string;
|
||||
publish: boolean;
|
||||
reviewedBy: string;
|
||||
}) => post("update-correction", body),
|
||||
updateCorrection: (body: {
|
||||
correlationId: string;
|
||||
correctedQuestion: string;
|
||||
correctedAnswer: string;
|
||||
publish: boolean;
|
||||
reviewedBy: string;
|
||||
}) => post("update-correction", body, authHeaders),
|
||||
|
||||
addManual: (body: {
|
||||
question: string;
|
||||
answer: string;
|
||||
publish: boolean;
|
||||
reviewedBy: string;
|
||||
}) => post("add-manual", body),
|
||||
addManual: (body: {
|
||||
question: string;
|
||||
answer: string;
|
||||
publish: boolean;
|
||||
reviewedBy: string;
|
||||
}) => post("add-manual", body, authHeaders),
|
||||
|
||||
delete: (body: { correlationId: string }) =>
|
||||
post("delete", body),
|
||||
};
|
||||
delete: (body: { correlationId: string }) =>
|
||||
post("delete", body, authHeaders),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,20 +1,37 @@
|
||||
import { StackClientApp } from "@stackframe/react";
|
||||
|
||||
const hostedComponentsUrl = process.env.NEXT_PUBLIC_STACK_HOSTED_COMPONENTS_URL;
|
||||
const internalToolUrl = process.env.NEXT_PUBLIC_STACK_INTERNAL_TOOL_URL;
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PLACEHOLDER = "REPLACE_ME";
|
||||
|
||||
// In dev, fall back to the seeded "internal" project if env vars are placeholders.
|
||||
// In prod, the real values must be set via hosting platform env vars.
|
||||
function envOrDevDefault(value: string | undefined, devDefault: string): string {
|
||||
if (!value || value === PLACEHOLDER) {
|
||||
if (IS_DEV) return devDefault;
|
||||
throw new Error("Stack Auth env var is not configured. Set the NEXT_PUBLIC_STACK_* vars in .env.local or hosting platform env.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const portPrefix = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
|
||||
|
||||
const projectId = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_PROJECT_ID, "internal");
|
||||
const publishableClientKey = envOrDevDefault(
|
||||
process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
"this-publishable-client-key-is-for-local-development-only",
|
||||
);
|
||||
const apiUrl = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${portPrefix}02`);
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
projectId,
|
||||
publishableClientKey,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
baseUrl: process.env.NEXT_PUBLIC_STACK_API_URL,
|
||||
baseUrl: apiUrl,
|
||||
urls: {
|
||||
handler: `${hostedComponentsUrl}/handler`,
|
||||
signIn: `${hostedComponentsUrl}/handler/sign-in`,
|
||||
signUp: `${hostedComponentsUrl}/handler/sign-up`,
|
||||
afterSignIn: internalToolUrl,
|
||||
afterSignUp: internalToolUrl,
|
||||
afterSignOut: `${hostedComponentsUrl}/handler/sign-in`,
|
||||
handler: "/handler",
|
||||
afterSignIn: "/",
|
||||
afterSignUp: "/",
|
||||
afterSignOut: "/handler/sign-in",
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user