Merge pull request #186 from AntlerVC/develop

Develop
This commit is contained in:
AntlerEngineering
2020-09-04 23:07:39 +10:00
committed by GitHub
19 changed files with 1126 additions and 363 deletions

View File

@@ -44,8 +44,8 @@ supported. More coming soon, for comprehensive list see ROADMAP.md.
- Upgrade project to the Blaze Plan
- Enable the Google sign-in method in **Authentication / Sign-in method**
- **⚠️ IMPORTANT:** If you try to sign in and see “This account does not
exist”, run
- **⚠️ IMPORTANT:** If you try to sign in and see “This account does not have
any roles”, run
[the following script](https://github.com/AntlerVC/firetable/blob/develop/RULES.md#custom-claims)
on your Firebase Authentication user.
@@ -126,9 +126,9 @@ yarn local
[Please create issues here.](https://github.com/antlervc/firetable/issues)
Make sure to provide console log outputs and screenshots!
### Known Issue: “This account does not exist
### Known Issue: “This account does not have any roles
If you try to sign in and see “This account does not exist”, run
If you try to sign in and see “This account does not have any roles”, run
[the following script](https://github.com/AntlerVC/firetable/blob/develop/RULES.md#custom-claims)
on your Firebase Authentication user.

View File

@@ -51,8 +51,11 @@ The firetable roles are stored in the users firebase auth token custom claims
### setting roles
this a basic script for setting your user roles. you can run this locally using
the adm sdk or implement it in your cloud functions
You can use the CLI tool to set your roles
[here](https://github.com/AntlerVC/firetable/blob/master/cli/README.md#Setting-user-Roles)
It relays on this basic script. you can run this locally using the adm sdk or
implement it in your cloud functions
```js
import * as admin from "firebase-admin";

View File

@@ -6,6 +6,7 @@ Make sure you have the following installed:
- [Git](https://git-scm.com/downloads)
- [Node](https://nodejs.org/en/download/)
- [Yarn](https://classic.yarnpkg.com/en/docs/install/)
- [Firebase CLI](https://firebase.google.com/docs/cli)
Also make sure you are logged in to your Firebase account in the Firebase CLI.
@@ -47,3 +48,25 @@ First, make sure that you have created a site in your Firebase project.
```
firetable deploy
```
## Firebase Rules & Firetable roles
Read more about firebase rules for firetable
[HERE](https://github.com/AntlerVC/firetable/blob/master/RULES.md)
### Setting user Roles
Download the admin key for your project then add it to the directory without
renaming it. You can find your service account here:
https://console.firebase.google.com/u/0/project/_/settings/serviceaccounts/adminsdk
```
firetable auth:setRoles <email> <roles>
```
email: needs to be associated with an existing firebase account on the example
roles: can be one role `ADMIN` or a comma separated array `ADMIN,OPS,DEV`
```
firetable auth:setRoles shams@antler.co OPS,INTERNAL
```

View File

@@ -16,6 +16,7 @@
"commander": "^6.0.0",
"configstore": "^5.0.1",
"figlet": "^1.5.0",
"firebase-admin": "^9.1.1",
"inquirer": "^7.3.3",
"lodash": "^4.17.19",
"open": "^7.1.0"

View File

@@ -6,11 +6,11 @@ const terminal = require("./lib/terminal");
const inquirer = require("./lib/inquirer");
const Configstore = require("configstore");
const config = new Configstore("firetable");
const { directoryExists } = require("./lib/files");
const { directoryExists, findFile } = require("./lib/files");
const process = require("process");
const { Command } = require("commander");
const { version } = require("../package.json");
const { setUserRoles } = require("./lib/firebaseAdmin");
const program = new Command();
program.version(version);
@@ -34,7 +34,6 @@ const systemHealthCheck = async () => {
Object.entries(versions).forEach(([app, version]) =>
console.log(`${app.padEnd(8)} ${chalk.green(version)}`)
);
console.log();
};
// checks the current directory of the cli app
@@ -207,4 +206,41 @@ program
}
});
program
.command("auth:setRoles <email> <roles>")
.description(
"Adds roles to the custom claims of a specified firebase account."
)
.action(async (email, roles) => {
try {
// check directory for admin sdk json
const adminSDKFilePath = await findFile(/.*-firebase-adminsdk.*json/);
// let directory = await directoryCheck();
// if (!directory) return;
// await deploy2firebase(directory);
const result = await setUserRoles(adminSDKFilePath)(
email,
roles.split(",")
);
if (result.success) {
console.log(result.message);
return;
} else if (result.code === "auth/user-not-found") {
console.log(
chalk.bold(chalk.red("FAILED: ")),
`could not find an account corresponding with`,
chalk.bold(email)
);
return;
} else {
console.log(chalk.bold(chalk.red(result.message)));
return;
}
} catch (error) {
console.log("\u{1F6D1}" + chalk.bold(chalk.red(" FAILED")));
console.log(error);
}
});
program.parse(process.argv);

View File

@@ -9,4 +9,19 @@ module.exports = {
directoryExists: (filePath) => {
return fs.existsSync(filePath);
},
findFile: (fileRegex) =>
new Promise((resolve, reject) =>
fs.readdir("./", (err, files) => {
const file = files
.map((file) => file.match(fileRegex))
.filter((_file) => _file)[0];
if (file) {
resolve(file[0]);
} else {
reject(
"Can not find the firebase service account key json file, download the admin key for your project then add it to this directory without renaming it.\nYou can find your service account here: https://console.firebase.google.com/u/0/project/_/settings/serviceaccounts/adminsdk"
);
}
})
),
};

View File

@@ -0,0 +1,36 @@
const admin = require("firebase-admin");
const fs = require("fs");
const initializeApp = (serviceAccountFile) => {
console.log(serviceAccountFile);
var serviceAccount = fs.readFileSync(`./${serviceAccountFile}`, {
encoding: "utf8",
});
const serviceAccountJSON = JSON.parse(serviceAccount);
admin.initializeApp({
credential: admin.credential.cert(serviceAccountJSON),
databaseURL: `https://${serviceAccountJSON.project_id}.firebaseio.com`,
});
const auth = admin.auth();
return { auth };
};
module.exports.setUserRoles = (serviceAccountFile) => async (email, roles) => {
try {
const { auth } = initializeApp(serviceAccountFile);
// Initialize Auth
// sets the custom claims on an account to the claims object provided
const user = await auth.getUserByEmail(email);
await auth.setCustomUserClaims(user.uid, { ...user.customClaims, roles });
return {
success: true,
message: `${email} now has the following roles ✨${roles.join(
" & "
)}`,
};
} catch (error) {
return {
success: false,
code: "auth/user-not-found",
message: error.message,
};
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,116 +0,0 @@
import React from "react";
import _find from "lodash/find";
import queryString from "query-string";
import { Link as RouterLink } from "react-router-dom";
import {
makeStyles,
createStyles,
Breadcrumbs as MuiBreadcrumbs,
BreadcrumbsProps,
Link,
Typography,
} from "@material-ui/core";
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
import { useFiretableContext } from "contexts/firetableContext";
import useRouter from "hooks/useRouter";
import routes from "constants/routes";
import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
const useStyles = makeStyles((theme) =>
createStyles({
ol: {
alignItems: "baseline",
paddingLeft: theme.spacing(2),
paddingRight: DRAWER_COLLAPSED_WIDTH,
userSelect: "none",
},
li: {
display: "flex",
alignItems: "center",
textTransform: "capitalize",
"&:first-of-type": { textTransform: "uppercase" },
},
separator: {
alignSelf: "flex-end",
marginBottom: -2,
},
})
);
export default function Breadcrumbs(props: BreadcrumbsProps) {
const classes = useStyles();
const { tables, tableState } = useFiretableContext();
const collection = tableState?.tablePath || "";
const router = useRouter();
const parentLabel = decodeURIComponent(
queryString.parse(router.location.search).parentLabel as string
);
const breadcrumbs = collection.split("/");
const getLabel = (collection: string) =>
_find(tables, ["collection", collection])?.name || collection;
return (
<MuiBreadcrumbs
separator={<ArrowRightIcon />}
aria-label="sub-table breadcrumbs"
classes={classes}
component="div"
{...(props as any)}
>
{breadcrumbs.map((crumb: string, index) => {
// If its the first breadcrumb, show with specific style
const crumbProps = {
key: index,
variant: index === 0 ? "h6" : "caption",
component: index === 0 ? "h1" : "div",
color: index === 0 ? "textPrimary" : "textSecondary",
} as const;
// If its the last crumb, just show the label without linking
if (index === breadcrumbs.length - 1)
return (
<Typography {...crumbProps}>
{getLabel(crumb.replace(/([A-Z])/g, " $1"))}
</Typography>
);
// If odd: breadcrumb points to a document — dont show a link
// TODO: show a picker here to switch between sub tables
if (index % 2 === 1)
return (
<Typography {...crumbProps}>
{getLabel(
parentLabel.split(",")[Math.ceil(index / 2) - 1] || crumb
)}
</Typography>
);
// Otherwise, even: breadcrumb points to a Firestore collection
return (
<Link
key={crumbProps.key}
component={RouterLink}
to={`${routes.table}/${encodeURIComponent(
breadcrumbs.slice(0, index + 1).join("/")
)}`}
variant={crumbProps.variant}
color={crumbProps.color}
>
{getLabel(crumb.replace(/([A-Z])/g, " $1"))}
</Link>
);
})}
</MuiBreadcrumbs>
);
}

View File

@@ -1,130 +0,0 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import clsx from "clsx";
import {
makeStyles,
createStyles,
List,
ListItem,
// ListItemIcon,
ListItemText,
Collapse,
} from "@material-ui/core";
import { fade } from "@material-ui/core/styles";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import { Table } from "contexts/firetableContext";
import { routes } from "constants/routes";
const useStyles = makeStyles((theme) =>
createStyles({
listItem: {
color: theme.palette.text.secondary,
minHeight: 48,
},
listItemSelected: {
"&&, &&:hover": {
color: theme.palette.primary.main,
backgroundColor: fade(
theme.palette.primary.main,
theme.palette.action.selectedOpacity
),
},
},
listItemIcon: {},
listItemText: {
...theme.typography.button,
display: "block",
color: "inherit",
},
dropdownIcon: { transition: theme.transitions.create("transform") },
dropdownIconOpen: { transform: "rotate(180deg)" },
childListItem: {
minHeight: 40,
paddingLeft: theme.spacing(4),
},
childListItemText: {
...theme.typography.overline,
display: "block",
color: "inherit",
},
})
);
export interface INavDrawerItemProps {
section: string;
tables: Table[];
currentSection?: string;
currentTable: string;
}
export default function NavDrawerItem({
section,
tables,
currentSection,
currentTable,
}: INavDrawerItemProps) {
const classes = useStyles();
const [open, setOpen] = useState(section === currentSection);
return (
<li>
<ListItem
button
classes={{
root: clsx(
classes.listItem,
!open && currentSection === section && classes.listItemSelected
),
}}
selected={!open && currentSection === section}
onClick={() => setOpen((o) => !o)}
>
<ListItemText
primary={section}
classes={{ primary: classes.listItemText }}
/>
<ArrowDropDownIcon
className={clsx(
classes.dropdownIcon,
open && classes.dropdownIconOpen
)}
/>
</ListItem>
<Collapse in={open}>
<List>
{tables.map((table) => (
<li key={table.collection}>
<ListItem
button
selected={table.collection === currentTable}
classes={{
root: clsx(classes.listItem, classes.childListItem),
selected: classes.listItemSelected,
}}
component={Link}
to={
table.isCollectionGroup
? `${routes.tableGroup}/${table.collection}`
: `${routes.table}/${table.collection}`
}
>
<ListItemText
primary={table.name}
classes={{ primary: classes.childListItemText }}
/>
</ListItem>
</li>
))}
</List>
</Collapse>
</li>
);
}

View File

@@ -1,90 +0,0 @@
import React, { useState, useRef } from "react";
import { Link } from "react-router-dom";
import {
makeStyles,
createStyles,
IconButton,
IconButtonProps,
Avatar,
Menu,
Typography,
MenuItem,
} from "@material-ui/core";
import AccountCircle from "@material-ui/icons/AccountCircle";
import { useAppContext } from "contexts/appContext";
import routes from "constants/routes";
const useStyles = makeStyles((theme) =>
createStyles({
avatar: {
width: 24,
height: 24,
},
paper: { minWidth: 160 },
displayName: {
display: "block",
padding: theme.spacing(1, 2),
userSelect: "none",
color: theme.palette.text.disabled,
},
})
);
export default function UserMenu(props: IconButtonProps) {
const classes = useStyles();
const anchorEl = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const { userDoc } = useAppContext();
const displayName = userDoc?.state?.doc?.user?.displayName;
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
return (
<>
<IconButton
aria-label="Open user menu"
aria-controls="user-menu"
aria-haspopup="true"
edge="end"
{...props}
ref={anchorEl}
onClick={() => setOpen(true)}
>
{avatarUrl ? (
<Avatar src={avatarUrl} className={classes.avatar} />
) : (
<AccountCircle />
)}
</IconButton>
<Menu
anchorEl={anchorEl.current}
id="user-menu"
keepMounted
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
getContentAnchorEl={null}
transformOrigin={{ vertical: "top", horizontal: "right" }}
open={open}
onClose={() => setOpen(false)}
classes={{ paper: classes.paper }}
>
{displayName && (
<Typography
variant="overline"
className={classes.displayName}
role="presentation"
>
{displayName}
</Typography>
)}
<MenuItem component={Link} to={routes.signOut}>
Sign Out
</MenuItem>
</Menu>
</>
);
}

View File

@@ -16,7 +16,7 @@ import MenuIcon from "@material-ui/icons/Menu";
import FiretableLogo from "assets/Firetable";
import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer";
import UserMenu from "./UserMenu";
import UserMenu from "components/Navigation/UserMenu";
export const APP_BAR_HEIGHT = 56;
@@ -76,7 +76,7 @@ const useStyles = makeStyles((theme) =>
},
logo: {
flex: 1,
marginLeft: theme.spacing(1),
textAlign: "center",
opacity: 1,
transition: theme.transitions.create("opacity"),

View File

@@ -57,6 +57,7 @@ export default function Breadcrumbs(props: BreadcrumbsProps) {
const breadcrumbs = collection.split("/");
const section = _find(tables, ["collection", breadcrumbs[0]])?.section || "";
const getLabel = (collection: string) =>
_find(tables, ["collection", collection])?.name || collection;
@@ -68,12 +69,24 @@ export default function Breadcrumbs(props: BreadcrumbsProps) {
component="div"
{...(props as any)}
>
{/* Section name */}
{section && (
<Link
component={RouterLink}
to={`${routes.home}#${section}`}
variant="h6"
color="textPrimary"
>
{section}
</Link>
)}
{breadcrumbs.map((crumb: string, index) => {
// If its the first breadcrumb, show with specific style
const crumbProps = {
key: index,
variant: index === 0 ? "h6" : "caption",
component: index === 0 ? "h1" : "div",
component: index === 0 ? "h2" : "div",
color: index === 0 ? "textPrimary" : "textSecondary",
} as const;
@@ -81,7 +94,7 @@ export default function Breadcrumbs(props: BreadcrumbsProps) {
if (index === breadcrumbs.length - 1)
return (
<Typography {...crumbProps}>
{getLabel(crumb.replace(/([A-Z])/g, " $1"))}
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
</Typography>
);
@@ -107,7 +120,7 @@ export default function Breadcrumbs(props: BreadcrumbsProps) {
variant={crumbProps.variant}
color={crumbProps.color}
>
{getLabel(crumb.replace(/([A-Z])/g, " $1"))}
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
</Link>
);
})}

View File

@@ -39,7 +39,9 @@ export default function UserMenu(props: IconButtonProps) {
const anchorEl = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const { userDoc } = useAppContext();
const { currentUser, userDoc } = useAppContext();
if (!currentUser || !userDoc || !userDoc?.state?.doc) return null;
const displayName = userDoc?.state?.doc?.user?.displayName;
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;

View File

@@ -40,7 +40,7 @@ const useSettings = () => {
//create the firetable collection doc with empty columns
db.collection("_FIRETABLE_/settings/schema")
.doc(data.collection)
.set({ ...data, columns: [] }, { merge: true });
.set({ ...data }, { merge: true });
};
const updateTable = (data: {

View File

@@ -24,6 +24,7 @@ export const SnackProvider: React.FC<ISnackProviderProps> = ({ children }) => {
setMessage("");
setDuration(0);
setSeverity(undefined);
setAction(<div />);
};
const open = (props: {
message: string;

View File

@@ -8,6 +8,8 @@ import AuthCard from "./AuthCard";
import { handleGoogleAuth } from "./utils";
import GoogleLogo from "assets/google-icon.svg";
import { useSnackContext } from "contexts/snackContext";
import { Link } from "react-router-dom";
import { auth } from "../../firebase";
export default function GoogleAuthView() {
const [loading, setLoading] = useState(false);
const snack = useSnackContext();
@@ -27,7 +29,45 @@ export default function GoogleAuthView() {
},
(error: Error) => {
setLoading(false);
snack.open({ message: error.message });
console.log(error);
if (
error.message ===
"The identity provider configuration is disabled."
) {
snack.open({
severity: "warning",
message:
"You need to enable Google authentication in your firebase project",
action: (
<Button
component="a"
href={`https://console.firebase.google.com/u/0/project/${auth.app.options["projectId"]}/authentication/providers`}
target="_blank"
>
Go to settings
</Button>
),
});
} else if (
error.message === "This account does not have any roles"
) {
snack.open({
severity: "warning",
message:
"You need to enable Google authentication in your firebase project",
action: (
<Button
component="a"
href={`https://github.com/AntlerVC/firetable/blob/master/RULES.md`}
target="_blank"
>
Instructions
</Button>
),
});
} else {
snack.open({ message: error.message });
}
},
parsedQuery.email as string
);

View File

@@ -14,7 +14,7 @@ export const handleGoogleAuth = async (
if (result.claims.roles && result.claims.roles.length !== 0) {
success(authUser, result.claims.roles);
} else {
throw Error("This account does not exist");
throw Error("This account does not have any roles");
}
} catch (error) {
if (auth.currentUser) {

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from "react";
import queryString from "query-string";
import { useFiretableContext } from "contexts/firetableContext";
import { useAppContext } from "contexts/appContext";
import { Hidden } from "@material-ui/core";
@@ -12,11 +13,12 @@ import { FireTableFilter } from "hooks/useFiretable";
import useRouter from "hooks/useRouter";
import ImportWizard from "components/Wizards/ImportWizard";
import { DocActions } from "hooks/useDoc";
export default function TableView() {
const router = useRouter();
const tableCollection = decodeURIComponent(router.match.params.id);
const { tableState, tableActions, sideDrawerRef } = useFiretableContext();
const { userDoc } = useAppContext();
let filters: FireTableFilter[] = [];
const parsed = queryString.parse(router.location.search);
if (typeof parsed.filters === "string") {
@@ -32,7 +34,15 @@ export default function TableView() {
tableState &&
tableState.tablePath !== tableCollection
) {
tableActions.table.set(tableCollection, filters);
if (filters && filters.length !== 0) {
tableActions.table.set(tableCollection, filters);
userDoc.dispatch({
action: DocActions.update,
data: {
tables: { [`${tableState?.tablePath}`]: { filters } },
},
});
}
if (sideDrawerRef?.current) sideDrawerRef.current.setCell!(null);
}
}, [tableCollection]);