mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Merge dev into update-oauth-docs
This commit is contained in:
commit
a15365bfe7
3
.github/workflows/restart-dev-and-test.yaml
vendored
3
.github/workflows/restart-dev-and-test.yaml
vendored
@ -17,6 +17,9 @@ env:
|
||||
jobs:
|
||||
restart-dev-and-test:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
STACK_DIRECT_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/stackframe"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"dev": "concurrently -n \"dev,codegen,prisma-studio\" -k \"next dev --turbopack --port 8102\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\"",
|
||||
"build": "pnpm run codegen && next build",
|
||||
"docker-build": "pnpm run codegen && next build --experimental-build-mode compile",
|
||||
"build-self-host-seed-script": "tsup --config prisma/tsup.config.ts",
|
||||
"build-self-host-migration-script": "tsup --config scripts/db-migrations.tsup.config.ts",
|
||||
"analyze-bundle": "ANALYZE_BUNDLE=1 pnpm run build",
|
||||
"start": "next start --port 8102",
|
||||
"codegen-prisma": "pnpm run prisma generate",
|
||||
@ -19,7 +19,7 @@
|
||||
"codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts",
|
||||
"codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run codegen-route-info",
|
||||
"codegen:watch": "concurrently -n \"prisma,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run watch-docs\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"",
|
||||
"psql-inner": "psql $STACK_DATABASE_CONNECTION_STRING",
|
||||
"psql-inner": "psql $(echo $STACK_DATABASE_CONNECTION_STRING | sed 's/\\?.*$//')",
|
||||
"psql": "pnpm run with-env pnpm run psql-inner",
|
||||
"prisma-studio": "pnpm run with-env prisma studio --port 8106 --browser none",
|
||||
"prisma": "pnpm run with-env prisma",
|
||||
@ -34,7 +34,7 @@
|
||||
"watch-docs": "pnpm run with-env bash -c 'tsx watch --clear-screen=false scripts/generate-openapi-fumadocs.ts && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs'",
|
||||
"generate-openapi-fumadocs": "pnpm run with-env tsx scripts/generate-openapi-fumadocs.ts",
|
||||
"generate-keys": "pnpm run with-env tsx scripts/generate-keys.ts",
|
||||
"db-seed-script": "pnpm run with-env tsx prisma/seed.ts",
|
||||
"db-seed-script": "pnpm run db:seed",
|
||||
"verify-data-integrity": "pnpm run with-env tsx scripts/verify-data-integrity.ts"
|
||||
},
|
||||
"prisma": {
|
||||
|
||||
@ -10,7 +10,7 @@ import { errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils
|
||||
|
||||
const globalPrisma = new PrismaClient();
|
||||
|
||||
async function seed() {
|
||||
export async function seed() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Optional default admin user
|
||||
@ -473,9 +473,11 @@ async function seed() {
|
||||
|
||||
process.env.STACK_SEED_MODE = 'true';
|
||||
|
||||
seed().catch(async (e) => {
|
||||
console.error(errorToNiceString(e));
|
||||
await globalPrisma.$disconnect();
|
||||
process.exit(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
}).finally(async () => await globalPrisma.$disconnect());
|
||||
if (require.main === module) {
|
||||
seed().catch(async (e) => {
|
||||
console.error(errorToNiceString(e));
|
||||
await globalPrisma.$disconnect();
|
||||
process.exit(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
}).finally(async () => await globalPrisma.$disconnect());
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { globalPrismaClient, globalPrismaSchema, sqlQuoteIdent } from "@/prisma-
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { execSync } from "child_process";
|
||||
import * as readline from 'readline';
|
||||
import { seed } from "../prisma/seed";
|
||||
|
||||
const dropSchema = async () => {
|
||||
await globalPrismaClient.$executeRaw(Prisma.sql`DROP SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} CASCADE`);
|
||||
@ -12,10 +13,6 @@ const dropSchema = async () => {
|
||||
await globalPrismaClient.$executeRaw(Prisma.sql`GRANT ALL ON SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} TO public`);
|
||||
};
|
||||
|
||||
const seed = async () => {
|
||||
execSync('pnpm run db-seed-script', { stdio: 'inherit' });
|
||||
};
|
||||
|
||||
const promptDropDb = async () => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
|
||||
@ -6,10 +6,10 @@ const customNoExternal = new Set([
|
||||
...Object.keys(packageJson.dependencies),
|
||||
]);
|
||||
|
||||
// tsup config to build the self-hosting seed script so it can be
|
||||
// tsup config to build the self-hosting migration script so it can be
|
||||
// run in the Docker container with no extra dependencies.
|
||||
export default defineConfig({
|
||||
entry: ['prisma/seed.ts'],
|
||||
entry: ['scripts/db-migrations.ts'],
|
||||
format: ['cjs'],
|
||||
outDir: 'dist',
|
||||
target: 'node22',
|
||||
@ -8,14 +8,17 @@ const TEST_DB_PREFIX = 'stack_auth_test_db';
|
||||
const getTestDbURL = (testDbName: string) => {
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const base = import.meta.env.STACK_DIRECT_DATABASE_CONNECTION_STRING.replace(/\/[^/]*$/, '');
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const query = import.meta.env.STACK_DIRECT_DATABASE_CONNECTION_STRING.split('?')[1] ?? '';
|
||||
return {
|
||||
full: `${base}/${testDbName}`,
|
||||
base,
|
||||
query,
|
||||
};
|
||||
};
|
||||
|
||||
const applySql = async (options: { sql: string | string[], fullDbURL: string }) => {
|
||||
const sql = postgres(options.fullDbURL);
|
||||
const applySql = async (options: { sql: string | string[], dbUrl: string }) => {
|
||||
const sql = postgres(options.dbUrl);
|
||||
|
||||
try {
|
||||
for (const query of Array.isArray(options.sql) ? options.sql : [options.sql]) {
|
||||
@ -31,12 +34,12 @@ const setupTestDatabase = async () => {
|
||||
const randomSuffix = Math.random().toString(16).substring(2, 12);
|
||||
const testDbName = `${TEST_DB_PREFIX}_${randomSuffix}`;
|
||||
const dbURL = getTestDbURL(testDbName);
|
||||
await applySql({ sql: `CREATE DATABASE ${testDbName}`, fullDbURL: dbURL.base });
|
||||
await applySql({ sql: `CREATE DATABASE ${testDbName}`, dbUrl: dbURL.base });
|
||||
|
||||
const prismaClient = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbURL.full,
|
||||
url: `${dbURL.full}?${dbURL.query}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -63,7 +66,7 @@ const teardownTestDatabase = async (prismaClient: PrismaClient, testDbName: stri
|
||||
`,
|
||||
`DROP DATABASE IF EXISTS ${testDbName}`
|
||||
],
|
||||
fullDbURL: dbURL.base
|
||||
dbUrl: dbURL.base
|
||||
});
|
||||
|
||||
// Wait a bit to ensure connections are terminated
|
||||
@ -234,12 +237,20 @@ import.meta.vitest?.test("applies migrations concurrently with 20 concurrent mig
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).toBe('test_value');
|
||||
}), {
|
||||
timeout: 40_000,
|
||||
timeout: 400_000,
|
||||
});
|
||||
|
||||
// TODO: this test, or a variant of it, might fail because the migrations waiting for locks are exhausting all
|
||||
// connections; the RUN_OUTSIDE_TRANSACTION_SENTINEL is then not able to run its own migration outside of the
|
||||
// transaction
|
||||
//
|
||||
// The fix would be to only *try* acquiring the migration lock when we apply a migration, and if it fails to acquire, we
|
||||
// wait *outside* of the transaction so it doesn't exhaust all connections
|
||||
import.meta.vitest?.test.todo("applies migrations concurrently with 20 concurrent migrations with RUN_OUTSIDE_TRANSACTION_SENTINEL");
|
||||
|
||||
|
||||
import.meta.vitest?.test("applies migration with a DB previously migrated with prisma", runTest(async ({ expect, prismaClient, dbURL }) => {
|
||||
await applySql({ sql: examplePrismaBasedInitQueries, fullDbURL: dbURL.full });
|
||||
await applySql({ sql: examplePrismaBasedInitQueries, dbUrl: dbURL.full });
|
||||
const result = await applyMigrations({ prismaClient, migrationFiles: examplePrismaBasedMigrationFiles, schema: 'public' });
|
||||
expect(result.newlyAppliedMigrationNames).toEqual(['20250314215050_age']);
|
||||
|
||||
@ -302,6 +313,7 @@ import.meta.vitest?.test("applies migration while running an interactive transac
|
||||
expect(result[0].name).toBe('test_value');
|
||||
}, {
|
||||
isolationLevel: undefined,
|
||||
timeout: 15_000,
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ RUN tsx ./scripts/generate-sdks.ts
|
||||
RUN turbo prune --scope=@stackframe/stack-backend --scope=@stackframe/stack-dashboard --docker
|
||||
|
||||
|
||||
|
||||
# Build stage
|
||||
FROM base AS builder
|
||||
|
||||
@ -55,7 +56,9 @@ ENV NEXT_CONFIG_OUTPUT=standalone
|
||||
RUN pnpm turbo run docker-build --filter=@stackframe/stack-backend... --filter=@stackframe/stack-dashboard...
|
||||
|
||||
# Build the self-host seed script
|
||||
RUN cd apps/backend && pnpm build-self-host-seed-script
|
||||
RUN cd apps/backend && pnpm build-self-host-migration-script
|
||||
|
||||
|
||||
|
||||
# Final image
|
||||
FROM node:${NODE_VERSION}-slim
|
||||
@ -68,14 +71,11 @@ RUN apt-get update && \
|
||||
apt-get install -y openssl socat && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
# Install Prisma CLI globally so we can run migrations on startup
|
||||
RUN npm i -g prisma
|
||||
|
||||
# Copy built backend
|
||||
COPY --from=builder --chown=node:node /app/apps/backend/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /app/apps/backend/.next/static ./apps/backend/.next/static
|
||||
COPY --from=builder --chown=node:node /app/apps/backend/prisma ./apps/backend/prisma
|
||||
COPY --from=builder --chown=node:node /app/apps/backend/dist/seed.js ./apps/backend
|
||||
COPY --from=builder --chown=node:node /app/apps/backend/dist ./apps/backend/dist
|
||||
|
||||
# Copy built dashboard
|
||||
COPY --from=builder --chown=node:node /app/apps/dashboard/.next/standalone ./
|
||||
|
||||
@ -37,7 +37,9 @@ if [ "$STACK_SKIP_MIGRATIONS" = "true" ]; then
|
||||
echo "Skipping migrations."
|
||||
else
|
||||
echo "Running migrations..."
|
||||
pnpm run db:migrate
|
||||
cd apps/backend
|
||||
node dist/db-migrations.js migrate
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
if [ "$STACK_SKIP_SEED_SCRIPT" = "true" ]; then
|
||||
@ -45,7 +47,7 @@ if [ "$STACK_SKIP_SEED_SCRIPT" = "true" ]; then
|
||||
else
|
||||
echo "Running seed script..."
|
||||
cd apps/backend
|
||||
node seed.js
|
||||
node dist/db-migrations.js seed
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { AlignLeft, ChevronDown, ExternalLink, FileText, Hash, Search, X } from 'lucide-react';
|
||||
import { AlignLeft, ChevronDown, ExternalLink, FileText, Hash, Search, Sparkles, X } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { useSidebar } from '../layouts/sidebar-context';
|
||||
|
||||
// Platform colors matching your theme
|
||||
const PLATFORM_COLORS = {
|
||||
@ -137,10 +138,28 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const sidebarContext = useSidebar();
|
||||
|
||||
// Available platforms for the dropdown
|
||||
const availablePlatforms = ['all', 'next', 'react', 'js', 'python', 'api'];
|
||||
|
||||
// Handle AI chat opening
|
||||
const handleOpenAIChat = () => {
|
||||
onOpenChange(false); // Close search dialog first
|
||||
if (!sidebarContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { toggleChat } = sidebarContext;
|
||||
|
||||
// Small delay to ensure search dialog closes smoothly
|
||||
setTimeout(() => {
|
||||
if (!sidebarContext.isChatOpen) {
|
||||
toggleChat();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -436,16 +455,30 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-fd-border px-3 py-2 text-xs text-fd-muted-foreground flex justify-between items-center">
|
||||
<span>Use ↑↓ to navigate, Enter to select, Esc to close</span>
|
||||
<span>
|
||||
{filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''}
|
||||
{selectedPlatformFilter !== 'all' && filteredResults.length > 0 && (
|
||||
<span className="ml-2 text-fd-primary">
|
||||
• {PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<div className="border-t border-fd-border px-3 py-2 text-xs text-fd-muted-foreground">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span>Use ↑↓ to navigate, Enter to select, Esc to close</span>
|
||||
<span>
|
||||
{filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''}
|
||||
{selectedPlatformFilter !== 'all' && filteredResults.length > 0 && (
|
||||
<span className="ml-2 text-fd-primary">
|
||||
• {PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* AI Chat Fallback */}
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<span className="text-fd-muted-foreground">Can't find what you're looking for?</span>
|
||||
<button
|
||||
onClick={handleOpenAIChat}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs rounded-md transition-all duration-300 ease-out relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
|
||||
>
|
||||
<Sparkles className="h-3 w-3 relative z-10" />
|
||||
<span className="font-medium relative z-10">Ask AI</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -45,20 +45,33 @@ function StackAuthLogo() {
|
||||
}
|
||||
|
||||
// AI Chat Toggle Button for Home Layout
|
||||
function HomeAIChatToggleButton() {
|
||||
function HomeAIChatToggleButton({ compact = false }: { compact?: boolean }) {
|
||||
const sidebarContext = useSidebar();
|
||||
const { isChatOpen, toggleChat } = sidebarContext || {
|
||||
isChatOpen: false,
|
||||
toggleChat: () => {},
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center justify-center transition-all duration-500 ease-out w-8 h-8 rounded-lg text-sm font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
|
||||
title={isChatOpen ? 'Close AI chat' : 'Open AI chat'}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 relative z-10" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center justify-center transition-all duration-500 ease-out w-8 h-8 rounded-lg text-sm font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
|
||||
className="flex items-center gap-2 transition-all duration-500 ease-out px-2 py-1 rounded-lg text-xs font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
|
||||
title={isChatOpen ? 'Close AI chat' : 'Open AI chat'}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 relative z-10" />
|
||||
<Sparkles className="h-3 w-3 relative z-10" />
|
||||
<span className="font-medium relative z-10">AI Chat</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -248,7 +261,7 @@ function HomeNavbar() {
|
||||
<ThemeToggle compact />
|
||||
|
||||
{/* Compact AI Chat Toggle */}
|
||||
<HomeAIChatToggleButton />
|
||||
<HomeAIChatToggleButton compact />
|
||||
|
||||
{/* Compact User Button */}
|
||||
<UserButton />
|
||||
|
||||
@ -81,18 +81,21 @@ function AIChatToggleButton() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toggleChat } = sidebarContext;
|
||||
const { toggleChat, isChatOpen } = sidebarContext;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-md w-8 h-8 text-xs transition-all duration-500 ease-out relative overflow-hidden',
|
||||
'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
|
||||
'flex items-center gap-2 rounded-md px-2 py-1 text-xs transition-all duration-500 ease-out relative overflow-hidden',
|
||||
isChatOpen
|
||||
? 'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
|
||||
: 'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
|
||||
)}
|
||||
onClick={toggleChat}
|
||||
title="AI Chat"
|
||||
>
|
||||
<Sparkles className="h-4 w-4 relative z-10" />
|
||||
<Sparkles className="h-3 w-3 relative z-10" />
|
||||
<span className="font-medium relative z-10">AI Chat</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -312,7 +315,7 @@ export function SharedHeader({
|
||||
</div>
|
||||
|
||||
{/* Right side - Mobile Menu and Search */}
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div className="flex items-center gap-3 relative z-10">
|
||||
{/* Search Bar - Responsive sizing */}
|
||||
{showSearch && (
|
||||
<>
|
||||
@ -344,7 +347,7 @@ export function SharedHeader({
|
||||
</div>
|
||||
|
||||
{/* User Button */}
|
||||
<div className="hidden md:block">
|
||||
<div className="hidden md:block ml-2">
|
||||
<UserButton />
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user