mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
parent
4792aa53f8
commit
c64fbf4fcd
2
.github/workflows/publish-docs.yaml
vendored
2
.github/workflows/publish-docs.yaml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectConfig" ADD COLUMN "signUpEnabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
@ -40,6 +40,7 @@ model ProjectConfig {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
allowLocalhost Boolean
|
||||
signUpEnabled Boolean @default(true)
|
||||
credentialEnabled Boolean
|
||||
magicLinkEnabled Boolean
|
||||
|
||||
|
||||
@ -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: []
|
||||
}
|
||||
})),
|
||||
},
|
||||
credentialEnabled: true,
|
||||
magicLinkEnabled: true,
|
||||
createTeamOnSignUp: false,
|
||||
},
|
||||
emailServiceConfig: {
|
||||
create: {
|
||||
proxiedEmailServiceConfig: {
|
||||
create: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
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!');
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -260,7 +263,7 @@ export const GET = createSmartRouteHandler({
|
||||
account_id: userInfo.accountId,
|
||||
email: userInfo.email,
|
||||
}],
|
||||
}
|
||||
},
|
||||
});
|
||||
await storeTokens();
|
||||
return {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -40,6 +40,7 @@ model ProjectConfig {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
allowLocalhost Boolean
|
||||
signUpEnabled Boolean @default(true)
|
||||
credentialEnabled Boolean
|
||||
magicLinkEnabled Boolean
|
||||
|
||||
|
||||
@ -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();
|
||||
@ -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}
|
||||
@ -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
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Auth Methods",
|
||||
title: "Auth Settings",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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);
|
||||
@ -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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DelayedInput, Form, Label, Switch, useToast } from "@stackframe/stack-ui";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DelayedInput, Form, Label, Switch, Typography, useToast } from "@stackframe/stack-ui";
|
||||
import { Settings } from "lucide-react";
|
||||
import React, { useEffect, useId, useState } from "react";
|
||||
import { FieldValues, useForm } from "react-hook-form";
|
||||
@ -8,7 +8,7 @@ import * as yup from "yup";
|
||||
|
||||
|
||||
export function SettingCard(props: {
|
||||
title: string,
|
||||
title?: string,
|
||||
description?: string,
|
||||
actions?: React.ReactNode,
|
||||
children?: React.ReactNode,
|
||||
@ -16,10 +16,12 @@ export function SettingCard(props: {
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && <CardDescription>{props.description}</CardDescription>}
|
||||
</CardHeader>
|
||||
{(props.title || props.description) && (
|
||||
<CardHeader>
|
||||
{props.title && <CardTitle>{props.title}</CardTitle>}
|
||||
{props.description && <CardDescription>{props.description}</CardDescription>}
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{props.accordion ?
|
||||
@ -45,6 +47,7 @@ export function SettingCard(props: {
|
||||
|
||||
export function SettingSwitch(props: {
|
||||
label: string | React.ReactNode,
|
||||
hint?: string | React.ReactNode,
|
||||
checked?: boolean,
|
||||
disabled?: boolean,
|
||||
onCheckedChange: (checked: boolean) => void | Promise<void>,
|
||||
@ -62,15 +65,18 @@ export function SettingSwitch(props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<Label className='px-2' htmlFor={id}>{props.label}</Label>
|
||||
{showActions && props.actions}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<Label className='px-2' htmlFor={id}>{props.label}</Label>
|
||||
{showActions && props.actions}
|
||||
</div>
|
||||
{props.hint && <Typography variant="secondary" type="footnote">{props.hint}</Typography>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ async function parseAuth(req: NextRequest): Promise<SmartRequestAuth | null> {
|
||||
if (!projectId) throw new KnownErrors.RequestTypeWithoutProjectId(requestType);
|
||||
|
||||
let projectAccessType: "key" | "internal-user-token";
|
||||
if (adminAccessToken) {
|
||||
if (adminAccessToken !== null) {
|
||||
const reason = await whyNotProjectAdmin(projectId, adminAccessToken);
|
||||
switch (reason) {
|
||||
case null: {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InternalProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
|
||||
import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
|
||||
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { camelCaseToSnakeCase } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import { expect } from "vitest";
|
||||
import { Context, Mailbox, NiceRequestInit, NiceResponse, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_ID, STACK_INTERNAL_PROJECT_SERVER_KEY, createMailbox, localRedirectUrl, niceFetch, updateCookiesFromResponse } from "../helpers";
|
||||
|
||||
@ -31,6 +31,7 @@ export type ProjectKeys = "no-project" | {
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
superSecretAdminKey?: string,
|
||||
adminAccessToken?: string,
|
||||
};
|
||||
|
||||
export const InternalProjectKeys = {
|
||||
@ -85,6 +86,7 @@ export async function niceBackendFetch(url: string | URL, options?: Omit<NiceReq
|
||||
"x-stack-publishable-client-key": projectKeys.publishableClientKey,
|
||||
"x-stack-secret-server-key": projectKeys.secretServerKey,
|
||||
"x-stack-super-secret-admin-key": projectKeys.superSecretAdminKey,
|
||||
'x-stack-admin-access-token': projectKeys.adminAccessToken,
|
||||
} : {},
|
||||
"x-stack-access-token": userAuth?.accessToken,
|
||||
"x-stack-refresh-token": userAuth?.refreshToken,
|
||||
@ -185,7 +187,7 @@ export namespace Auth {
|
||||
const mailbox = backendContext.value.mailbox;
|
||||
const sendSignInCodeRes = await sendSignInCode();
|
||||
const messages = await mailbox.fetchMessages();
|
||||
const message = messages.findLast((message) => message.subject === "Sign in to Stack Dashboard") ?? throwErr("Sign-in code message not found");
|
||||
const message = messages.findLast((message) => message.subject.includes("Sign in to")) ?? throwErr("Sign-in code message not found");
|
||||
const signInCode = message.body?.text.match(/http:\/\/localhost:12345\/some-callback-url\?code=([a-zA-Z0-9]+)/)?.[1] ?? throwErr("Sign-in URL not found");
|
||||
const response = await niceBackendFetch("/api/v1/auth/otp/sign-in", {
|
||||
method: "POST",
|
||||
@ -582,8 +584,8 @@ export namespace ApiKey {
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAndSetProjectKeys(adminAccessToken: string, body?: any) {
|
||||
const res = await ApiKey.create(adminAccessToken, body);
|
||||
export async function createAndSetProjectKeys(adminAccessToken?: string, body?: any) {
|
||||
const res = await ApiKey.create(adminAccessToken ?? (backendContext.value.projectKeys !== "no-project" && backendContext.value.projectKeys.adminAccessToken || throwErr("Missing adminAccessToken")), body);
|
||||
backendContext.set({ projectKeys: res.projectKeys });
|
||||
return res;
|
||||
}
|
||||
@ -599,13 +601,19 @@ export namespace Project {
|
||||
...body,
|
||||
},
|
||||
});
|
||||
expect(response).toMatchObject({
|
||||
status: 201,
|
||||
body: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
});
|
||||
return {
|
||||
createProjectResponse: response,
|
||||
projectId: response.body.id,
|
||||
projectId: response.body.id as string,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateCurrent(adminAccessToken: string, body: any) {
|
||||
export async function updateCurrent(adminAccessToken: string, body: Partial<InternalProjectsCrud["Admin"]["Create"]>) {
|
||||
const response = await niceBackendFetch(`/api/v1/projects/current`, {
|
||||
accessType: "admin",
|
||||
method: "PATCH",
|
||||
@ -620,19 +628,19 @@ export namespace Project {
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAndSetAdmin(body?: any) {
|
||||
export async function createAndGetAdminToken(body?: Partial<InternalProjectsCrud["Admin"]["Create"]>) {
|
||||
backendContext.set({
|
||||
projectKeys: InternalProjectKeys,
|
||||
});
|
||||
await Auth.Otp.signIn();
|
||||
const { projectId, createProjectResponse } = await Project.create(body);
|
||||
const adminAccessToken = backendContext.value.userAuth?.accessToken;
|
||||
|
||||
expect(adminAccessToken).toBeDefined();
|
||||
const { projectId, createProjectResponse } = await Project.create(body);
|
||||
|
||||
const createResult = await Project.create(body);
|
||||
backendContext.set({
|
||||
projectKeys: {
|
||||
projectId,
|
||||
projectId: createResult.projectId,
|
||||
},
|
||||
userAuth: null,
|
||||
});
|
||||
@ -643,6 +651,17 @@ export namespace Project {
|
||||
createProjectResponse,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAndSwitch(body?: Partial<InternalProjectsCrud["Admin"]["Create"]>) {
|
||||
const createResult = await Project.createAndGetAdminToken(body);
|
||||
backendContext.set({
|
||||
projectKeys: {
|
||||
projectId: createResult.projectId,
|
||||
adminAccessToken: createResult.adminAccessToken,
|
||||
},
|
||||
});
|
||||
return createResult;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Team {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
|
||||
import { it, updateCookiesFromResponse } from "../../../../../../helpers";
|
||||
import { Auth, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
import { ApiKey, Auth, Project, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
it("should return outer authorization code when inner callback url is valid", async ({ expect }) => {
|
||||
const response = await Auth.OAuth.getAuthorizationCode();
|
||||
@ -34,6 +34,33 @@ it("should fail when inner callback has invalid provider ID", async ({ expect })
|
||||
`);
|
||||
});
|
||||
|
||||
it("should fail when account is new and sign ups are disabled", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { sign_up_enabled: false, oauth_providers: [ { id: "facebook", type: "shared", enabled: true } ] } });
|
||||
await ApiKey.createAndSetProjectKeys();
|
||||
const getInnerCallbackUrlResponse = await Auth.OAuth.getInnerCallbackUrl();
|
||||
const cookie = updateCookiesFromResponse("", getInnerCallbackUrlResponse.authorizeResponse);
|
||||
const response = await niceBackendFetch(getInnerCallbackUrlResponse.innerCallbackUrl, {
|
||||
redirect: "manual",
|
||||
headers: {
|
||||
cookie,
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "SIGN_UP_NOT_ENABLED",
|
||||
"error": "Creation of new accounts is not enabled for this project. Please ask the project owner to enable it.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"set-cookie": <deleting cookie 'stack-oauth-inner-<stripped cookie name key>' at path '/'>,
|
||||
"x-stack-known-error": "SIGN_UP_NOT_ENABLED",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should fail when cookies are missing", async ({ expect }) => {
|
||||
const getInnerCallbackUrlResponse = await Auth.OAuth.getInnerCallbackUrl();
|
||||
const response = await niceBackendFetch(getInnerCallbackUrlResponse.innerCallbackUrl, {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { it } from "../../../../../../helpers";
|
||||
import { backendContext, Auth, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
import { Auth, Project, backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
it("should send a sign-in code per e-mail", async ({ expect }) => {
|
||||
await Auth.Otp.sendSignInCode();
|
||||
@ -40,6 +40,52 @@ it('should refuse to send a sign-in code if the redirect URL is invalid', async
|
||||
`);
|
||||
});
|
||||
|
||||
it("should refuse to sign up a new user if magic links are disabled on the project", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { magic_link_enabled: false } });
|
||||
const mailbox = backendContext.value.mailbox;
|
||||
const response = await niceBackendFetch("/api/v1/auth/otp/send-sign-in-code", {
|
||||
method: "POST",
|
||||
accessType: "client",
|
||||
body: {
|
||||
email: mailbox.emailAddress,
|
||||
callback_url: "http://localhost:12345/some-callback-url",
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 403,
|
||||
"body": "Magic link is not enabled for this project",
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should refuse to sign up a new user if sign ups are disabled on the project", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { sign_up_enabled: false, credential_enabled: false, magic_link_enabled: true } });
|
||||
const mailbox = backendContext.value.mailbox;
|
||||
const response = await niceBackendFetch("/api/v1/auth/otp/send-sign-in-code", {
|
||||
method: "POST",
|
||||
accessType: "client",
|
||||
body: {
|
||||
email: mailbox.emailAddress,
|
||||
callback_url: "http://localhost:12345/some-callback-url",
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "SIGN_UP_NOT_ENABLED",
|
||||
"error": "Creation of new accounts is not enabled for this project. Please ask the project owner to enable it.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"x-stack-known-error": "SIGN_UP_NOT_ENABLED",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it.todo("should create a team for newly created users if configured as such");
|
||||
|
||||
it.todo("should not create a team for newly created users if not configured as such");
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { it } from "../../../../../../helpers";
|
||||
import { Auth, backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
import { Auth, Project, backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
it("should sign up new users and sign in existing users", async ({ expect }) => {
|
||||
const res1 = await Auth.Otp.signIn();
|
||||
@ -39,6 +39,33 @@ it("should sign in users created with the server API", async ({ expect }) => {
|
||||
primary_email_auth_enabled: true,
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
const res2 = await Auth.Otp.signIn();
|
||||
expect(res2.signInResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"access_token": <stripped field 'access_token'>,
|
||||
"is_new_user": false,
|
||||
"refresh_token": <stripped field 'refresh_token'>,
|
||||
"user_id": "<stripped UUID>",
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should sign in users created with the server API even if sign up is disabled", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { sign_up_enabled: false, magic_link_enabled: true } });
|
||||
const response = await niceBackendFetch("/api/v1/users", {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
body: {
|
||||
primary_email: backendContext.value.mailbox.emailAddress,
|
||||
primary_email_auth_enabled: true,
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
const res2 = await Auth.Otp.signIn();
|
||||
expect(res2.signInResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
|
||||
import { it } from "../../../../../../helpers";
|
||||
import { Auth, backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
import { Auth, Project, backendContext, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
it("should sign up new users", async ({ expect }) => {
|
||||
const res = await Auth.Password.signUpWithEmail();
|
||||
@ -70,6 +70,58 @@ it("should not allow signing up with an e-mail that already exists", async ({ ex
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not allow signing up if credentials are disabled", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { credential_enabled: false } });
|
||||
const res2 = await niceBackendFetch("/api/v1/auth/password/sign-up", {
|
||||
method: "POST",
|
||||
accessType: "client",
|
||||
body: {
|
||||
email: backendContext.value.mailbox.emailAddress,
|
||||
password: generateSecureRandomString(),
|
||||
verification_callback_url: "http://localhost:12345",
|
||||
},
|
||||
});
|
||||
expect(res2).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "PASSWORD_AUTHENTICATION_NOT_ENABLED",
|
||||
"error": "Password authentication is not enabled for this project.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"x-stack-known-error": "PASSWORD_AUTHENTICATION_NOT_ENABLED",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not allow signing up if sign ups are disabled", async ({ expect }) => {
|
||||
await Project.createAndSwitch({ config: { sign_up_enabled: false, credential_enabled: true } });
|
||||
const res2 = await niceBackendFetch("/api/v1/auth/password/sign-up", {
|
||||
method: "POST",
|
||||
accessType: "client",
|
||||
body: {
|
||||
email: backendContext.value.mailbox.emailAddress,
|
||||
password: generateSecureRandomString(),
|
||||
verification_callback_url: "http://localhost:12345",
|
||||
},
|
||||
});
|
||||
expect(res2).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 400,
|
||||
"body": {
|
||||
"code": "SIGN_UP_NOT_ENABLED",
|
||||
"error": "Creation of new accounts is not enabled for this project. Please ask the project owner to enable it.",
|
||||
},
|
||||
"headers": Headers {
|
||||
"x-stack-known-error": "SIGN_UP_NOT_ENABLED",
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("cannot use empty password to sign up", async ({ expect }) => {
|
||||
const res = await niceBackendFetch("/api/v1/auth/password/sign-up", {
|
||||
method: "POST",
|
||||
|
||||
@ -126,7 +126,7 @@ describe("with admin access to a non-internal project", () => {
|
||||
});
|
||||
|
||||
it("creates, list, updates, revokes api keys", async ({ expect }) => {
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { createApiKeyResponse: response1 } = await ApiKey.create(adminAccessToken);
|
||||
expect(response1).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
|
||||
@ -57,7 +57,7 @@ it("lists all current projects (empty list)", async ({ expect }) => {
|
||||
it("creates a new project", async ({ expect }) => {
|
||||
backendContext.set({ projectKeys: InternalProjectClientKeys });
|
||||
await Auth.Otp.signIn();
|
||||
const result = await Project.createAndSetAdmin({
|
||||
const result = await Project.createAndGetAdminToken({
|
||||
display_name: "Test Project",
|
||||
});
|
||||
expect(result.createProjectResponse).toMatchInlineSnapshot(`
|
||||
@ -74,6 +74,7 @@ it("creates a new project", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -98,6 +99,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
is_production_mode: true,
|
||||
config: {
|
||||
allow_localhost: false,
|
||||
sign_up_enabled: false,
|
||||
credential_enabled: false,
|
||||
magic_link_enabled: true,
|
||||
},
|
||||
@ -107,15 +109,16 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"status": 201,
|
||||
"body": {
|
||||
"config": {
|
||||
"allow_localhost": true,
|
||||
"allow_localhost": false,
|
||||
"create_team_on_sign_up": false,
|
||||
"credential_enabled": true,
|
||||
"credential_enabled": false,
|
||||
"domains": [],
|
||||
"email_config": { "type": "shared" },
|
||||
"enabled_oauth_providers": [],
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": true,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": false,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -177,6 +180,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"type": "shared",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -214,6 +218,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -265,6 +270,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -318,6 +324,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -354,6 +361,7 @@ it("lists the current projects after creating a new project", async ({ expect })
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
|
||||
@ -41,6 +41,7 @@ it("gets current project (internal)", async ({ expect }) => {
|
||||
{ "id": "microsoft" },
|
||||
],
|
||||
"magic_link_enabled": true,
|
||||
"sign_up_enabled": true,
|
||||
},
|
||||
"display_name": "Stack Dashboard",
|
||||
"id": "internal",
|
||||
@ -52,7 +53,7 @@ it("gets current project (internal)", async ({ expect }) => {
|
||||
|
||||
it("creates and updates the basic project information of a project", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, {
|
||||
display_name: "Updated Project",
|
||||
description: "Updated description",
|
||||
@ -72,6 +73,7 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -89,10 +91,11 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
|
||||
it("updates the basic project configuration", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
allow_localhost: false,
|
||||
sign_up_enabled: false,
|
||||
credential_enabled: false,
|
||||
magic_link_enabled: true,
|
||||
},
|
||||
@ -111,6 +114,7 @@ it("updates the basic project configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": true,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": false,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -128,7 +132,7 @@ it("updates the basic project configuration", async ({ expect }) => {
|
||||
|
||||
it("updates the project domains configuration", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
domains: [{
|
||||
@ -156,6 +160,7 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -207,6 +212,7 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -224,7 +230,7 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
|
||||
it("updates the project email configuration", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
email_config: {
|
||||
@ -260,6 +266,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -310,6 +317,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -346,6 +354,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -382,6 +391,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -432,6 +442,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -449,7 +460,7 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
|
||||
it("updates the project email configuration with the wrong parameters", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
email_config: {
|
||||
@ -500,7 +511,7 @@ it("updates the project email configuration with the wrong parameters", async ({
|
||||
|
||||
it("updates the project oauth configuration", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
// create google oauth provider with shared type
|
||||
const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
@ -531,6 +542,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"type": "shared",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -575,6 +587,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"type": "shared",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -623,6 +636,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"type": "standard",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -700,6 +714,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"type": "shared",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
@ -756,6 +771,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"type": "shared",
|
||||
},
|
||||
],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
|
||||
@ -178,7 +178,7 @@ it("creates a team and manage users on the server", async ({ expect }) => {
|
||||
|
||||
it("should give team creator default permissions", async ({ expect }) => {
|
||||
backendContext.set({ projectKeys: InternalProjectKeys });
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
await ApiKey.createAndSetProjectKeys(adminAccessToken);
|
||||
|
||||
const { userId: userId1 } = await Auth.Password.signUpWithEmail({ password: 'test1234' });
|
||||
|
||||
@ -6,7 +6,7 @@ it("lists all the team permissions", async ({ expect }) => {
|
||||
backendContext.set({
|
||||
projectKeys: InternalProjectKeys,
|
||||
});
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
@ -74,7 +74,7 @@ it("lists all the team permissions", async ({ expect }) => {
|
||||
|
||||
it("creates, updates, and delete a new team permission", async ({ expect }) => {
|
||||
backendContext.set({ projectKeys: InternalProjectKeys });
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response1 = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
|
||||
@ -45,7 +45,7 @@ it("is not allowed to grant non-existing permission to a user on the server", as
|
||||
|
||||
it("can create a new permission and grant it to a user on the server", async ({ expect }) => {
|
||||
backendContext.set({ projectKeys: InternalProjectKeys });
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
// create a permission child
|
||||
await niceBackendFetch(`/api/v1/team-permission-definitions`, {
|
||||
@ -149,7 +149,7 @@ it("can create a new permission and grant it to a user on the server", async ({
|
||||
|
||||
it("can customize default team permissions", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndSetAdmin();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
const response1 = await niceBackendFetch(`/api/v1/team-permission-definitions`, {
|
||||
accessType: "admin",
|
||||
@ -194,6 +194,7 @@ it("can customize default team permissions", async ({ expect }) => {
|
||||
"id": "<stripped UUID>",
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "test" }],
|
||||
},
|
||||
|
||||
@ -46,6 +46,7 @@ export const projectsCrudServerReadSchema = yupObject({
|
||||
config: yupObject({
|
||||
id: schemaFields.projectConfigIdSchema.required(),
|
||||
allow_localhost: schemaFields.projectAllowLocalhostSchema.required(),
|
||||
sign_up_enabled: schemaFields.projectSignUpEnabledSchema.required(),
|
||||
credential_enabled: schemaFields.projectCredentialEnabledSchema.required(),
|
||||
magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(),
|
||||
oauth_providers: yupArray(oauthProviderSchema.required()).required(),
|
||||
@ -62,6 +63,7 @@ export const projectsCrudClientReadSchema = yupObject({
|
||||
id: schemaFields.projectIdSchema.required(),
|
||||
display_name: schemaFields.projectDisplayNameSchema.required(),
|
||||
config: yupObject({
|
||||
sign_up_enabled: schemaFields.projectSignUpEnabledSchema.required(),
|
||||
credential_enabled: schemaFields.projectCredentialEnabledSchema.required(),
|
||||
magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(),
|
||||
enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.required()).required(),
|
||||
@ -74,6 +76,7 @@ export const projectsCrudServerUpdateSchema = yupObject({
|
||||
description: schemaFields.projectDescriptionSchema.optional(),
|
||||
is_production_mode: schemaFields.projectIsProductionModeSchema.optional(),
|
||||
config: yupObject({
|
||||
sign_up_enabled: schemaFields.projectSignUpEnabledSchema.optional(),
|
||||
credential_enabled: schemaFields.projectCredentialEnabledSchema.optional(),
|
||||
magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.optional(),
|
||||
allow_localhost: schemaFields.projectAllowLocalhostSchema.optional(),
|
||||
|
||||
@ -661,6 +661,16 @@ const ProjectNotFound = createKnownErrorConstructor(
|
||||
(json: any) => [json.project_id] as const,
|
||||
);
|
||||
|
||||
const SignUpNotEnabled = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"SIGN_UP_NOT_ENABLED",
|
||||
() => [
|
||||
400,
|
||||
"Creation of new accounts is not enabled for this project. Please ask the project owner to enable it.",
|
||||
] as const,
|
||||
() => [] as const,
|
||||
);
|
||||
|
||||
const PasswordAuthenticationNotEnabled = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"PASSWORD_AUTHENTICATION_NOT_ENABLED",
|
||||
@ -1135,6 +1145,7 @@ export const KnownErrors = {
|
||||
UserNotFound,
|
||||
ApiKeyNotFound,
|
||||
ProjectNotFound,
|
||||
SignUpNotEnabled,
|
||||
PasswordAuthenticationNotEnabled,
|
||||
EmailPasswordMismatch,
|
||||
RedirectUrlNotWhitelisted,
|
||||
|
||||
@ -148,6 +148,7 @@ export const projectConfigIdSchema = yupString().meta({ openapiField: { descript
|
||||
export const projectAllowLocalhostSchema = yupBoolean().meta({ openapiField: { description: 'Whether localhost is allowed as a domain for this project. Should only be allowed in development mode', exampleValue: true } });
|
||||
export const projectCreateTeamOnSignUpSchema = yupBoolean().meta({ openapiField: { description: 'Whether a team should be created for each user that signs up', exampleValue: true } });
|
||||
export const projectMagicLinkEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether magic link authentication is enabled for this project', exampleValue: true } });
|
||||
export const projectSignUpEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether users can sign up new accounts, or whether they are only allowed to sign in to existing accounts. Regardless of this option, the server API can always create new users with the `POST /users` endpoint.', exampleValue: true } });
|
||||
export const projectCredentialEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether email password authentication is enabled for this project', exampleValue: true } });
|
||||
// Project OAuth config
|
||||
export const oauthIdSchema = yupString().oneOf(allProviders).meta({ openapiField: { description: `OAuth provider ID, one of ${allProviders.map(x => `\`${x}\``).join(', ')}`, exampleValue: 'google' } });
|
||||
|
||||
@ -21,7 +21,7 @@ export function SimpleTooltip(props: {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-w-60 text-center text-wrap">
|
||||
<div className="max-w-60 text-center text-wrap whitespace-pre-wrap">
|
||||
{props.tooltip}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@ -38,7 +38,7 @@ const CardHeader = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
className={cn("flex flex-col space-y-1.5 p-6 pb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@ -72,10 +72,21 @@ const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
<div ref={ref} className={cn("p-6", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardSubtitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h4
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground font-bold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
@ -88,4 +99,4 @@ const CardFooter = React.forwardRef<
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, ClickableCard, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
export { Card, ClickableCard, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, CardSubtitle };
|
||||
|
||||
@ -675,6 +675,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
return {
|
||||
id: crud.id,
|
||||
config: {
|
||||
signUpEnabled: crud.config.sign_up_enabled,
|
||||
credentialEnabled: crud.config.credential_enabled,
|
||||
magicLinkEnabled: crud.config.magic_link_enabled,
|
||||
oauthProviders: crud.config.enabled_oauth_providers.map((p) => ({
|
||||
@ -1788,6 +1789,7 @@ class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string
|
||||
isProductionMode: data.is_production_mode,
|
||||
config: {
|
||||
id: data.config.id,
|
||||
signUpEnabled: data.config.sign_up_enabled,
|
||||
credentialEnabled: data.config.credential_enabled,
|
||||
magicLinkEnabled: data.config.magic_link_enabled,
|
||||
allowLocalhost: data.config.allow_localhost,
|
||||
@ -2324,6 +2326,7 @@ function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): Pr
|
||||
sender_email: options.config.emailConfig.senderEmail,
|
||||
}
|
||||
),
|
||||
sign_up_enabled: options.config?.signUpEnabled,
|
||||
credential_enabled: options.config?.credentialEnabled,
|
||||
magic_link_enabled: options.config?.magicLinkEnabled,
|
||||
allow_localhost: options.config?.allowLocalhost,
|
||||
@ -2347,6 +2350,7 @@ function adminProjectCreateOptionsToCrud(options: AdminProjectCreateOptions): In
|
||||
type _______________PROJECT_CONFIG_______________ = never; // this is a marker for VSCode's outline view
|
||||
|
||||
export type ProjectConfig = {
|
||||
readonly signUpEnabled: boolean,
|
||||
readonly credentialEnabled: boolean,
|
||||
readonly magicLinkEnabled: boolean,
|
||||
readonly oauthProviders: OAuthProviderConfig[],
|
||||
@ -2357,6 +2361,7 @@ export type OAuthProviderConfig = {
|
||||
};
|
||||
|
||||
export type AdminProjectConfig = {
|
||||
readonly signUpEnabled: boolean,
|
||||
readonly credentialEnabled: boolean,
|
||||
readonly magicLinkEnabled: boolean,
|
||||
readonly allowLocalhost: boolean,
|
||||
@ -2407,6 +2412,7 @@ export type AdminProjectConfigUpdateOptions = {
|
||||
handlerPath: string,
|
||||
}[],
|
||||
oauthProviders?: AdminOAuthProviderConfig[],
|
||||
signUpEnabled?: boolean,
|
||||
credentialEnabled?: boolean,
|
||||
magicLinkEnabled?: boolean,
|
||||
allowLocalhost?: boolean,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user