stack/apps/backend/prisma/seed.ts
BilalG1 479d5432ba
fix dev failing tests (#955)
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- RECURSEML_SUMMARY:START -->
## High-level PR Summary
This PR fixes database schema issues related to Prisma migrations by
setting the correct `search_path` before applying auto-migrations and
removing the `SchemaMigration` table from the CI workflow to ensure
proper schema comparison during migration checks.

⏱️ Estimated Review Time: 5-15 minutes

<details>
<summary>💡 Review Order Suggestion</summary>

| Order | File Path |
|-------|-----------|
| 1 | `apps/backend/src/auto-migrations/index.tsx` |
| 2 | `.github/workflows/check-prisma-migrations.yaml` |
</details>



[![Need help? Join our
Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U)


[![Analyze latest
changes](d1ed7a31db/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=955)
<!-- RECURSEML_SUMMARY:END -->
<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Fixes Prisma migration schema issues and a string concatenation error
in `getUserInfo`.
> 
>   - **Database Schema**:
> - Removes `SchemaMigration` table in
`.github/workflows/check-prisma-migrations.yaml` to ensure correct
schema comparison.
> - Sets `STACK_SEED_MODE` to `'true'` in `seed()` in `seed.ts` for
consistent seeding behavior.
>   - **Bug Fix**:
> - Fixes string concatenation error in `getUserInfo` in
`myFunctions.ts` by using `+` instead of `obj.displayName`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 5b664a0fe7. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Corrected user info message to properly display the user’s name in the
example app.

* **Chores**
* CI workflow now clears auto-generated migration metadata before
comparing schema changes to reduce false positives.
* Seeding now explicitly sets a seed-mode environment flag and no longer
auto-executes on direct invocation, enabling more controlled runs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-15 13:57:28 -07:00

475 lines
14 KiB
TypeScript

/* eslint-disable no-restricted-syntax */
import { usersCrudHandlers } from '@/app/api/latest/users/crud';
import { overrideEnvironmentConfigOverride } from '@/lib/config';
import { grantTeamPermission, updatePermissionDefinition } from '@/lib/permissions';
import { createOrUpdateProjectWithLegacyConfig, getProject } from '@/lib/projects';
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from '@/lib/tenancies';
import { getPrismaClientForTenancy, globalPrismaClient } from '@/prisma-client';
import { PrismaClient } from '@prisma/client';
import { errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils/errors';
const globalPrisma = new PrismaClient();
export async function seed() {
process.env.STACK_SEED_MODE = 'true';
console.log('Seeding database...');
// Optional default admin user
const adminEmail = process.env.STACK_SEED_INTERNAL_PROJECT_USER_EMAIL;
const adminPassword = process.env.STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD;
const adminInternalAccess = process.env.STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS === 'true';
const adminGithubId = process.env.STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID;
// dashboard settings
const dashboardDomain = process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL;
const oauthProviderIds = process.env.STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS?.split(',') ?? [];
const otpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED === 'true';
const signUpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED === 'true';
const allowLocalhost = process.env.STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST === 'true';
const emulatorEnabled = process.env.STACK_EMULATOR_ENABLED === 'true';
const emulatorProjectId = process.env.STACK_EMULATOR_PROJECT_ID;
const apiKeyId = '3142e763-b230-44b5-8636-aa62f7489c26';
const defaultUserId = '33e7c043-d2d1-4187-acd3-f91b5ed64b46';
const internalTeamId = 'a23e1b7f-ab18-41fc-9ee6-7a9ca9fa543c';
const emulatorAdminUserId = '63abbc96-5329-454a-ba56-e0460173c6c1';
const emulatorAdminTeamId = '5a0c858b-d9e9-49d4-9943-8ce385d86428';
let internalProject = await getProject('internal');
if (!internalProject) {
internalProject = await createOrUpdateProjectWithLegacyConfig({
type: 'create',
projectId: 'internal',
data: {
display_name: 'Stack Dashboard',
owner_team_id: internalTeamId,
description: 'Stack\'s admin dashboard',
is_production_mode: false,
config: {
allow_localhost: true,
oauth_providers: oauthProviderIds.map((id) => ({
id: id as any,
type: 'shared',
})),
sign_up_enabled: signUpEnabled,
credential_enabled: true,
magic_link_enabled: otpEnabled,
},
},
});
console.log('Internal project created');
}
const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
const internalPrisma = await getPrismaClientForTenancy(internalTenancy);
internalProject = await createOrUpdateProjectWithLegacyConfig({
projectId: 'internal',
branchId: DEFAULT_BRANCH_ID,
type: 'update',
data: {
config: {
create_team_on_sign_up: true,
sign_up_enabled: signUpEnabled,
magic_link_enabled: otpEnabled,
allow_localhost: allowLocalhost,
client_team_creation_enabled: true,
domains: [
...(dashboardDomain && new URL(dashboardDomain).hostname !== 'localhost' ? [{ domain: dashboardDomain, handler_path: '/handler' }] : []),
...Object.values(internalTenancy.config.domains.trustedDomains)
.filter((d) => d.baseUrl !== dashboardDomain && d.baseUrl)
.map((d) => ({ domain: d.baseUrl || throwErr('Domain base URL is required'), handler_path: d.handlerPath })),
],
},
},
});
await overrideEnvironmentConfigOverride({
projectId: 'internal',
branchId: DEFAULT_BRANCH_ID,
environmentConfigOverrideOverride: {
dataVault: {
stores: {
'neon-connection-strings': {
displayName: 'Neon Connection Strings',
}
}
},
payments: {
catalogs: {
plans: {
displayName: "Plans",
}
},
products: {
team: {
catalogId: "plans",
displayName: "Team",
customerType: "team",
serverOnly: false,
stackable: false,
prices: {
monthly: {
USD: "49",
interval: [1, "month"] as any,
serverOnly: false
}
},
includedItems: {
dashboard_admins: {
quantity: 3,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
growth: {
catalogId: "plans",
displayName: "Growth",
customerType: "team",
serverOnly: false,
stackable: false,
prices: {
monthly: {
USD: "299",
interval: [1, "month"] as any,
serverOnly: false
}
},
includedItems: {
dashboard_admins: {
quantity: 5,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
free: {
catalogId: "plans",
displayName: "Free",
customerType: "team",
serverOnly: false,
stackable: false,
prices: "include-by-default",
includedItems: {
dashboard_admins: {
quantity: 1,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
"extra-admins": {
catalogId: "plans",
displayName: "Extra Admins",
customerType: "team",
serverOnly: false,
stackable: true,
prices: {
monthly: {
USD: "49",
interval: [1, "month"] as any,
serverOnly: false
}
},
includedItems: {
dashboard_admins: {
quantity: 1,
repeat: "never",
expires: "when-purchase-expires"
}
},
isAddOnTo: {
team: true,
growth: true,
}
}
},
items: {
dashboard_admins: {
displayName: "Dashboard Admins",
customerType: "team"
}
},
}
}
});
await updatePermissionDefinition(
globalPrismaClient,
internalPrisma,
{
oldId: "team_member",
scope: "team",
tenancy: internalTenancy,
data: {
id: "team_member",
description: "1",
contained_permission_ids: ["$read_members"],
}
}
);
const updatedInternalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
await updatePermissionDefinition(
globalPrismaClient,
internalPrisma,
{
oldId: "team_admin",
scope: "team",
tenancy: updatedInternalTenancy,
data: {
id: "team_admin",
description: "2",
contained_permission_ids: ["$read_members", "$remove_members", "$update_team"],
}
}
);
const internalTeam = await internalPrisma.team.findUnique({
where: {
tenancyId_teamId: {
tenancyId: internalTenancy.id,
teamId: internalTeamId,
},
},
});
if (!internalTeam) {
await internalPrisma.team.create({
data: {
tenancyId: internalTenancy.id,
teamId: internalTeamId,
displayName: 'Internal Team',
mirroredProjectId: 'internal',
mirroredBranchId: DEFAULT_BRANCH_ID,
},
});
console.log('Internal team created');
}
const keySet = {
publishableClientKey: process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
secretServerKey: process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set'),
superSecretAdminKey: process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set'),
};
await globalPrisma.apiKeySet.upsert({
where: { projectId_id: { projectId: 'internal', id: apiKeyId } },
update: {
...keySet,
},
create: {
id: apiKeyId,
projectId: 'internal',
description: "Internal API key set",
expiresAt: new Date('2099-12-31T23:59:59Z'),
...keySet,
}
});
console.log('Updated internal API key set');
// Create optional default admin user if credentials are provided.
// This user will be able to login to the dashboard with both email/password and magic link.
if ((adminEmail && adminPassword) || adminGithubId) {
const oldAdminUser = await internalPrisma.projectUser.findFirst({
where: {
mirroredProjectId: 'internal',
mirroredBranchId: DEFAULT_BRANCH_ID,
projectUserId: defaultUserId
}
});
if (oldAdminUser) {
console.log(`Admin user already exists, skipping creation`);
} else {
const newUser = await internalPrisma.projectUser.create({
data: {
displayName: 'Administrator (created by seed script)',
projectUserId: defaultUserId,
tenancyId: internalTenancy.id,
mirroredProjectId: 'internal',
mirroredBranchId: DEFAULT_BRANCH_ID,
}
});
if (adminInternalAccess) {
await internalPrisma.teamMember.create({
data: {
tenancyId: internalTenancy.id,
teamId: internalTeamId,
projectUserId: defaultUserId,
},
});
}
if (adminEmail && adminPassword) {
await usersCrudHandlers.adminUpdate({
tenancy: internalTenancy,
user_id: defaultUserId,
data: {
password: adminPassword,
primary_email: adminEmail,
primary_email_auth_enabled: true,
},
});
console.log(`Added admin user with email ${adminEmail}`);
}
if (adminGithubId) {
const githubAccount = await internalPrisma.projectUserOAuthAccount.findFirst({
where: {
tenancyId: internalTenancy.id,
configOAuthProviderId: 'github',
providerAccountId: adminGithubId,
}
});
if (githubAccount) {
console.log(`GitHub account already exists, skipping creation`);
} else {
await internalPrisma.projectUserOAuthAccount.create({
data: {
tenancyId: internalTenancy.id,
projectUserId: newUser.projectUserId,
configOAuthProviderId: 'github',
providerAccountId: adminGithubId
}
});
await internalPrisma.authMethod.create({
data: {
tenancyId: internalTenancy.id,
projectUserId: newUser.projectUserId,
oauthAuthMethod: {
create: {
projectUserId: newUser.projectUserId,
configOAuthProviderId: 'github',
providerAccountId: adminGithubId,
}
}
}
});
console.log(`Added admin user with GitHub ID ${adminGithubId}`);
}
}
}
await grantTeamPermission(internalPrisma, {
tenancy: internalTenancy,
teamId: internalTeamId,
userId: defaultUserId,
permissionId: "team_admin",
});
}
if (emulatorEnabled) {
if (!emulatorProjectId) {
throw new Error('STACK_EMULATOR_PROJECT_ID is not set');
}
const emulatorTeam = await internalPrisma.team.findUnique({
where: {
tenancyId_teamId: {
tenancyId: internalTenancy.id,
teamId: emulatorAdminTeamId,
},
},
});
if (!emulatorTeam) {
await internalPrisma.team.create({
data: {
tenancyId: internalTenancy.id,
teamId: emulatorAdminTeamId,
displayName: 'Emulator Team',
mirroredProjectId: "internal",
mirroredBranchId: DEFAULT_BRANCH_ID,
},
});
console.log('Created emulator team');
}
const existingUser = await internalPrisma.projectUser.findFirst({
where: {
mirroredProjectId: 'internal',
mirroredBranchId: DEFAULT_BRANCH_ID,
projectUserId: emulatorAdminUserId,
}
});
if (existingUser) {
console.log('Emulator user already exists, skipping creation');
} else {
const newEmulatorUser = await internalPrisma.projectUser.create({
data: {
displayName: 'Local Emulator User',
projectUserId: emulatorAdminUserId,
tenancyId: internalTenancy.id,
mirroredProjectId: 'internal',
mirroredBranchId: DEFAULT_BRANCH_ID,
}
});
await internalPrisma.teamMember.create({
data: {
tenancyId: internalTenancy.id,
teamId: emulatorAdminTeamId,
projectUserId: newEmulatorUser.projectUserId,
},
});
await usersCrudHandlers.adminUpdate({
tenancy: internalTenancy,
user_id: newEmulatorUser.projectUserId,
data: {
password: 'LocalEmulatorPassword',
primary_email: 'local-emulator@stack-auth.com',
primary_email_auth_enabled: true,
},
});
console.log('Created emulator user');
}
const existingProject = await internalPrisma.project.findUnique({
where: {
id: emulatorProjectId,
},
});
if (existingProject) {
console.log('Emulator project already exists, skipping creation');
} else {
await createOrUpdateProjectWithLegacyConfig({
projectId: emulatorProjectId,
type: 'create',
data: {
display_name: 'Emulator Project',
owner_team_id: emulatorAdminTeamId,
config: {
allow_localhost: true,
create_team_on_sign_up: false,
client_team_creation_enabled: false,
passkey_enabled: true,
oauth_providers: oauthProviderIds.map((id) => ({
id: id as any,
type: 'shared',
})),
}
},
});
console.log('Created emulator project');
}
}
console.log('Seeding complete!');
}