Skip to content

Commit

Permalink
Sign up restriction button on dashboard
Browse files Browse the repository at this point in the history
Fix #66, #74
  • Loading branch information
N2D4 committed Aug 10, 2024
1 parent 2fef40b commit fccbcf1
Show file tree
Hide file tree
Showing 37 changed files with 429 additions and 124 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
run:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_STACK_URL: http://localhost:8101
NEXT_PUBLIC_STACK_URL: http://localhost:8102
NEXT_PUBLIC_STACK_PROJECT_ID: internal
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: internal-project-publishable-client-key
STACK_SECRET_SERVER_KEY: internal-project-secret-server-key
Expand Down
1 change: 1 addition & 0 deletions apps/backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ STACK_SVIX_API_KEY=# enter the API key for the Svix webhook service here. Use `e

# Misc, optional
STACK_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here. Optional, don't specify it for default value
STACK_SETUP_ADMIN_GITHUB_ID=# enter the account ID of the admin user here, and after running the seed script they will be able to access the internal project in the Stack dashboard. Optional, don't specify it for default value
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ProjectConfig" ADD COLUMN "signUpEnabled" BOOLEAN NOT NULL DEFAULT true;
1 change: 1 addition & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ model ProjectConfig {
updatedAt DateTime @updatedAt
allowLocalhost Boolean
signUpEnabled Boolean @default(true)
credentialEnabled Boolean
magicLinkEnabled Boolean
Expand Down
142 changes: 90 additions & 52 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,110 @@
import { PrismaClient } from '@prisma/client';
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
const prisma = new PrismaClient();


async function seed() {
console.log('Seeding database...');
const oldProjects = await prisma.project.findUnique({

const oldProject = await prisma.project.findUnique({
where: {
id: 'internal',
},
});

if (oldProjects) {
console.log('Internal project already exists, skipping seeding');
return;
}

await prisma.project.upsert({
where: {
id: 'internal',
},
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'),
}],
let createdProject;
if (oldProject) {
console.log('Internal project already exists, skipping its creation');
} else {
createdProject = await prisma.project.upsert({
where: {
id: 'internal',
},
config: {
create: {
allowLocalhost: true,
oauthProviderConfigs: {
create: (['github', 'facebook', 'google', 'microsoft'] as const).map((id) => ({
id,
proxiedOAuthConfig: {
create: {
type: id.toUpperCase() as any,
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', 'facebook', 'google', 'microsoft'] as const).map((id) => ({
id,
proxiedOAuthConfig: {
create: {
type: id.toUpperCase() as any,
}
},
projectUserOAuthAccounts: {
create: []
}
})),
},
emailServiceConfig: {
create: {
proxiedEmailServiceConfig: {
create: {}
}
},
projectUserOAuthAccounts: {
create: []
}
})),
},
emailServiceConfig: {
create: {
proxiedEmailServiceConfig: {
create: {}
}
}
},
credentialEnabled: true,
magicLinkEnabled: true,
createTeamOnSignUp: false,
},
credentialEnabled: true,
magicLinkEnabled: true,
createTeamOnSignUp: false,
},
},
},
update: {},
});
console.log('Internal project created');
update: {},
});
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',
primaryEmailVerified: false,
authWithEmail: false,
serverMetadata: {
managedProjectIds: [
"internal",
],
},
projectUserOAuthAccounts: {
create: [{
providerAccountId: adminGithubId,
projectConfigId: createdProject?.configId ?? oldProject?.configId ?? throwErr('No internal project config ID found'),
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');
}

console.log('Seeding complete!');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ export const GET = createSmartRouteHandler({

// ========================== sign up user ==========================

const newAccount = await usersCrudHandlers.serverCreate({
if (!project.config.sign_up_enabled) {
throw new KnownErrors.SignUpNotEnabled();
}
const newAccount = await usersCrudHandlers.adminCreate({
project,
data: {
display_name: userInfo.displayName,
Expand All @@ -260,7 +263,7 @@ export const GET = createSmartRouteHandler({
account_id: userInfo.accountId,
email: userInfo.email,
}],
}
},
});
await storeTokens();
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const POST = createSmartRouteHandler({

const userPrisma = usersPrisma.length > 0 ? usersPrisma[0] : null;
const isNewUser = !userPrisma;
if (isNewUser && !project.config.sign_up_enabled) {
throw new KnownErrors.SignUpNotEnabled();
}

let userObj: Pick<NonNullable<typeof userPrisma>, "projectUserId" | "displayName" | "primaryEmail"> | null = userPrisma;
if (!userObj) {
// TODO this should be in the same transaction as the read above
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/app/api/v1/auth/password/sign-up/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const POST = createSmartRouteHandler({
throw passwordError;
}

if (!project.config.sign_up_enabled) {
throw new KnownErrors.SignUpNotEnabled();
}

const createdUser = await usersCrudHandlers.adminCreate({
project,
data: {
Expand Down
11 changes: 6 additions & 5 deletions apps/backend/src/app/api/v1/internal/projects/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand
id: generateUuid(),
displayName: data.display_name,
description: data.description,
isProductionMode: data.is_production_mode || false,
isProductionMode: data.is_production_mode ?? false,
config: {
create: {
credentialEnabled: data.config?.credential_enabled || true,
magicLinkEnabled: data.config?.magic_link_enabled || false,
allowLocalhost: data.config?.allow_localhost || true,
createTeamOnSignUp: data.config?.create_team_on_sign_up || false,
signUpEnabled: data.config?.sign_up_enabled ?? true,
credentialEnabled: data.config?.credential_enabled ?? true,
magicLinkEnabled: data.config?.magic_link_enabled ?? false,
allowLocalhost: data.config?.allow_localhost ?? true,
createTeamOnSignUp: data.config?.create_team_on_sign_up ?? false,
domains: data.config?.domains ? {
create: data.config.domains.map(item => ({
domain: item.domain,
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/app/api/v1/projects/current/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
isProductionMode: data.is_production_mode,
config: {
update: {
signUpEnabled: data.config?.sign_up_enabled,
credentialEnabled: data.config?.credential_enabled,
magicLinkEnabled: data.config?.magic_link_enabled,
allowLocalhost: data.config?.allow_localhost,
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/lib/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function projectPrismaToCrud(
config: {
id: prisma.config.id,
allow_localhost: prisma.config.allowLocalhost,
sign_up_enabled: prisma.config.signUpEnabled,
credential_enabled: prisma.config.credentialEnabled,
magic_link_enabled: prisma.config.magicLinkEnabled,
create_team_on_sign_up: prisma.config.createTeamOnSignUp,
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ model ProjectConfig {
updatedAt DateTime @updatedAt
allowLocalhost Boolean
signUpEnabled Boolean @default(true)
credentialEnabled Boolean
magicLinkEnabled Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
import { ProviderSettingSwitch } from "./providers";
import { CardSubtitle } from "../../../../../../../../../packages/stack-ui/dist/components/ui/card";

export default function PageClient() {
const stackAdminApp = useAdminApp();
Expand All @@ -12,7 +13,10 @@ export default function PageClient() {

return (
<PageLayout title="Auth Methods" description="Configure how users can sign in to your app">
<SettingCard title="Email Authentication" description="Email address based sign in.">
<SettingCard>
<CardSubtitle>
Email-based
</CardSubtitle>
<SettingSwitch
label="Email password authentication"
checked={project.config.credentialEnabled}
Expand All @@ -35,9 +39,9 @@ export default function PageClient() {
});
}}
/>
</SettingCard>

<SettingCard title="OAuth Providers" description={`The "Sign in with XYZ" buttons on your app.`}>
<CardSubtitle className="mt-2">
SSO (OAuth)
</CardSubtitle>
{allProviders.map((id) => {
const provider = oauthProviders.find((provider) => provider.id === id);
return <ProviderSettingSwitch
Expand All @@ -57,6 +61,20 @@ export default function PageClient() {
/>;
})}
</SettingCard>
<SettingCard title="Settings">
<SettingSwitch
label="Allow everyone to create accounts"
checked={project.config.signUpEnabled}
onCheckedChange={async (checked) => {
await project.update({
config: {
signUpEnabled: checked,
},
});
}}
hint="When disabled, only users with an existing account can sign in. You can still create new accounts manually on the dashboard."
/>
</SettingCard>
</PageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PageClient from "./page-client";

export const metadata = {
title: "Auth Methods",
title: "Auth Settings",
};

export default function Page() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function ProviderSettingSwitch(props: Props) {
<div className="flex items-center gap-2">
{toTitle(props.id)}
{isShared && enabled &&
<SimpleTooltip tooltip="Shared keys are automatically created by Stack, but contain Stack's logo on the OAuth sign-in page.">
<SimpleTooltip tooltip={"Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page.\n\nYou should replace these before you go into production."}>
<Badge variant="secondary">Shared keys</Badge>
</SimpleTooltip>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,10 @@ export default function PageClient() {
});
}}
label="Allow all localhost callbacks for development"
hint={<>
When enabled, allow access from all localhost URLs by default. This makes development easier but <b>should be disabled in production.</b>
</>}
/>


<Typography variant="secondary" type="footnote">
When enabled, allow access from all localhost URLs by default. This makes development easier but <b>should be disabled in production.</b>
</Typography>
</SettingCard>
</PageLayout>
);
Expand Down
2 changes: 0 additions & 2 deletions apps/dashboard/src/components/data-table/elements/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,10 @@ export function DataTableToolbar<TData>({

const rowModel = table.getCoreRowModel();
const rows = rowModel.rows.map(row => Object.fromEntries(row.getAllCells().map(c => [c.column.id, renderCellValue(c)]).filter(([_, v]) => v !== undefined)));
console.log(table.getAllColumns());
if (rows.length === 0) {
alert("No data to export");
return;
}
console.log(rows);
const csv = generateCsv(csvConfig)(rows as any);
download(csvConfig)(csv);
}}
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useRouter } from "@/components/router";
import { useFromNow } from '@/hooks/use-from-now';
import { AdminProject } from '@stackframe/stack';
import { CardDescription, CardFooter, CardHeader, CardTitle, ClickableCard, Typography } from '@stackframe/stack-ui';
import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ClickableCard, Typography } from '@stackframe/stack-ui';

export function ProjectCard({ project }: { project: AdminProject }) {
const createdAt = useFromNow(project.createdAt);
Expand All @@ -14,7 +14,7 @@ export function ProjectCard({ project }: { project: AdminProject }) {
<CardTitle className="normal-case">{project.displayName}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
<CardFooter className="flex justify-between">
<CardFooter className="flex justify-between mt-2">
<Typography type='label' variant='secondary'>
{project.userCount} users
</Typography>
Expand Down
Loading

0 comments on commit fccbcf1

Please sign in to comment.