Merge seed scripts (#354)

This commit is contained in:
Zai Shi 2024-12-03 16:53:17 +01:00 committed by GitHub
parent 7431a3c2fb
commit d5d28b2dd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 283 additions and 336 deletions

View File

@ -24,7 +24,8 @@ jobs:
with:
images: ${{ secrets.DOCKER_REPO }}/server
tags: |
type=ref,event=branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch,enable=${{ github.ref != 'refs/heads/main' }}
type=sha,prefix=
type=match,pattern=\d.\d.\d
@ -56,5 +57,5 @@ jobs:
context: .
file: ./docker/server/Dockerfile
push: ${{ steps.push-condition.outputs.should_push }}
tags: ${{ steps.meta.outputs.tags == 'main' && 'latest' || steps.meta.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -3,6 +3,17 @@ NEXT_PUBLIC_STACK_API_URL=# the base URL of Stack's backend/API. For local devel
NEXT_PUBLIC_STACK_DASHBOARD_URL=# the URL of Stack's dashboard. For local development, this is `http://localhost:8101`; for the managed service, this is `https://app.stack-auth.com`.
STACK_SERVER_SECRET=# a random, unguessable secret key generated by `pnpm generate-keys`
# seed script settings
STACK_SEED_SIGN_UP_ENABLED=# true to add OTP auth to the dashboard when seeding
STACK_SEED_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
STACK_SEED_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
STACK_SEED_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
STACK_SEED_CLIENT_TEAM_CREATION=# true to allow the users of the internal project to create teams
STACK_SEED_USER_EMAIL=# default user added to the dashboard
STACK_SEED_USER_PASSWORD=# default user's password, paired with STACK_SEED_USER_EMAIL
STACK_SEED_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
STACK_SEED_USER_GITHUB_ID=# add github oauth id to the default user
# OAuth mock provider settings
STACK_OAUTH_MOCK_URL=# enter the URL of the mock OAuth provider here. For local development, use `http://localhost:8114`.

View File

@ -2,6 +2,18 @@ NEXT_PUBLIC_STACK_API_URL=http://localhost:8102
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
STACK_SEED_SIGN_UP_ENABLED=true
STACK_SEED_OTP_ENABLED=true
STACK_SEED_ALLOW_LOCALHOST=true
STACK_SEED_OAUTH_PROVIDERS=github,spotify,google,microsoft
STACK_SEED_CLIENT_TEAM_CREATION=true
STACK_SEED_USER_INTERNAL_ACCESS=true
NEXT_PUBLIC_STACK_PROJECT_ID=project-id-from-stack-dashboard
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
STACK_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
STACK_OAUTH_MOCK_URL=http://localhost:8114
STACK_GITHUB_CLIENT_ID=MOCK

View File

@ -1,202 +0,0 @@
/* eslint-disable no-restricted-syntax */
import { PrismaClient } from '@prisma/client';
import { hashPassword } from "@stackframe/stack-shared/dist/utils/hashes";
const prisma = new PrismaClient();
async function seed() {
console.log('Seeding database...');
// Optional default admin user
const adminEmail = process.env.STACK_DEFAULT_DASHBOARD_USER_EMAIL;
const adminPassword = process.env.STACK_DEFAULT_DASHBOARD_USER_PASSWORD;
const adminInternalAccess = process.env.STACK_DEFAULT_DASHBOARD_USER_INTERNAL_ACCESS === 'true';
// Optionally disable sign up for "internal" project
const signUpEnabled = process.env.STACK_INTERNAL_SIGN_UP_ENABLED === 'true';
// Optionally add a custom domain to the internal project
const dashboardDomain = process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL;
const allowLocalhost = process.env.STACK_DASHBOARD_ALLOW_LOCALHOST === 'true';
let internalProject = await prisma.project.findUnique({
where: {
id: 'internal',
},
include: {
config: true,
}
});
if (!internalProject) {
console.log('No existing internal project found, creating...');
internalProject = await prisma.project.create({
data: {
id: 'internal',
displayName: 'Stack Dashboard',
description: 'Stack\'s admin dashboard',
isProductionMode: false,
apiKeySets: {
create: [{
description: "Internal API key set",
// These keys must match the values used in the Stack dashboard env to be able to login via the UI.
publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
superSecretAdminKey: process.env.STACK_SUPER_SECRET_ADMIN_KEY,
expiresAt: new Date('2099-12-31T23:59:59Z'),
}],
},
config: {
create: {
allowLocalhost: true,
signUpEnabled, // see STACK_SIGN_UP_DISABLED var above
emailServiceConfig: {
create: {
proxiedEmailServiceConfig: {
create: {}
}
}
},
createTeamOnSignUp: false,
clientTeamCreationEnabled: false,
authMethodConfigs: {
create: [
{
passwordConfig: {
create: {},
}
},
],
}
}
}
},
include: {
config: true,
}
});
console.log('Internal project created');
}
// 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) {
const oldAdminUser = await prisma.projectUser.findFirst({
where: {
projectId: 'internal',
contactChannels: {
some: {
type: 'EMAIL',
value: adminEmail,
}
}
}
});
if (oldAdminUser) {
console.log(`User with email ${adminEmail} already exists, skipping creation`);
} else {
console.log(`No existing admin user with email ${adminEmail} found, creating...`);
await prisma.$transaction(async (tx) => {
const newUser = await tx.projectUser.create({
data: {
projectId: 'internal',
serverMetadata: adminInternalAccess
? { managedProjectIds: ['internal'] }
: undefined,
}
});
await tx.contactChannel.create({
data: {
projectUserId: newUser.projectUserId,
projectId: 'internal',
type: 'EMAIL' as const,
value: adminEmail as string,
isVerified: false,
isPrimary: 'TRUE',
usedForAuth: 'TRUE',
}
});
const passwordConfig = await tx.passwordAuthMethodConfig.findFirstOrThrow({
where: {
projectConfigId: (internalProject as any).configId
},
include: {
authMethodConfig: true,
}
});
await tx.authMethod.create({
data: {
projectId: 'internal',
projectConfigId: (internalProject as any).configId,
projectUserId: newUser.projectUserId,
authMethodConfigId: passwordConfig.authMethodConfigId,
passwordAuthMethod: {
create: {
passwordHash: await hashPassword(adminPassword),
projectUserId: newUser.projectUserId,
}
}
}
});
});
console.log('Initial admin user created: ', adminEmail);
}
}
if (internalProject.config.allowLocalhost !== allowLocalhost) {
console.log('Updating allowLocalhost for internal project: ', allowLocalhost);
await prisma.project.update({
where: { id: 'internal' },
data: {
config: {
update: {
allowLocalhost,
}
}
}
});
}
if (dashboardDomain) {
const url = new URL(dashboardDomain);
if (url.hostname !== 'localhost') {
console.log('Adding trusted domain for internal project: ', dashboardDomain);
await prisma.projectDomain.upsert({
where: {
projectConfigId_domain: {
projectConfigId: internalProject.configId,
domain: dashboardDomain,
}
},
update: {},
create: {
projectConfigId: internalProject.configId,
domain: dashboardDomain,
handlerPath: '/',
}
});
} else if (!allowLocalhost) {
throw new Error('Cannot use localhost as a trusted domain if STACK_DASHBOARD_ALLOW_LOCALHOST is not set to true');
}
}
console.log('Seeding complete!');
}
seed().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
}).finally(async () => await prisma.$disconnect());

View File

@ -1,141 +1,260 @@
import { prismaClient } from '@/prisma-client';
/* eslint-disable no-restricted-syntax */
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
import { hashPassword } from "@stackframe/stack-shared/dist/utils/hashes";
const prisma = new PrismaClient();
async function seed() {
console.log('Seeding database...');
const oldProject = await prisma.project.findUnique({
// Optional default admin user
const adminEmail = process.env.STACK_SEED_USER_EMAIL;
const adminPassword = process.env.STACK_SEED_USER_PASSWORD;
const adminInternalAccess = process.env.STACK_SEED_USER_INTERNAL_ACCESS === 'true';
const adminGithubId = process.env.STACK_SEED_USER_GITHUB_ID;
// dashboard settings
const dashboardDomain = process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL;
const oauthProviderIds = process.env.STACK_SEED_OAUTH_PROVIDERS?.split(',') ?? [];
const otpEnabled = process.env.STACK_SEED_OTP_ENABLED === 'true';
const signUpEnabled = process.env.STACK_SEED_SIGN_UP_ENABLED === 'true';
const allowLocalhost = process.env.STACK_SEED_ALLOW_LOCALHOST === 'true';
const clientTeamCreation = process.env.STACK_SEED_CLIENT_TEAM_CREATION === 'true';
let internalProject = await prisma.project.findUnique({
where: {
id: 'internal',
},
include: {
config: true,
}
});
if (oldProject) {
console.log('Internal project already exists, skipping its creation');
} else {
await prismaClient.$transaction(async (tx) => {
const createdProject = await prisma.project.upsert({
where: {
id: 'internal',
if (!internalProject) {
internalProject = await prisma.project.create({
data: {
id: 'internal',
displayName: 'Stack Dashboard',
description: 'Stack\'s admin dashboard',
isProductionMode: false,
apiKeySets: {
create: [{
description: "Internal API key set",
// These keys must match the values used in the Stack dashboard env to be able to login via the UI.
publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || throwErr('NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY is not set'),
secretServerKey: process.env.STACK_SECRET_SERVER_KEY || throwErr('STACK_SECRET_SERVER_KEY is not set'),
superSecretAdminKey: process.env.STACK_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SUPER_SECRET_ADMIN_KEY is not set'),
expiresAt: new Date('2099-12-31T23:59:59Z'),
}],
},
create: {
id: 'internal',
displayName: 'Stack Dashboard',
description: 'Stack\'s admin dashboard',
isProductionMode: false,
apiKeySets: {
create: [{
description: "Internal API key set",
publishableClientKey: "this-publishable-client-key-is-for-local-development-only",
secretServerKey: "this-secret-server-key-is-for-local-development-only",
superSecretAdminKey: "this-super-secret-admin-key-is-for-local-development-only",
expiresAt: new Date('2099-12-31T23:59:59Z'),
}],
},
config: {
create: {
allowLocalhost: true,
oauthProviderConfigs: {
create: (['github', 'spotify', 'google', 'microsoft'] as const).map((id) => ({
id,
proxiedOAuthConfig: {
create: {
type: id.toUpperCase() as any,
}
},
projectUserOAuthAccounts: {
create: []
}
})),
},
emailServiceConfig: {
create: {
proxiedEmailServiceConfig: {
create: {}
}
}
},
createTeamOnSignUp: false,
clientTeamCreationEnabled: true,
},
},
},
update: {},
});
await prisma.projectConfig.update({
where: {
id: createdProject.configId,
},
data: {
authMethodConfigs: {
create: [
{
otpConfig: {
create: {
contactChannelType: 'EMAIL',
}
}
},
{
passwordConfig: {
config: {
create: {
allowLocalhost: true,
signUpEnabled,
emailServiceConfig: {
create: {
proxiedEmailServiceConfig: {
create: {}
}
},
...(['github', 'spotify', 'google', 'microsoft'] as const).map((id) => ({
oauthProviderConfig: {
connect: {
projectConfigId_id: {
id,
projectConfigId: createdProject.configId,
}
}
},
createTeamOnSignUp: false,
clientTeamCreationEnabled: clientTeamCreation,
authMethodConfigs: {
create: [
{
passwordConfig: {
create: {},
}
},
...(otpEnabled ? [{
otpConfig: {
create: {
contactChannelType: 'EMAIL'
},
}
}]: []),
],
},
oauthProviderConfigs: {
create: oauthProviderIds.map((id) => ({
id,
proxiedOAuthConfig: {
create: {
type: id.toUpperCase() as any,
}
},
projectUserOAuthAccounts: {
create: []
}
}))
],
},
})),
},
}
}
});
console.log('Internal project created');
// eslint-disable-next-line no-restricted-syntax
const adminGithubId = process.env.STACK_SETUP_ADMIN_GITHUB_ID;
if (adminGithubId) {
console.log("Found admin GitHub ID in environment variables, creating admin user...");
await prisma.projectUser.upsert({
where: {
projectId_projectUserId: {
projectId: 'internal',
projectUserId: '707156c3-0d1b-48cf-b09d-3171c7f613d5',
},
},
create: {
projectId: 'internal',
projectUserId: '707156c3-0d1b-48cf-b09d-3171c7f613d5',
displayName: 'Admin user generated by seed script',
serverMetadata: {
managedProjectIds: [
"internal",
"12345678-1234-1234-1234-123456789abc", // intentionally invalid project ID to ensure we don't rely on project IDs being valid
],
},
projectUserOAuthAccounts: {
create: [{
providerAccountId: adminGithubId,
projectConfigId: createdProject.configId,
oauthProviderConfigId: 'github',
}],
},
},
update: {},
});
console.log(`Admin user created (if it didn't already exist)`);
} else {
console.log('No admin GitHub ID found in environment variables, skipping admin user creation');
},
include: {
config: true,
}
});
await prisma.projectConfig.update({
where: {
id: internalProject.configId,
},
data: {
authMethodConfigs: {
create: [
...oauthProviderIds.map((id) => ({
oauthProviderConfig: {
connect: {
projectConfigId_id: {
id,
projectConfigId: (internalProject as any).configId,
}
}
}
}))
],
},
}
});
console.log('Internal project created');
}
// 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) {
await prisma.$transaction(async (tx) => {
const oldAdminUser = await tx.projectUser.findFirst({
where: {
projectId: 'internal',
projectUserId: '33e7c043-d2d1-4187-acd3-f91b5ed64b46'
}
});
if (oldAdminUser) {
console.log(`User with email ${adminEmail} already exists, skipping creation`);
} else {
const newUser = await tx.projectUser.create({
data: {
projectUserId: '33e7c043-d2d1-4187-acd3-f91b5ed64b46',
projectId: 'internal',
serverMetadata: adminInternalAccess
? { managedProjectIds: ['internal'] }
: undefined,
}
});
if (adminEmail && adminPassword) {
await tx.contactChannel.create({
data: {
projectUserId: newUser.projectUserId,
projectId: 'internal',
type: 'EMAIL' as const,
value: adminEmail as string,
isVerified: false,
isPrimary: 'TRUE',
usedForAuth: 'TRUE',
}
});
const passwordConfig = await tx.passwordAuthMethodConfig.findFirstOrThrow({
where: {
projectConfigId: (internalProject as any).configId
},
include: {
authMethodConfig: true,
}
});
await tx.authMethod.create({
data: {
projectId: 'internal',
projectConfigId: (internalProject as any).configId,
projectUserId: newUser.projectUserId,
authMethodConfigId: passwordConfig.authMethodConfigId,
passwordAuthMethod: {
create: {
passwordHash: await hashPassword(adminPassword),
projectUserId: newUser.projectUserId,
}
}
}
});
console.log(`Added admin user with email ${adminEmail}`);
}
if (adminGithubId) {
const githubConfig = await tx.oAuthProviderConfig.findUnique({
where: {
projectConfigId_id: {
projectConfigId: (internalProject as any).configId,
id: 'github'
}
}
});
if (!githubConfig) {
throw new Error('GitHub OAuth provider config not found');
}
await tx.projectUserOAuthAccount.create({
data: {
projectId: 'internal',
projectConfigId: (internalProject as any).configId,
projectUserId: newUser.projectUserId,
oauthProviderConfigId: 'github',
providerAccountId: adminGithubId
}
});
console.log(`Added admin user with GitHub ID ${adminGithubId}`);
}
}
});
}
if (internalProject.config.allowLocalhost !== allowLocalhost) {
console.log('Updating allowLocalhost for internal project: ', allowLocalhost);
await prisma.project.update({
where: { id: 'internal' },
data: {
config: {
update: {
allowLocalhost,
}
}
}
});
}
if (dashboardDomain) {
const url = new URL(dashboardDomain);
if (url.hostname !== 'localhost') {
console.log('Adding trusted domain for internal project: ', dashboardDomain);
await prisma.projectDomain.upsert({
where: {
projectConfigId_domain: {
projectConfigId: internalProject.configId,
domain: dashboardDomain,
}
},
update: {},
create: {
projectConfigId: internalProject.configId,
domain: dashboardDomain,
handlerPath: '/',
}
});
} else if (!allowLocalhost) {
throw new Error('Cannot use localhost as a trusted domain if STACK_SEED_ALLOW_LOCALHOST is not set to true');
}
}
console.log('Seeding complete!');
@ -145,5 +264,5 @@ seed().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
// eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/return-await
// eslint-disable-next-line @typescript-eslint/no-misused-promises
}).finally(async () => await prisma.$disconnect());

View File

@ -3,7 +3,7 @@ import { defineConfig } from 'tsup';
// tsup config to build the self-hosting seed script so it can be
// run in the Docker container with no extra dependencies.
export default defineConfig({
entry: ['prisma/seed-self-host.ts'],
entry: ['prisma/seed.ts'],
format: ['cjs'],
outDir: 'dist',
target: 'node22',

View File

@ -1,6 +1,6 @@
NEXT_PUBLIC_STACK_API_URL=# https://your-backend-domain.com
NEXT_PUBLIC_STACK_DASHBOARD_URL=# https://your-dashboard-domain.com, this will be added as a trusted domain by the seed script
STACK_DASHBOARD_ALLOW_LOCALHOST=# if true, the internal dashboard project will allow localhost as a trusted domain. Do not set this to true in production.
STACK_SEED_ALLOW_LOCALHOST=# if true, the internal dashboard project will allow localhost as a trusted domain. Do not set this to true in production.
STACK_DATABASE_CONNECTION_STRING=# postgres connection string with pooler
STACK_DIRECT_DATABASE_CONNECTION_STRING=# postgres direct connection string
@ -10,10 +10,16 @@ NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# a secure random string
STACK_SECRET_SERVER_KEY=# a secure random string
STACK_SERVER_SECRET=# a 32 bytes base64url encoded random string, used for JWT encryption. can be generated with `pnpm generate-keys`
# set these if you want to use the default admin user
STACK_DEFAULT_DASHBOARD_USER_EMAIL=# your admin email
STACK_DEFAULT_DASHBOARD_USER_PASSWORD=# your admin password
STACK_DEFAULT_DASHBOARD_USER_INTERNAL_ACCESS=# if true, the default admin user will have access to the internal dashboard project
# seed script settings
STACK_SEED_SIGN_UP_ENABLED=# true to add OTP auth to the dashboard when seeding
STACK_SEED_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
STACK_SEED_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
STACK_SEED_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
STACK_SEED_CLIENT_TEAM_CREATION=# true to allow the users of the internal project to create teams
STACK_SEED_USER_EMAIL=# default user added to the dashboard
STACK_SEED_USER_PASSWORD=# default user's password, paired with STACK_SEED_USER_EMAIL
STACK_SEED_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
STACK_SEED_USER_GITHUB_ID=# add github oauth id to the default user
# Set these if you want to use any email functionality
STACK_EMAIL_HOST=

View File

@ -1,6 +1,5 @@
NEXT_PUBLIC_STACK_API_URL=http://localhost:8102
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101
STACK_DASHBOARD_ALLOW_LOCALHOST=true
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:password@host.docker.internal:5432/stackframe
STACK_DIRECT_DATABASE_CONNECTION_STRING=postgres://postgres:password@host.docker.internal:5432/stackframe
@ -10,9 +9,10 @@ NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-loca
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
STACK_DEFAULT_DASHBOARD_USER_EMAIL=admin@email.com
STACK_DEFAULT_DASHBOARD_USER_PASSWORD=password
STACK_DEFAULT_DASHBOARD_USER_INTERNAL_ACCESS=false
STACK_SEED_ALLOW_LOCALHOST=true
STACK_SEED_USER_EMAIL=admin@email.com
STACK_SEED_USER_PASSWORD=password
STACK_SEED_USER_INTERNAL_ACCESS=false
STACK_RUN_MIGRATIONS=true
STACK_RUN_SEED_SCRIPT=true

View File

@ -70,7 +70,7 @@ RUN npm i -g prisma
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-self-host.js ./apps/backend
COPY --from=builder --chown=node:node /app/apps/backend/dist/seed.js ./apps/backend
# Copy built dashboard
COPY --from=builder --chown=node:node /app/apps/dashboard/.next/standalone ./

View File

@ -12,7 +12,7 @@ fi
if [ "$STACK_RUN_SEED_SCRIPT" = "true" ]; then
echo "Running seed script..."
cd apps/backend
node seed-self-host.js
node seed.js
cd ../..
else
echo "Skipping seed script."