fixed domain and handler

This commit is contained in:
Zai Shi 2024-03-06 14:15:13 +08:00
parent 5840bcfa10
commit 32c16f0704
3 changed files with 197 additions and 83 deletions

View File

@ -1,7 +1,7 @@
"use client";
import { IconButton, List, ListItem, ListDivider, Input, FormControl, FormLabel, Checkbox, Typography, Box } from "@mui/joy";
import React, { use, useState } from "react";
import { IconButton, List, ListItem, ListDivider, Input, FormControl, FormLabel, Checkbox, Typography, Box, FormHelperText } from "@mui/joy";
import React, { use, useEffect, useState } from "react";
import { Paragraph } from "@/components/paragraph";
import { Icon } from "@/components/icon";
import { Dialog } from "@/components/dialog";
@ -9,6 +9,139 @@ import { AsyncButton } from "@/components/async-button";
import { SimpleCard } from "@/components/simple-card";
import { useAdminApp } from "../../use-admin-app";
import { SmartSwitch } from "@/components/smart-switch";
import { Project } from "@stackframe/stack/dist/lib/stack-app";
import { DomainConfigJson } from "@stackframe/stack-shared/dist/interface/clientInterface";
function isValidUrl(urlString: string) {
try {
return Boolean(new URL(urlString));
}
catch(e){
return false;
}
}
function EditDialog(props: {
open: boolean,
onClose: () => void,
domains: Set<DomainConfigJson>,
project: Project,
type: 'update' | 'create',
editIndex?: number,
}) {
const [newDomain, setNewDomain] = useState("");
const [newDomainError, setNewDomainError] = useState<string | null>(null);
const [newHandlerPath, setNewHandlerPath] = useState("/handler");
const [newHandlerPathError, setNewHandlerPathError] = useState<string | null>(null);
useEffect(() => {
console.log(props.editIndex, props.domains);
if (props.editIndex !== undefined) {
const domain = [...props.domains][props.editIndex];
setNewDomain(domain.domain);
setNewHandlerPath(domain.handlerPath);
}
}, [props.editIndex, props.domains]);
return (
<Dialog
title={(props.type === 'create' ? "Create" : "Update") + " domain and handler"}
open={props.open}
onClose={() => {
setNewDomain("");
setNewDomainError(null);
props.onClose();
}}
okButton={{
label: props.type === 'create' ? "Create" : "Update",
onClick: async () => {
if (!newDomain.startsWith("http://") && !newDomain.startsWith("https://")) {
setNewDomainError("Domain must start with http:// or https://");
return "prevent-close";
}
if (!newHandlerPath.startsWith("/")) {
setNewHandlerPathError("Handler path must start with /");
return "prevent-close";
}
if (!isValidUrl(newDomain)) {
setNewDomainError("Invalid domain");
return "prevent-close";
}
if (!isValidUrl(newDomain + newHandlerPath)) {
setNewHandlerPathError("Invalid handler path");
return "prevent-close";
}
const domainAlreadyExists = [...props.domains].some(({ domain }) => domain === newDomain);
if (props.type === 'create' && domainAlreadyExists ) {
setNewDomainError("Domain already exists");
return "prevent-close";
}
if (props.type === 'create') {
await props.project.update({
config: {
domains: [...props.domains, {
domain: newDomain,
handlerPath: newHandlerPath,
}],
},
});
} else {
await props.project.update({
config: {
domains: [...props.domains].map((domain, i) => {
if (i === props.editIndex) {
return {
domain: newDomain,
handlerPath: newHandlerPath,
};
}
return domain;
})
},
});
}
}
}}
cancelButton
>
<Paragraph body>
<b>Warning:</b> Make sure this is a trusted domain or a URL that you control.
</Paragraph>
<FormControl required error={!!newDomainError} sx={{ mt: 2}}>
<FormLabel>
Domain (http:// or https://)
</FormLabel>
<Input
type="text"
value={newDomain}
onChange={(e) => setNewDomain(e.target.value)}
/>
<FormHelperText>
{newDomainError}
</FormHelperText>
</FormControl>
<FormControl required error={!!newHandlerPathError} sx={{ mt: 2}}>
<FormLabel>
Handler path
</FormLabel>
<Input
type="text"
value={newHandlerPath}
onChange={(e) => setNewHandlerPath(e.target.value)}
/>
<FormHelperText>
{newHandlerPathError}
</FormHelperText>
</FormControl>
</Dialog>
);
}
export default function UrlsAndCallbacksClient() {
const stackAdminApp = useAdminApp();
@ -16,19 +149,15 @@ export default function UrlsAndCallbacksClient() {
const domains = new Set(project.evaluatedConfig.domains);
const [deleteDialogPrefix, setDeleteDialogDomain] = useState<string | null>(null);
const [deleteDialogDomain, setDeleteDialogDomain] = useState<string | null>(null);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [addNewDialogOpen, setAddNewDialogOpen] = useState(false);
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
const [newDomain, setNewDomain] = useState("");
const [newDomainError, setNewDomainError] = useState(false);
const [newHandlerPath, setNewHandlerPath] = useState("/handler");
const [newHandlerPathError, setNewHandlerPathError] = useState(false);
return (
<>
<SimpleCard title="Your Domains and Handler">
<SimpleCard title="Domains and Handler">
<Box sx={{ my: 2 }}>
<SmartSwitch
checked={project.evaluatedConfig.allowLocalhost}
@ -44,29 +173,42 @@ export default function UrlsAndCallbacksClient() {
</SmartSwitch>
</Box>
{domains.size >= 0 || (
{domains.size >= 0 && (
<List
variant="soft"
sx={{
"--List-radius": "9px"
"--List-radius": "9px",
}}
>
{[...domains].map(({ domain }, i) => (
{[...domains].map(({ domain, handlerPath }, i) => (
<React.Fragment key={domain}>
{i !== 0 && <ListDivider />}
<ListItem
endAction={
<IconButton
aria-label="Delete"
size="sm"
color="danger"
onClick={() => setDeleteDialogDomain(domain)}
>
<Icon icon="delete" />
</IconButton>
<>
<IconButton
aria-label="Edit"
size="sm"
onClick={() => {
setEditIndex(i);
setEditDialogOpen(true);
}}
>
<Icon icon="edit" />
</IconButton>
<IconButton
aria-label="Delete"
size="sm"
color="danger"
onClick={() => setDeleteDialogDomain(domain)}
>
<Icon icon="delete" />
</IconButton>
</>
}
>
{domain}
<Typography paddingRight={1}>{domain}</Typography>
<Typography>{handlerPath}</Typography>
</ListItem>
</React.Fragment>
))}
@ -83,9 +225,26 @@ export default function UrlsAndCallbacksClient() {
</AsyncButton>
</SimpleCard>
<EditDialog
type='create'
open={addNewDialogOpen}
onClose={() => setAddNewDialogOpen(false)}
domains={domains}
project={project}
/>
<EditDialog
type='update'
open={editDialogOpen}
onClose={() => setEditDialogOpen(false)}
editIndex={editIndex}
domains={domains}
project={project}
/>
<Dialog
title
open={deleteDialogPrefix !== null}
open={deleteDialogDomain !== null}
onClose={() => setDeleteDialogDomain(null)}
danger
okButton={{
@ -93,7 +252,7 @@ export default function UrlsAndCallbacksClient() {
onClick: async () => {
await project.update({
config: {
domains: [...domains].filter(({ domain }) => domain !== deleteDialogPrefix),
domains: [...domains].filter(({ domain }) => domain !== deleteDialogDomain),
}
});
}
@ -101,66 +260,12 @@ export default function UrlsAndCallbacksClient() {
cancelButton
>
<Paragraph body sx={{ mt: 0 }}>
Do you really want to delete the callback prefix <b>{deleteDialogPrefix}</b>?
Do you really want to remove <b>{deleteDialogDomain}</b> from the allow list ?
</Paragraph>
<Paragraph body sx={{ mb: 0 }}>
All callback URLs that start with this prefix will be disallowed.
Your project will no longer be able to receive callbacks from this domain.
</Paragraph>
</Dialog>
<Dialog
title="Add new prefix"
open={addNewDialogOpen}
onClose={() => {
setAddNewDialogOpen(false);
setNewDomain("");
setNewDomainError(false);
}}
okButton={{
label: "Add",
onClick: async () => {
if (!newDomain.startsWith("http://") && !newDomain.startsWith("https://")) {
setNewDomainError(true);
return "prevent-close";
}
await project.update({
config: {
domains: [...domains, {
domain: newDomain,
handlerPath: newHandlerPath,
}],
},
});
}
}}
cancelButton
>
<Paragraph body>
<b>Warning:</b> Make sure this is a trusted domain or a URL that you control.
</Paragraph>
<FormControl required error={newDomainError} sx={{ mt: 2}}>
<FormLabel>
Domain (http:// or https://)
</FormLabel>
<Input
type="text"
value={newDomain}
onChange={(e) => setNewDomain(e.target.value)}
/>
</FormControl>
<FormControl required error={newHandlerPathError} sx={{ mt: 2}}>
<FormLabel>
Handler path
</FormLabel>
<Input
type="text"
value={newHandlerPath}
onChange={(e) => setNewHandlerPath(e.target.value)}
/>
</FormControl>
</Dialog>
</>
);
}

View File

@ -234,14 +234,14 @@ export async function updateProject(
// delete existing domains
transaction.push(prismaClient.projectDomain.deleteMany({
where: { projectConfigId: projectId },
where: { projectConfigId: project.config.id },
}));
// create new domains
newDomains.forEach(domainConfig => {
transaction.push(prismaClient.projectDomain.create({
data: {
projectConfigId: projectId,
projectConfigId: project.config.id,
domain: domainConfig.domain,
handlerPath: domainConfig.handlerPath,
},

View File

@ -1,2 +1,11 @@
import { PrismaClient } from '@prisma/client';
export const prismaClient = new PrismaClient();
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prismaClient = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prismaClient;
}