mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
add advanced setting for enabling plain HTTP domains (#403)
This commit is contained in:
parent
6ba15f352d
commit
064b52267c
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
*.untracked.*
|
||||
node-compile-cache/
|
||||
|
||||
|
||||
*.cpuprofile
|
||||
|
||||
|
||||
|
||||
@ -3,14 +3,15 @@ import { FormDialog } from "@/components/form-dialog";
|
||||
import { InputField, SwitchField } from "@/components/form-fields";
|
||||
import { SettingCard, SettingSwitch } from "@/components/settings";
|
||||
import { AdminDomainConfig, AdminProject } from "@stackframe/stack";
|
||||
import { urlSchema } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { isValidUrl } from "@stackframe/stack-shared/dist/utils/urls";
|
||||
import { createUrlIfValid, isValidUrl } from "@stackframe/stack-shared/dist/utils/urls";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionCell, ActionDialog, Alert, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from "@stackframe/stack-ui";
|
||||
import React from "react";
|
||||
import * as yup from "yup";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
|
||||
const DOMAIN_REGEX = /^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/;
|
||||
|
||||
function EditDialog(props: {
|
||||
open?: boolean,
|
||||
onOpenChange?: (open: boolean) => void,
|
||||
@ -30,9 +31,13 @@ function EditDialog(props: {
|
||||
}
|
||||
)) {
|
||||
const domainFormSchema = yup.object({
|
||||
domain: urlSchema
|
||||
.url("Invalid URL")
|
||||
.transform((value) => 'https://' + value)
|
||||
domain: yup.string()
|
||||
.test('is-domain', "Invalid Domain", (domain) => {
|
||||
if (!domain) {
|
||||
return true;
|
||||
}
|
||||
const urlIfValid = createUrlIfValid(`https://${domain}`);
|
||||
return !!urlIfValid && urlIfValid.hostname === domain; })
|
||||
.notOneOf(
|
||||
props.domains
|
||||
.filter((_, i) => (props.type === 'update' && i !== props.editIndex) || props.type === 'create')
|
||||
@ -44,6 +49,7 @@ function EditDialog(props: {
|
||||
.matches(/^\//, "Handler path must start with /")
|
||||
.defined(),
|
||||
addWww: yup.boolean(),
|
||||
allowInsecureHttp: yup.boolean(),
|
||||
});
|
||||
|
||||
const canAddWww = (domain: string | undefined) => {
|
||||
@ -70,6 +76,7 @@ function EditDialog(props: {
|
||||
addWww: props.type === 'create',
|
||||
domain: props.type === 'update' ? props.defaultDomain.replace(/^https:\/\//, "") : undefined,
|
||||
handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler",
|
||||
allowInsecureHttp: false,
|
||||
}}
|
||||
onOpenChange={props.onOpenChange}
|
||||
trigger={props.trigger}
|
||||
@ -83,11 +90,11 @@ function EditDialog(props: {
|
||||
domains: [
|
||||
...props.domains,
|
||||
{
|
||||
domain: values.domain,
|
||||
domain: (values.allowInsecureHttp ? 'http' : 'https') + `://` + values.domain,
|
||||
handlerPath: values.handlerPath,
|
||||
},
|
||||
...(canAddWww(values.domain.slice(8)) && values.addWww ? [{
|
||||
domain: 'https://www.' + values.domain.slice(8),
|
||||
...(canAddWww(values.domain) && values.addWww ? [{
|
||||
domain: `${values.allowInsecureHttp ? 'http' : 'https'}://www.` + values.domain,
|
||||
handlerPath: values.handlerPath,
|
||||
}] : []),
|
||||
],
|
||||
@ -118,7 +125,7 @@ function EditDialog(props: {
|
||||
label="Domain"
|
||||
name="domain"
|
||||
control={form.control}
|
||||
prefixItem='https://'
|
||||
prefixItem={form.getValues('allowInsecureHttp') ? 'http://' : 'https://'}
|
||||
placeholder='example.com'
|
||||
/>
|
||||
|
||||
@ -144,6 +151,16 @@ function EditDialog(props: {
|
||||
<Typography variant="secondary" type="footnote">
|
||||
only modify this if you changed the default handler path in your app
|
||||
</Typography>
|
||||
<div className="my-4">
|
||||
<SwitchField
|
||||
label="Allow insecure HTTP domains"
|
||||
name="allowInsecureHttp"
|
||||
control={form.control}
|
||||
/>
|
||||
</div>
|
||||
<Typography variant="secondary" type="footnote">
|
||||
Warning: HTTP domains are insecure and should only be used for development / internal networks.
|
||||
</Typography>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@ -274,6 +274,106 @@ it("is not allowed to have two identical domains", async ({ expect }) => {
|
||||
`);
|
||||
});
|
||||
|
||||
it("should allow insecure HTTP domains", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
domains: [{
|
||||
domain: 'http://insecure-domain.stack-test.example.com',
|
||||
handler_path: '/handler'
|
||||
}]
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"config": {
|
||||
"allow_localhost": true,
|
||||
"client_team_creation_enabled": false,
|
||||
"client_user_deletion_enabled": false,
|
||||
"create_team_on_sign_up": false,
|
||||
"credential_enabled": true,
|
||||
"domains": [
|
||||
{
|
||||
"domain": "http://insecure-domain.stack-test.example.com",
|
||||
"handler_path": "/handler",
|
||||
},
|
||||
],
|
||||
"email_config": { "type": "shared" },
|
||||
"enabled_oauth_providers": [],
|
||||
"id": "<stripped UUID>",
|
||||
"legacy_global_jwt_signing": false,
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"passkey_enabled": false,
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"user_count": 0,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not allow protocols other than http(s) in trusted domains", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, {
|
||||
config: {
|
||||
domains: [{
|
||||
domain: 'whatever://disallowed-domain.stack-test.example.com',
|
||||
handler_path: '/handler'
|
||||
}]
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"config": {
|
||||
"allow_localhost": true,
|
||||
"client_team_creation_enabled": false,
|
||||
"client_user_deletion_enabled": false,
|
||||
"create_team_on_sign_up": false,
|
||||
"credential_enabled": true,
|
||||
"domains": [
|
||||
{
|
||||
"domain": "whatever://disallowed-domain.stack-test.example.com",
|
||||
"handler_path": "/handler",
|
||||
},
|
||||
],
|
||||
"email_config": { "type": "shared" },
|
||||
"enabled_oauth_providers": [],
|
||||
"id": "<stripped UUID>",
|
||||
"legacy_global_jwt_signing": false,
|
||||
"magic_link_enabled": false,
|
||||
"oauth_providers": [],
|
||||
"passkey_enabled": false,
|
||||
"sign_up_enabled": true,
|
||||
"team_creator_default_permissions": [{ "id": "admin" }],
|
||||
"team_member_default_permissions": [{ "id": "member" }],
|
||||
},
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"user_count": 0,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("updates the project email configuration", async ({ expect }) => {
|
||||
await Auth.Otp.signIn();
|
||||
const { adminAccessToken } = await Project.createAndGetAdminToken();
|
||||
|
||||
@ -57,7 +57,8 @@ export const emailConfigSchema = yupObject({
|
||||
});
|
||||
|
||||
const domainSchema = yupObject({
|
||||
domain: schemaFields.projectTrustedDomainSchema.defined(),
|
||||
domain: schemaFields.urlSchema.defined()
|
||||
.meta({ openapiField: { description: 'URL. Must either start with https:// or', exampleValue: 'https://example.com' } }),
|
||||
handler_path: schemaFields.handlerPathSchema.defined(),
|
||||
});
|
||||
|
||||
|
||||
@ -288,7 +288,6 @@ export const emailUsernameSchema = yupString().meta({ openapiField: { descriptio
|
||||
export const emailSenderEmailSchema = emailSchema.meta({ openapiField: { description: 'Email sender email. Needs to be specified when using type="standard"', exampleValue: 'example@your-domain.com' } });
|
||||
export const emailPasswordSchema = passwordSchema.meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
|
||||
// Project domain config
|
||||
export const projectTrustedDomainSchema = urlSchema.test('is-https', 'Trusted domain must start with https://', (value) => value?.startsWith('https://')).meta({ openapiField: { description: 'Your domain URL. Make sure you own and trust this domain. Needs to start with https://', exampleValue: 'https://example.com' } });
|
||||
export const handlerPathSchema = yupString().test('is-handler-path', 'Handler path must start with /', (value) => value?.startsWith('/')).meta({ openapiField: { description: 'Handler path. If you did not setup a custom handler path, it should be "/handler" by default. It needs to start with /', exampleValue: '/handler' } });
|
||||
|
||||
// Users
|
||||
|
||||
Loading…
Reference in New Issue
Block a user