mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
350 lines
10 KiB
TypeScript
350 lines
10 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
|
|
|
import {
|
|
Typography,
|
|
FormControlLabel,
|
|
Checkbox,
|
|
Button,
|
|
Link,
|
|
Grid,
|
|
} from "@mui/material";
|
|
import LoadingButton from "@mui/lab/LoadingButton";
|
|
import InfoIcon from "@mui/icons-material/InfoOutlined";
|
|
import CopyIcon from "@src/assets/icons/Copy";
|
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
|
|
|
import SetupItem from "./SetupItem";
|
|
import DiffEditor from "@src/components/CodeEditor/DiffEditor";
|
|
import CodeEditor from "@src/components/CodeEditor";
|
|
|
|
import { name } from "@root/package.json";
|
|
import { useAppContext } from "@src/contexts/AppContext";
|
|
import { CONFIG } from "@src/config/dbPaths";
|
|
import {
|
|
requiredRules,
|
|
adminRules,
|
|
utilFns,
|
|
insecureRule,
|
|
} from "@src/config/firestoreRules";
|
|
import { rowyRun } from "@src/utils/rowyRun";
|
|
import { runRoutes } from "@src/constants/runRoutes";
|
|
// import { useConfirmation } from "@src/components/ConfirmationDialog";
|
|
|
|
const insecureRuleRegExp = new RegExp(
|
|
insecureRule
|
|
.replace(/\//g, "\\/")
|
|
.replace(/\*/g, "\\*")
|
|
.replace(/\s{2,}/g, "\\s+")
|
|
.replace(/\s/g, "\\s*")
|
|
.replace(/\n/g, "\\s+")
|
|
.replace(/;/g, ";?")
|
|
);
|
|
|
|
export default function Step3Rules({
|
|
rowyRunUrl,
|
|
completion,
|
|
setCompletion,
|
|
}: ISetupStepBodyProps) {
|
|
const { projectId, getAuthToken } = useAppContext();
|
|
// const { requestConfirmation } = useConfirmation();
|
|
|
|
const [error, setError] = useState<string | false>(false);
|
|
const [hasRules, setHasRules] = useState(completion.rules);
|
|
const [adminRule, setAdminRule] = useState(true);
|
|
const [showManualMode, setShowManualMode] = useState(false);
|
|
|
|
const rules = `${
|
|
error === "security-rules/not-found"
|
|
? `rules_version = '2';\n\nservice cloud.firestore {\n match /databases/{database}/documents {\n`
|
|
: ""
|
|
}${adminRule ? adminRules : ""}${requiredRules}${utilFns}${
|
|
error === "security-rules/not-found" ? " }\n}" : ""
|
|
}`.replace("\n", "");
|
|
|
|
const [currentRules, setCurrentRules] = useState("");
|
|
useEffect(() => {
|
|
if (rowyRunUrl && !hasRules && !currentRules)
|
|
getAuthToken(true)
|
|
.then((authToken) =>
|
|
rowyRun({
|
|
serviceUrl: rowyRunUrl,
|
|
route: runRoutes.firestoreRules,
|
|
authToken,
|
|
})
|
|
)
|
|
.then((data) => {
|
|
if (data?.code) {
|
|
setError(data.code);
|
|
setShowManualMode(true);
|
|
} else {
|
|
setCurrentRules(data?.source?.[0]?.content ?? "");
|
|
}
|
|
});
|
|
}, [rowyRunUrl, hasRules, currentRules, getAuthToken]);
|
|
|
|
const hasInsecureRule = insecureRuleRegExp.test(currentRules);
|
|
|
|
const [newRules, setNewRules] = useState("");
|
|
useEffect(() => {
|
|
let rulesToInsert = rules;
|
|
|
|
if (currentRules.indexOf("function isDocOwner") > -1) {
|
|
rulesToInsert = rulesToInsert.replace(/function isDocOwner[^}]*}/s, "");
|
|
}
|
|
if (currentRules.indexOf("function hasAnyRole") > -1) {
|
|
rulesToInsert = rulesToInsert.replace(/function hasAnyRole[^}]*}/s, "");
|
|
}
|
|
|
|
let inserted = currentRules.replace(
|
|
/match\s*\/databases\/\{database\}\/documents\s*\{/,
|
|
`match /databases/{database}/documents {\n` + rulesToInsert
|
|
);
|
|
|
|
if (hasInsecureRule) inserted = inserted.replace(insecureRuleRegExp, "");
|
|
|
|
setNewRules(inserted);
|
|
}, [currentRules, rules, hasInsecureRule]);
|
|
|
|
const [rulesStatus, setRulesStatus] = useState<"LOADING" | string>("");
|
|
const setRules = async () => {
|
|
setRulesStatus("LOADING");
|
|
try {
|
|
const authToken = await getAuthToken();
|
|
if (!authToken) throw new Error("Failed to generate auth token");
|
|
|
|
const res = await rowyRun({
|
|
serviceUrl: rowyRunUrl,
|
|
route: runRoutes.setFirestoreRules,
|
|
authToken,
|
|
body: { ruleset: newRules },
|
|
});
|
|
if (!res.success) throw new Error(res.message);
|
|
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
|
if (isSuccessful) {
|
|
setCompletion((c) => ({ ...c, rules: true }));
|
|
setHasRules(true);
|
|
}
|
|
setRulesStatus("");
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
setRulesStatus(e.message);
|
|
}
|
|
};
|
|
const verifyRules = async () => {
|
|
setRulesStatus("LOADING");
|
|
try {
|
|
const authToken = await getAuthToken();
|
|
if (!authToken) throw new Error("Failed to generate auth token");
|
|
|
|
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
|
if (isSuccessful) {
|
|
setCompletion((c) => ({ ...c, rules: true }));
|
|
setHasRules(true);
|
|
}
|
|
setRulesStatus("");
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
setRulesStatus(e.message);
|
|
}
|
|
};
|
|
|
|
// const handleSkip = () => {
|
|
// requestConfirmation({
|
|
// title: "Skip rules",
|
|
// body: "This might prevent you or other users in your project from accessing firestore data on Rowy",
|
|
// confirm: "Skip",
|
|
// cancel: "cancel",
|
|
// handleConfirm: async () => {
|
|
// setCompletion((c) => ({ ...c, rules: true }));
|
|
// setHasRules(true);
|
|
// },
|
|
// });
|
|
// };
|
|
|
|
return (
|
|
<>
|
|
<Typography variant="inherit">
|
|
{name} configuration is stored in the <code>{CONFIG}</code> collection
|
|
on Firestore. Your users will need read access to this collection and
|
|
admins will need write access.
|
|
</Typography>
|
|
|
|
{!hasRules && error !== "security-rules/not-found" && (
|
|
<SetupItem
|
|
status="incomplete"
|
|
title="Add the following rules to enable access to Rowy configuration:"
|
|
>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={adminRule}
|
|
onChange={(e) => setAdminRule(e.target.checked)}
|
|
/>
|
|
}
|
|
label="Allow admins to read and write all documents"
|
|
sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }}
|
|
/>
|
|
|
|
<Typography>
|
|
<InfoIcon
|
|
aria-label="Info"
|
|
sx={{ fontSize: 18, mr: 11 / 8, verticalAlign: "sub" }}
|
|
/>
|
|
We removed an insecure rule that allows anyone to access any part of
|
|
your database
|
|
</Typography>
|
|
|
|
<DiffEditor
|
|
original={currentRules}
|
|
modified={newRules}
|
|
containerProps={{ sx: { width: "100%" } }}
|
|
minHeight={400}
|
|
options={{ renderValidationDecorations: "off" }}
|
|
/>
|
|
|
|
<Typography
|
|
variant="inherit"
|
|
color={
|
|
rulesStatus !== "LOADING" && rulesStatus ? "error" : undefined
|
|
}
|
|
>
|
|
Please verify the new rules first.
|
|
</Typography>
|
|
|
|
<LoadingButton
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={setRules}
|
|
loading={rulesStatus === "LOADING"}
|
|
>
|
|
Set Firestore Rules
|
|
</LoadingButton>
|
|
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
|
<Typography variant="caption" color="error">
|
|
{rulesStatus}
|
|
</Typography>
|
|
)}
|
|
{!showManualMode && (
|
|
<Link
|
|
component="button"
|
|
variant="body2"
|
|
onClick={() => setShowManualMode(true)}
|
|
>
|
|
Alternatively, add these rules in the Firebase Console
|
|
</Link>
|
|
)}
|
|
</SetupItem>
|
|
)}
|
|
|
|
{!hasRules && showManualMode && (
|
|
<SetupItem
|
|
status="incomplete"
|
|
title={
|
|
error === "security-rules/not-found"
|
|
? "Add the following rules in the Firebase Console to enable access to Rowy configuration:"
|
|
: "Alternatively, you can add these rules in the Firebase Console."
|
|
}
|
|
>
|
|
<Typography
|
|
variant="caption"
|
|
component="pre"
|
|
sx={{
|
|
width: "100%",
|
|
height: 400,
|
|
resize: "both",
|
|
overflow: "auto",
|
|
|
|
"& .comment": { color: "info.dark" },
|
|
}}
|
|
dangerouslySetInnerHTML={{
|
|
__html: rules.replace(
|
|
/(\/\/.*$)/gm,
|
|
`<span class="comment">$1</span>`
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<div>
|
|
<Grid container spacing={1}>
|
|
<Grid item>
|
|
<Button
|
|
startIcon={<CopyIcon />}
|
|
onClick={() => navigator.clipboard.writeText(rules)}
|
|
>
|
|
Copy to clipboard
|
|
</Button>
|
|
</Grid>
|
|
|
|
<Grid item>
|
|
<Button
|
|
href={`https://console.firebase.google.com/project/${
|
|
projectId || "_"
|
|
}/firestore/rules`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Firebase Console
|
|
<InlineOpenInNewIcon />
|
|
</Button>
|
|
</Grid>
|
|
|
|
<Grid item>
|
|
<LoadingButton
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={verifyRules}
|
|
loading={rulesStatus === "LOADING"}
|
|
>
|
|
Verify
|
|
</LoadingButton>
|
|
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
|
<Typography variant="caption" color="error">
|
|
{rulesStatus}
|
|
</Typography>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
</div>
|
|
</SetupItem>
|
|
)}
|
|
|
|
{hasRules && (
|
|
<SetupItem status="complete" title="Firestore Rules are set up." />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export const checkRules = async (
|
|
rowyRunUrl: string,
|
|
authToken: string,
|
|
signal?: AbortSignal
|
|
) => {
|
|
if (!authToken) return false;
|
|
|
|
try {
|
|
const res = await rowyRun({
|
|
serviceUrl: rowyRunUrl,
|
|
route: runRoutes.firestoreRules,
|
|
authToken,
|
|
signal,
|
|
});
|
|
const rules = res?.source?.[0]?.content || "";
|
|
if (!rules) return false;
|
|
|
|
const sanitizedRules = rules.replace(/\s{2,}/g, " ").replace(/\n/g, " ");
|
|
const hasRules =
|
|
sanitizedRules.includes(
|
|
requiredRules.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
|
) &&
|
|
sanitizedRules.includes(
|
|
utilFns.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
|
);
|
|
return hasRules;
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
};
|