Compare commits

..

58 Commits

Author SHA1 Message Date
Szilárd Dóró
21fb316655 Merge pull request #1554 from nhost/changeset-release/main
chore: update versions
2023-01-30 15:39:36 +01:00
github-actions[bot]
9c4f350508 chore: update versions 2023-01-30 11:18:24 +00:00
Szilárd Dóró
eb3ba21afc Merge pull request #1556 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2023-01-30 12:16:59 +01:00
Szilárd Dóró
a84ac62999 Merge pull request #1546 from nhost/roles-iagsd9ahsd
fix(dashboard): allowed roles
2023-01-30 11:38:27 +01:00
Szilárd Dóró
f32bfed9a9 Merge pull request #1558 from nhost/fix/pnpm-lock
fix pnpm-lock.yaml
2023-01-30 10:49:40 +01:00
Szilárd Dóró
55632506e4 fix pnpm-lock.yaml 2023-01-30 10:49:21 +01:00
szilarddoro
e146d32e69 chore(deps): update dependency @types/react to v18.0.27 2023-01-30 09:44:29 +00:00
Szilárd Dóró
df8a13997c Merge pull request #1487 from nhost/renovate/jsdom-21.x
chore(deps): update dependency jsdom to v21
2023-01-30 10:43:48 +01:00
Szilárd Dóró
c488fd4c0c Merge pull request #1533 from nhost/renovate/react-18.x
chore(deps): update dependency @types/react to v18.0.27
2023-01-30 10:43:20 +01:00
Johan Eliasson
a6e8569822 Merge pull request #1544 from nhost/docs-auth-uba9sdasd
docs(auth): improvements
2023-01-30 10:37:56 +01:00
Johan Eliasson
a8023c9a3f Merge pull request #1552 from jonorossi/patch-1
docs: fix link to sign-in with security keys
2023-01-30 10:37:32 +01:00
Johan Eliasson
59347fcd4b added changeset 2023-01-30 10:18:05 +01:00
Johan Eliasson
5b65cac91e added changeset 2023-01-30 10:16:46 +01:00
Johan Eliasson
331ae02e2d revert 2023-01-30 10:15:34 +01:00
Johan Eliasson
3b8b3be393 Update docs/docs/authentication/sign-in-methods/3-security-keys.mdx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-30 10:12:30 +01:00
Johan Eliasson
9fb4d82d86 Apply suggestions from code review
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-30 10:12:13 +01:00
Szilárd Dóró
37d836b9b6 Merge pull request #1553 from nhost/chore/enhanced-feedback
feat(dashboard): include project info in feedback
2023-01-30 10:08:50 +01:00
Szilárd Dóró
3c1996b13b Merge pull request #1543 from nhost/changeset-release/main
chore: update versions
2023-01-30 10:01:21 +01:00
github-actions[bot]
e3d90fd5d2 chore: update versions 2023-01-30 08:58:53 +00:00
Szilárd Dóró
af016e1caa Merge pull request #1548 from nhost/remove-functions-section
fix(dashboard): removed functions section
2023-01-30 09:57:31 +01:00
Szilárd Dóró
963f9b5e85 feat(dashboard): include project info in feedback 2023-01-30 09:56:56 +01:00
Szilárd Dóró
ed4c780115 fix(dashboard): lint error 2023-01-30 09:40:51 +01:00
Jonathon Rossi
260997c6fe docs: fix link to sign-in with security keys 2023-01-30 12:25:29 +10:00
Johan Eliasson
941f0f5755 removed unused code 2023-01-28 20:17:43 +01:00
Johan Eliasson
75344b2bc0 remove 2023-01-28 18:02:57 +01:00
Johan Eliasson
65da426e8b update 2023-01-28 17:46:18 +01:00
Johan Eliasson
a711727e94 wording 2023-01-28 14:13:14 +01:00
Johan Eliasson
8e8f6fd9c9 updates 2023-01-28 09:41:15 +01:00
Johan Eliasson
a1c2e5d8ee docs updates 2023-01-27 21:42:40 +01:00
Johan Eliasson
5c276ae844 improved docs for auth 2023-01-27 20:16:58 +01:00
Johan Eliasson
f34702f3c5 Merge pull request #1541 from nhost/docs-graphql-integrations
stripe graphql api updates
2023-01-27 19:35:31 +01:00
Johan Eliasson
6cb70eee01 Update docs/docs/graphql/remote-schemas/stripe.mdx
Co-authored-by: Guido Curcio <guidomaurocurcio@gmail.com>
2023-01-27 16:12:43 +01:00
Johan Eliasson
9395c9687f update 2023-01-27 16:03:31 +01:00
Johan Eliasson
eb1eb934a4 update 2023-01-27 10:36:56 +01:00
Johan Eliasson
c62fed2c9a update 2023-01-27 09:16:24 +01:00
Johan Eliasson
16fe1a47da fixed broken links 2023-01-27 09:13:42 +01:00
Johan Eliasson
0f04e8b8b8 added example response 2023-01-27 09:07:38 +01:00
Johan Eliasson
e6dad4d696 added changeset 2023-01-27 08:57:30 +01:00
Johan Eliasson
bcb3b79add updates 2023-01-27 08:54:23 +01:00
Szilárd Dóró
fe658231b4 Merge pull request #1538 from nhost/changeset-release/main
chore: update versions
2023-01-23 10:37:11 +01:00
github-actions[bot]
a1188b7d98 chore: update versions 2023-01-23 09:10:50 +00:00
Szilárd Dóró
cd4bdc581d Merge pull request #1537 from nhost/fix/local-auth-page
fix(dashboard): don't break Auth page in local mode
2023-01-23 10:09:26 +01:00
Szilárd Dóró
4e2f8ccd52 fix(dashboard): don't break Auth page in local mode 2023-01-23 09:30:11 +01:00
Szilárd Dóró
8a6d8c7534 Merge pull request #1534 from nhost/changeset-release/main
chore: update versions
2023-01-19 08:17:58 +01:00
github-actions[bot]
fa75409f09 chore: update versions 2023-01-18 20:37:34 +00:00
Szilárd Dóró
74662052ae Merge pull request #1531 from nhost/fix/allowed-emails-and-domains
fix(dashboard): enable toggle when settings are filled in
2023-01-18 21:36:20 +01:00
renovate[bot]
2de904c865 chore(deps): update dependency @types/react to v18.0.27 2023-01-18 20:06:28 +00:00
Szilárd Dóró
37ab5fe878 trigger build 2023-01-18 17:50:28 +01:00
Szilárd Dóró
be9af96fa7 fix(dashboard): remove values if toggle is disabled 2023-01-18 16:42:44 +01:00
Szilárd Dóró
31abbe5f30 fix(dashboard): enable toggle when settings are filled in 2023-01-18 15:18:39 +01:00
Szilárd Dóró
268b461d5b Merge pull request #1529 from nhost/changeset-release/main
chore: update versions
2023-01-18 10:44:26 +01:00
github-actions[bot]
58af592cfa chore: update versions 2023-01-18 09:04:47 +00:00
Szilárd Dóró
0e9d623c69 Merge pull request #1527 from nhost/fix/permission-editor-array-input
fix(dashboard): don't throw validation error for valid permission rules
2023-01-18 10:03:35 +01:00
Szilárd Dóró
412a290646 Merge pull request #1528 from nhost/chore/lower-storage-page-limit
chore(dashboard): list fewer images per page on the Storage page
2023-01-18 09:35:15 +01:00
Szilárd Dóró
123add38a4 fix(dashboard): fetch images from correct URL 2023-01-17 16:43:34 +01:00
Szilárd Dóró
5bdd31ad36 chore(dashboard): list fewer images per page on the Storage page 2023-01-17 16:41:14 +01:00
Szilárd Dóró
5121851c8b fix(dashboard): don't throw validation error for valid permission rules 2023-01-17 16:35:29 +01:00
renovate[bot]
a21aa05b5a chore(deps): update dependency jsdom to v21 2023-01-12 08:53:01 +00:00
61 changed files with 1021 additions and 1435 deletions

View File

@@ -1,5 +1,39 @@
# @nhost/dashboard
## 0.10.1
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
- 59347fcd: correct allowed role name
- 5b65cac9: updated authentication documentation
- 963f9b5e: feat(dashboard): include project info in feedback
## 0.10.0
### Minor Changes
- ed4c7801: chore(dashboard): remove Functions section
## 0.9.10
### Patch Changes
- 4e2f8ccd: fix(dashboard): don't break Auth page in local mode
## 0.9.9
### Patch Changes
- 31abbe5f: fix(dashboard): enable toggle when settings are filled in
## 0.9.8
### Patch Changes
- 5bdd31ad: chore(dashboard): list fewer images per page on the Storage page
- 5121851c: fix(dashboard): don't throw validation error for valid permission rules
## 0.9.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
# Nhost Dashboard
This is the Nhost Dashboard, a web application that allows you to manage your Nhost project.
This is the Nhost Dashboard, a web application that allows you to manage your Nhost projects.
To get started, you need to have an Nhost project. If you don't have one, you can [create a project here](https://app.nhost.io).
```bash

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.9.7",
"version": "0.10.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -8,7 +8,7 @@
"build": "next build --no-lint",
"analyze": "ANALYZE=true pnpm build --no-lint",
"start": "next start",
"lint": "next lint --max-warnings 3",
"lint": "next lint --max-warnings 2",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d",
@@ -104,7 +104,7 @@
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.0.25",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5",
@@ -126,7 +126,7 @@
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^20.0.3",
"jsdom": "^21.0.0",
"lint-staged": ">=13",
"msw": "^0.49.0",
"msw-storybook-addon": "^1.6.3",

View File

@@ -1,5 +0,0 @@
export type FunctionLog = {
name: string;
language: string;
logs: { date: string; message: string; createdAt: string }[];
};

View File

@@ -1,30 +0,0 @@
import { Text } from '@/ui/Text';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistance } from 'date-fns';
export interface FunctionLogDataEntryProps {
time: string;
nav: string;
}
export function FunctionLogDataEntry({ time, nav }: FunctionLogDataEntryProps) {
return (
<a href={`#${nav}`}>
<div className="flex cursor-pointer flex-row place-content-between border-t py-3">
<Text
color="greyscaleDark"
variant="body"
className="flex font-medium"
size="tiny"
>
{formatDistance(new Date(time), new Date(), {
addSuffix: true,
})}
</Text>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center text-greyscaleDark" />
</div>
</a>
);
}
export default FunctionLogDataEntry;

View File

@@ -1,52 +0,0 @@
import { Text } from '@/ui/Text';
import { FunctionLogDataEntry } from './FunctionLogDataEntry';
export interface FunctionLogHistoryProps {
logs?: Log[];
}
type Log = {
createdAt: string;
date: any;
message: string;
};
export function FunctionLogHistory({ logs }: FunctionLogHistoryProps) {
return (
<div className=" mx-auto max-w-6xl pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log History
</Text>
</div>
</div>
<div className="mt-5 flex flex-col">
<div className="flex flex-row">
<Text className="font-semibold" size="normal" color="greyscaleDark">
Time
</Text>
</div>
<div className="flex flex-col">
{logs ? (
<div>
{logs.slice(0, 4).map((log: Log) => (
<FunctionLogDataEntry
time={log.createdAt}
nav={`#-${log.date}`}
key={`${log.date}-${log.message.slice(66)}`}
/>
))}
</div>
) : (
<div className="pt-1 pl-0.5 font-mono text-xs text-greyscaleDark">
No log history.
</div>
)}
</div>
</div>
</div>
);
}
export default FunctionLogHistory;

View File

@@ -1,89 +0,0 @@
import { normalizeToIndividualFunctionsWithLogs } from '@/components/applications/functions/normalizeToIndividualFunctionsWithLogs';
import terminalTheme from '@/data/terminalTheme';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetFunctionLogQuery } from '@/utils/__generated__/graphql';
import { useEffect, useState } from 'react';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
import { FunctionLogHistory } from './FunctionLogHistory';
SyntaxHighlighter.registerLanguage('json', json);
export function FunctionsLogsTerminalPage({ functionName }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
const { data, startPolling } = useGetFunctionLogQuery({
variables: {
subdomain: currentApplication.subdomain,
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
},
});
useEffect(() => {
startPolling(3000);
}, [startPolling]);
useEffect(() => {
if (!data || data.getFunctionLogs.length === 0) {
return;
}
setNormalizedFunctionData(
normalizeToIndividualFunctionsWithLogs(data.getFunctionLogs)[0],
);
}, [data]);
if (
!data ||
data.getFunctionLogs.length === 0 ||
!normalizedFunctionData ||
normalizedFunctionData.logs.length === 0
) {
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
<div className="font-mono text-xs text-grey">
There are no stored logs yet. Try calling your function for logs to
appear.
</div>
</div>
<FunctionLogHistory />
</div>
);
}
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
{normalizedFunctionData.logs.map((log) => (
<div
key={`${log.date}-${log.message.slice(66)}`}
className=" flex text-sm"
>
<div id={`#-${log.date}`}>
<pre className="inline">
<span className="mr-4 text-greyscaleGrey">{log.date}</span>{' '}
<span className="">
{' '}
<SyntaxHighlighter
style={terminalTheme}
customStyle={{
display: 'inline',
}}
className="inline-flex"
language="json"
>
{log.message}
</SyntaxHighlighter>
</span>
</pre>
</div>
</div>
))}
</div>
<FunctionLogHistory logs={normalizedFunctionData.logs} />
</div>
);
}
export default FunctionsLogsTerminalPage;

View File

@@ -1,5 +0,0 @@
export type FunctionResponseLog = {
functionPath: string;
createdAt: string;
message: string;
};

View File

@@ -1,46 +0,0 @@
import { Button } from '@/ui/Button';
import Loading from '@/ui/Loading';
import { Text } from '@/ui/Text';
import Image from 'next/image';
export function FunctionsNotDeployed() {
return (
<div className="mx-auto mt-12 max-w-2xl text-center">
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/terminal-text.svg"
alt="Terminal with a green dot"
width={72}
height={72}
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Functions Logs
</Text>
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Once you deploy a function, you can view the logs here.
</Text>
<div className="mt-1.5 flex text-center">
<Button
Component="a"
transparent
color="blue"
className="mx-auto cursor-pointer font-medium"
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
Read more
</Button>
</div>
<div className="mt-24 flex flex-col text-center">
<Loading />
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Awaiting new requests
</Text>
</div>
</div>
);
}
export default FunctionsNotDeployed;

View File

@@ -1,79 +0,0 @@
export type FinalFunction = {
folder: string;
funcs: Func[];
nestedLevel: number;
parentFolder?: string;
};
export type Func = {
name: string;
id: string;
lang: string;
functionName: string;
route?: string;
path?: string;
createdAt?: string;
updatedAt?: string;
createdWithCommitSha?: string;
formattedCreatedAt?: string;
formattedUpdatedAt?: string;
};
export const normalizeFunctionMetadata = (functions): FinalFunction[] => {
const finalFunctions: FinalFunction[] = [
{ folder: 'functions', funcs: [], nestedLevel: 0 },
];
const topLevelFunctionsFolder = finalFunctions[0].funcs;
functions.forEach((func) => {
const nestedLevel = func.path?.split('/').length;
const newFuncToAdd = {
...func,
name: func.path?.split('/')[nestedLevel - 1],
lang: func.path?.split('.')[1],
// formattedCreatedAt: `${format(
// parseISO(func.createdAt),
// 'yyyy-MM-dd HH:mm:ss',
// )}`,
// formattedUpdatedAt: `${formatDistanceToNowStrict(
// parseISO(func.updatedAt),
// {
// addSuffix: true,
// },
// )}`,
};
if (nestedLevel === 2) {
topLevelFunctionsFolder.push(newFuncToAdd);
} else if (nestedLevel > 2) {
const nameOfTheFolder = func.path?.split('/')[nestedLevel - 2];
const nameOfParentFolder = func.path?.split('/')[nestedLevel - 3];
const checkForFolderExistence = finalFunctions.find(
(functionFolder) => functionFolder.folder === nameOfTheFolder,
);
if (!checkForFolderExistence) {
finalFunctions.push({
folder: nameOfTheFolder,
funcs: [newFuncToAdd],
nestedLevel: nestedLevel - 2,
parentFolder: nameOfParentFolder,
});
} else {
checkForFolderExistence.funcs.push(newFuncToAdd);
}
}
});
// Sort folders by putting the subfolder next to their parent folder, even though they share the same place in the array
// except for the nestedLevel prop. A future change to this would be to make folders have subfolders, which is easier
// understand, but would require a change in the UI.
// @TODO: Change to have elements have subfolders inside the object?
finalFunctions.sort((a, b) => {
if (a.folder === b.parentFolder) {
return -1;
}
return 1;
});
return finalFunctions;
};

View File

@@ -1,39 +0,0 @@
import { format, parseISO } from 'date-fns';
import type { FunctionLog } from './FunctionLog';
import type { FunctionResponseLog } from './FunctionResponseLog';
export const normalizeToIndividualFunctionsWithLogs = (
functionLogs: FunctionResponseLog[],
) => {
const arrayOfFunctions: FunctionLog[] = [];
const sortedFunctions = [...functionLogs].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
sortedFunctions.forEach((functionLog) => {
const funcName = functionLog.functionPath;
const logMessage = {
createdAt: functionLog.createdAt,
date: `${format(parseISO(functionLog.createdAt), 'yyyy-MM-dd HH:mm:ss')}`,
message: functionLog.message,
};
const newFunc = {
name: funcName,
language: functionLog.functionPath.split('.')[1],
logs: [logMessage],
};
// If the function is already in the array of functions to log, just add the new log message to the existing object...
if (arrayOfFunctions.some((obj) => obj.name === funcName)) {
const index = arrayOfFunctions.findIndex((obj) => obj.name === funcName);
const currentFunction = arrayOfFunctions[index];
currentFunction.logs.push(logMessage);
} else {
// If the function is not in the array of functions, add it with the log message to it.
arrayOfFunctions.push(newFunc);
}
});
return arrayOfFunctions;
};
export default normalizeToIndividualFunctionsWithLogs;

View File

@@ -4,7 +4,17 @@ import * as Yup from 'yup';
const ruleSchema = Yup.object().shape({
column: Yup.string().nullable().required('Please select a column.'),
operator: Yup.string().nullable().required('Please select an operator.'),
value: Yup.string().nullable().required('Please enter a value.'),
value: Yup.mixed()
.test(
'isArray',
'Please enter a valid value.',
(value) =>
typeof value === 'string' ||
(Array.isArray(value) &&
value.every((item) => typeof item === 'string')),
)
.nullable()
.required('Please enter a value.'),
});
const ruleGroupSchema = Yup.object().shape({

View File

@@ -37,7 +37,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
parseInt(router.query.page as string, 10) - 1 || 0,
);
const [sortBy, setSortBy] = useState<SortingRule<StoredFile>[]>();
const limit = 25;
const limit = 10;
const emptyStateMessage = searchString
? 'No search results found.'
: 'No files are uploaded yet.';

View File

@@ -1,22 +1,33 @@
import { useInsertFeedbackOneMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useUserData } from '@nhost/nextjs';
import * as React from 'react';
export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [insertFeedback, { loading }] = useInsertFeedbackOneMutation();
const user = nhost.auth.getUser();
const user = useUserData();
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const feedbackWithProjectInfo = [
currentApplication && `Project ID: ${currentApplication.id}`,
typeof window !== 'undefined' && `URL: ${window.location.href}`,
feedback,
]
.filter(Boolean)
.join('\n\n');
try {
await insertFeedback({
variables: {
feedback: {
feedback,
feedback: feedbackWithProjectInfo,
},
},
});

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface AllowedEmailSettingsFormValues {
/**
* Determines whether or not the allowed email settings are enabled.
*/
enabled: boolean;
/**
* Set of email that are allowed to be used for project's users authentication.
*/
@@ -25,7 +29,6 @@ export interface AllowedEmailSettingsFormValues {
export default function AllowedEmailDomainsSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -36,12 +39,30 @@ export default function AllowedEmailDomainsSettings() {
const form = useForm<AllowedEmailSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlAllowedEmails) ||
Boolean(data?.app?.authAccessControlAllowedEmailDomains),
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
authAccessControlAllowedEmailDomains:
data?.app?.authAccessControlAllowedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlAllowedEmails &&
!data.app?.authAccessControlAllowedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -56,8 +77,6 @@ export default function AllowedEmailDomainsSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: AllowedEmailSettingsFormValues,
) => {
@@ -65,7 +84,12 @@ export default function AllowedEmailDomainsSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlAllowedEmails: values.enabled
? values.authAccessControlAllowedEmails
: '',
authAccessControlAllowedEmailDomains: values.enabled
? values.authAccessControlAllowedEmailDomains
: '',
},
},
});
@@ -89,13 +113,17 @@ export default function AllowedEmailDomainsSettings() {
<SettingsContainer
title="Allowed Emails and Domains"
description="Allow specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -84,7 +84,7 @@ export default function AllowedRedirectURLsSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
className="grid grid-flow-row px-4 lg:grid-cols-5"
>
<Input

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface BlockedEmailFormValues {
/**
* Determines whether or not the blocked email settings are enabled.
*/
enabled: boolean;
/**
* Set of emails that are blocked from registering to the user's project.
*/
@@ -24,7 +28,6 @@ export interface BlockedEmailFormValues {
export default function BlockedEmailSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -35,12 +38,30 @@ export default function BlockedEmailSettings() {
const form = useForm<BlockedEmailFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlBlockedEmails) ||
Boolean(data?.app?.authAccessControlBlockedEmailDomains),
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
authAccessControlBlockedEmailDomains:
data?.app?.authAccessControlBlockedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlBlockedEmails &&
!data.app?.authAccessControlBlockedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -55,8 +76,6 @@ export default function BlockedEmailSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: BlockedEmailFormValues,
) => {
@@ -64,7 +83,12 @@ export default function BlockedEmailSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlBlockedEmails: values.enabled
? values.authAccessControlBlockedEmails
: '',
authAccessControlBlockedEmailDomains: values.enabled
? values.authAccessControlBlockedEmailDomains
: '',
},
},
});
@@ -88,13 +112,17 @@ export default function BlockedEmailSettings() {
<SettingsContainer
title="Blocked Emails and Domains"
description="Block specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -82,7 +82,7 @@ export default function ClientURLSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#client-url"
className="grid grid-flow-row lg:grid-cols-5"
>
<Input

View File

@@ -89,7 +89,7 @@ export default function DisableNewUsersSettings() {
<SettingsContainer
title="Disable New Users"
description="If set, newly registered users are disabled and wont be able to sign in."
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#disable-new-users"
switchId="authDisableNewUsers"
showSwitch
enabled={authDisableNewUsers}

View File

@@ -110,7 +110,7 @@ export default function GravatarSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#gravatar"
switchId="authGravatarEnabled"
showSwitch
enabled={authGravatarEnabled}

View File

@@ -99,7 +99,7 @@ export default function MFASettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
switchId="authMfaEnabled"
enabled={authMfaEnabled}
showSwitch

View File

@@ -58,10 +58,10 @@ export default function BaseRoleForm({
return (
<div className="grid grid-flow-row gap-3 px-6 pb-6">
<Text variant="subtitle1" component="span">
Enter the name for the role below.
Enter the name for the allowed role below.
</Text>
{submitButtonText !== 'Create' && (
{submitButtonText !== 'Add' && (
<Alert severity="warning" className="text-left">
<span className="text-left">
<strong>Note:</strong> Changing the name of the role will lose the

View File

@@ -85,11 +85,7 @@ export default function CreateRoleForm({
return (
<FormProvider {...form}>
<BaseRoleForm
submitButtonText="Create"
onSubmit={handleSubmit}
{...props}
/>
<BaseRoleForm submitButtonText="Add" onSubmit={handleSubmit} {...props} />
</FormProvider>
);
}

View File

@@ -8,18 +8,18 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import DotsVerticalIcon from '@/ui/v2/icons/DotsVerticalIcon';
import LockIcon from '@/ui/v2/icons/LockIcon';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import {
useGetRolesQuery,
useUpdateAppMutation
} from '@/utils/__generated__/graphql';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetRolesQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { Fragment } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
@@ -98,9 +98,9 @@ export default function RoleSettings() {
await toast.promise(
updateAppPromise,
{
loading: 'Deleting role...',
success: 'Role has been deleted successfully.',
error: 'An error occurred while trying to delete the role.',
loading: 'Deleting allowed role...',
success: 'Allowed Role has been deleted successfully.',
error: 'An error occurred while trying to delete the allowed role.',
},
toastStyleProps,
);
@@ -108,7 +108,7 @@ export default function RoleSettings() {
function handleOpenCreator() {
openDialog('CREATE_ROLE', {
title: 'Create Role',
title: 'Create Allowed Role',
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' },
@@ -118,7 +118,7 @@ export default function RoleSettings() {
function handleOpenEditor(originalRole: Role) {
openDialog('EDIT_ROLE', {
title: 'Edit Role',
title: 'Edit Allowed Role',
payload: { originalRole },
props: {
titleProps: { className: '!pb-0' },
@@ -129,12 +129,11 @@ export default function RoleSettings() {
function handleConfirmDelete(originalRole: Role) {
openAlertDialog({
title: 'Delete Role',
title: 'Delete Allowed Role',
payload: (
<Text>
Are you sure you want to delete the &quot;
<strong>{originalRole.name}</strong>&quot; role? This cannot be
undone.
Are you sure you want to delete the allowed role &quot;
<strong>{originalRole.name}</strong>&quot;?.
</Text>
),
props: {
@@ -145,13 +144,15 @@ export default function RoleSettings() {
});
}
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
const availableAllowedRoles = getUserRoles(
data?.app?.authUserDefaultAllowedRoles,
);
return (
<SettingsContainer
title="Roles"
description="Roles are used to control access to your application."
docsLink="https://docs.nhost.io/authentication/users#roles"
title="Allowed Roles"
description="Allowed roles are roles users get automatically when they sign up."
docsLink="https://docs.nhost.io/authentication/users#allowed-roles"
rootClassName="gap-0"
className="px-0 my-2"
slotProps={{ submitButton: { className: 'invisible' } }}
@@ -162,7 +163,7 @@ export default function RoleSettings() {
<div className="grid grid-flow-row gap-2">
<List>
{availableRoles.map((role, index) => (
{availableAllowedRoles.map((role, index) => (
<Fragment key={role.name}>
<ListItem.Root
className="px-4"
@@ -249,7 +250,9 @@ export default function RoleSettings() {
<Divider
component="li"
className={twMerge(
index === availableRoles.length - 1 ? '!mt-4' : '!my-4',
index === availableAllowedRoles.length - 1
? '!mt-4'
: '!my-4',
)}
/>
</Fragment>
@@ -262,7 +265,7 @@ export default function RoleSettings() {
startIcon={<PlusIcon />}
onClick={handleOpenCreator}
>
Create Role
Create Allowed Role
</Button>
</div>
</SettingsContainer>

View File

@@ -80,7 +80,7 @@ export default function MagicLinkSettings() {
<Form onSubmit={handleMagicLinkSettingsUpdate}>
<SettingsContainer
title="Magic Link"
description="Allow users to sign in with a magic link."
description="Allow users to sign in with a Magic Link."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,

View File

@@ -90,7 +90,7 @@ export default function SMSSettings() {
<FormProvider {...form}>
<Form onSubmit={handleSMSSettingsChange}>
<SettingsContainer
title="SMS"
title="Phone Number (SMS)"
description="Allow users to sign in with Phone Number (SMS)."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,

View File

@@ -67,8 +67,8 @@ export default function CreateUserForm({
} = form;
const baseAuthUrl = generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
currentApplication?.subdomain,
currentApplication?.region?.awsName,
'auth',
);

View File

@@ -8,18 +8,18 @@ import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputLabel from '@/ui/v2/InputLabel';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetRolesQuery,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { Avatar } from '@mui/material';
import { format } from 'date-fns';
@@ -137,7 +137,7 @@ export default function EditUserForm({
}
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = getUserRoles(
@@ -206,11 +206,7 @@ export default function EditUserForm({
</div>
<div>
<Dropdown.Root>
<Dropdown.Trigger
autoFocus={false}
asChild
className="gap-2"
>
<Dropdown.Trigger autoFocus={false} asChild className="gap-2">
<Button variant="outlined" color="secondary">
Actions
</Button>

View File

@@ -7,12 +7,14 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import DotsHorizontalIcon from '@/ui/v2/icons/DotsHorizontalIcon';
import TrashIcon from '@/ui/v2/icons/TrashIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import {
useDeleteRemoteAppUserRolesMutation,
@@ -21,9 +23,6 @@ import {
useRemoteAppDeleteUserMutation,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { ApolloQueryResult } from '@apollo/client';
import { Avatar } from '@mui/material';
import { formatDistance } from 'date-fns';
@@ -77,7 +76,7 @@ export default function UsersBody({
* in the drawer form.
*/
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = useMemo(

View File

@@ -1,114 +0,0 @@
const terminalTheme = {
hljs: {
display: 'block',
background: '#F4F7F9',
color: '#21324B',
},
'hljs-tag': {
color: '#9C73DF',
},
'hljs-keyword': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-selector-tag': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-literal': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-strong': {
color: '#9C73DF',
},
'hljs-name': {
color: '#9C73DF',
},
'hljs-code': {
color: '#66d9ef',
},
'hljs-class .hljs-title': {
color: 'red',
},
'hljs-attribute': {
color: '#bf79db',
},
'hljs-symbol': {
color: '#bf79db',
},
'hljs-regexp': {
color: '#bf79db',
},
'hljs-link': {
color: '#bf79db',
},
'hljs-string': {
color: '#B7A590',
},
'hljs-bullet': {
color: '#B7A590',
},
'hljs-subst': {
color: '#B7A590',
},
'hljs-title': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-section': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-emphasis': {
color: '#3ECF8E',
},
'hljs-type': {
color: '#3ECF8E',
fontWeight: 'bold',
},
'hljs-built_in': {
color: '#3ECF8E',
},
'hljs-builtin-name': {
color: '#3ECF8E',
},
'hljs-selector-attr': {
color: '#3ECF8E',
},
'hljs-selector-pseudo': {
color: '#3ECF8E',
},
'hljs-addition': {
color: '#3ECF8E',
},
'hljs-variable': {
color: '#3ECF8E',
},
'hljs-template-tag': {
color: '#3ECF8E',
},
'hljs-template-variable': {
color: '#3ECF8E',
},
'hljs-comment': {
color: '#21324B',
},
'hljs-quote': {
color: '#75715e',
},
'hljs-deletion': {
color: '#75715e',
},
'hljs-meta': {
color: '#75715e',
},
'hljs-doctag': {
fontWeight: 'bold',
},
'hljs-selector-id': {
fontWeight: 'bold',
},
};
export default terminalTheme;

View File

@@ -6,7 +6,6 @@ import FileTextIcon from '@/ui/v2/icons/FileTextIcon';
import GraphQLIcon from '@/ui/v2/icons/GraphQLIcon';
import HasuraIcon from '@/ui/v2/icons/HasuraIcon';
import HomeIcon from '@/ui/v2/icons/HomeIcon';
import LambdaIcon from '@/ui/v2/icons/LambdaIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import StorageIcon from '@/ui/v2/icons/StorageIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
@@ -54,13 +53,6 @@ export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const nhostRoutes: ProjectRoute[] = [
{
relativePath: '/functions',
exact: false,
label: 'Functions',
icon: <LambdaIcon />,
disabled: !isPlatform,
},
{
relativePath: '/deployments',
exact: false,

View File

@@ -70,7 +70,7 @@ export default function useFiles({
currentApplication.subdomain,
currentApplication.region.awsName,
'storage',
)}/${file.id}`;
)}/files/${file.id}`;
const fetchParams = new URLSearchParams();

View File

@@ -1,269 +0,0 @@
import ConnectGithubModal from '@/components/applications/ConnectGithubModal';
import { FunctionsNotDeployed } from '@/components/applications/functions/FunctionsNotDeployed';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import Folder from '@/components/icons/Folder';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useWorkspaceContext } from '@/context/workspace-context';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Button } from '@/ui/Button';
import DelayedLoading from '@/ui/DelayedLoading';
import { Modal } from '@/ui/Modal';
import Status, { StatusEnum } from '@/ui/Status';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import Image from 'next/image';
import Link from 'next/link';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
function FunctionsNoRepo() {
const [githubModal, setGithubModal] = useState(false);
const [githubRepoModal, setGithubRepoModal] = useState(false);
const { openGitHubModal } = useGitHubModal();
return (
<>
<Modal showModal={githubModal} close={() => setGithubModal(!githubModal)}>
<ConnectGithubModal close={() => setGithubModal(false)} />
</Modal>
<Modal
showModal={githubRepoModal}
close={() => setGithubRepoModal(!githubRepoModal)}
>
<EditRepositorySettings
openConnectGithubModal={() => setGithubModal(true)}
close={() => setGithubRepoModal(false)}
handleSelectAnotherRepository={openGitHubModal}
/>
</Modal>
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/assets/githubRepo.svg"
width={72}
height={72}
alt="GitHub Logo"
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Function Logs
</Text>
<div className="flex">
<div className="mx-auto flex flex-row self-center text-center">
<Text size="normal" color="greyscaleDark" className="mt-1">
To deploy serverless functions, you need to connect your project to
version control.
</Text>
</div>
</div>
<div className="mt-3 flex text-center">
<Button
transparent
color="blue"
className="mx-auto font-medium"
onClick={() => setGithubModal(true)}
>
Connect your Project to GitHub
</Button>
</div>
</>
);
}
export default function FunctionsPage() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { workspaceContext } = useWorkspaceContext();
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [normalizedFunctions, setNormalizedFunctions] = useState(null);
useEffect(() => {
if (!data) {
return;
}
if (data.app.metadataFunctions) {
setNormalizedFunctions(
normalizeFunctionMetadata(data.app.metadataFunctions),
);
}
}, [data]);
if (!workspaceContext.repository) {
return (
<Container className="mt-12 max-w-3xl text-center antialiased">
<FunctionsNoRepo />
</Container>
);
}
if (loading) {
return (
<Container>
<DelayedLoading delay={500} className="mt-12" />
</Container>
);
}
if (!data || normalizedFunctions === null) {
return (
<Container>
<FunctionsNotDeployed />
</Container>
);
}
if (error) {
return <Container>Error</Container>;
}
return (
<Container>
<div className="mt-2">
{normalizedFunctions?.map((folder) => (
<div key={folder.folder}>
<div
className={clsx(
'flex flex-row pt-8 pb-2 align-middle',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
<div className={clsx('flex w-full')}>
{folder.nestedLevel > 0 && (
<Folder className="self-center align-middle text-greyscaleGrey" />
)}
<Text
color="greyscaleDark"
variant="body"
className={clsx(
'font-medium',
folder.nestedLevel > 0 && 'ml-2',
)}
size="tiny"
>
{folder.folder}/
</Text>
</div>
{folder.nestedLevel === 0 ? (
<div className="flex w-full flex-row">
<div className="flex w-52">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Created At
</Text>
</div>
<div className="flex w-16 self-end">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Status
</Text>
</div>
</div>
) : null}
</div>
<div
className={clsx(
'border-t py-1',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
{folder.funcs.map((func) => (
<Link
key={func.id}
href={{
pathname:
'/[workspaceSlug]/[appSlug]/functions/[functionId]',
query: {
workspaceSlug: currentWorkspace.slug,
appSlug: currentApplication.slug,
functionId: func.functionName,
},
}}
passHref
>
<a
href="[workspaceSlug]/[appSlug]/functions/[functionId]"
className={clsx(
'flex cursor-pointer flex-row border-b py-2.5',
folder.nestedLevel && 'ml-0',
)}
>
<div className="flex w-full flex-row items-center">
<Image
src={`/assets/functions/${func.lang}.svg`}
alt={`Logo of ${func.lang}`}
width={16}
height={16}
/>
<Text
color="greyscaleDark"
variant="body"
className="pl-2 font-medium"
size="small"
>
{func.name}
</Text>
</div>
<div className="flex w-full flex-row">
<div className={clsx('flex w-52 self-center')}>
<Text
color="greyscaleDark"
variant="body"
className=""
size="tiny"
>
{func.formattedCreatedAt || '-'}
</Text>
</div>
<div className="flex w-16 self-center">
<Status status={StatusEnum.Live}>Live</Status>
<ChevronRightIcon className="middl ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</a>
</Link>
))}
</div>
</div>
))}
</div>
<div className="mx-auto mt-10 max-w-6xl">
<div className="text-center">
<Text size="tiny" color="greyscaleDark" className="font-medium">
Base URL for function endpoints is{' '}
{generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}
</Text>
</div>
</div>
</Container>
);
}
FunctionsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -1,142 +0,0 @@
import { FunctionsLogsTerminalPage } from '@/components/applications/functions/FunctionLogsTerminalFromPage';
import type { Func } from '@/components/applications/functions/normalizeFunctionMetadata';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { LoadingScreen } from '@/components/common/LoadingScreen';
import Help from '@/components/icons/Help';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetAllUserWorkspacesAndApplications } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { yieldFunction } from '@/utils/helpers';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import Image from 'next/image';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
export default function FunctionDetailsPage() {
const { currentApplication, currentWorkspace } =
useCurrentWorkspaceAndApplication();
useGetAllUserWorkspacesAndApplications(false);
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [currentFunction, setCurrentFunction] = useState<Func | null>(null);
const router = useRouter();
// currentFunction will be null until we get data back from remote and we set it to be the function we're looking for.
useEffect(() => {
if (!data) {
return;
}
const appFunctions = normalizeFunctionMetadata(data?.app.metadataFunctions);
setCurrentFunction(yieldFunction(appFunctions, router));
}, [data, router]);
if (!currentApplication || !currentWorkspace || loading) {
return <LoadingScreen />;
}
if (error) {
throw new Error(
error.message ||
'An unexpected error has ocurred. Please try again later.',
);
}
if (!currentFunction) {
return (
<Container>
<h1 className="text-4xl font-semibold text-greyscaleDark">Not found</h1>
<p className="text-sm text-greyscaleGrey">
This function does not exist.
</p>
</Container>
);
}
return (
<>
<Container>
<div className="flex place-content-between">
<div className="flex flex-row items-center py-1">
<Image
src={`/assets/functions/${
currentFunction.name.split('.')[1]
}.svg`}
alt={`Logo of ${currentFunction.name.split('.')[1]}`}
width={40}
height={40}
/>
<div className="flex flex-col">
<Text
color="greyscaleDark"
variant="body"
className="ml-2 font-medium"
size="big"
>
{currentFunction.name}
</Text>
<a
className="ml-2 text-xs font-medium text-greyscaleGrey"
href={`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
target="_blank"
rel="noreferrer"
>
{`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
</a>
</div>
</div>
</div>
</Container>
<Container className="pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log
</Text>
</div>
<div className="flex">
<Text
size="tiny"
className="self-center font-medium"
color="greyscaleDark"
>
Awaiting new requests
</Text>
<a
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
<Help className="h-7 w-7" />
</a>
</div>
</div>
<div className="mt-5">
<FunctionsLogsTerminalPage functionName={currentFunction?.path} />
</div>
</Container>
</>
);
}
FunctionDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -1,10 +1,5 @@
import type {
FinalFunction,
Func,
} from '@/components/applications/functions/normalizeFunctionMetadata';
import features from '@/data/features.json';
import { ApplicationStatus } from '@/types/application';
import type { NextRouter } from 'next/router';
import slugify from 'slugify';
import { LOCAL_BACKEND_URL } from './env';
import type { DeploymentRowFragment } from './__generated__/graphql';
@@ -89,26 +84,6 @@ export function emptyWorkspace() {
};
}
export function yieldFunction(
functionsToSearch: FinalFunction[],
router: NextRouter,
): Func {
let functionToReturn: Func = null;
functionsToSearch.forEach((currentFolder) => {
currentFolder.funcs.forEach((currentFunction) => {
if (
!functionToReturn &&
currentFunction.functionName === router.query.functionId
) {
functionToReturn = currentFunction;
}
});
});
return functionToReturn;
}
/**
* Converts the state number of the application to its string equivalent.
* @param appStatus The current state of the application.

View File

@@ -1,5 +1,18 @@
# @nhost/docs
## 0.0.12
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
- 5b65cac9: updated authentication documentation
## 0.0.11
### Patch Changes
- e6dad4d6: Added remote schemas
## 0.0.10
### Patch Changes

View File

@@ -3,7 +3,7 @@ title: Email Templates
sidebar_position: 4
---
Nhost Authentication sends out transactional emails as part of the authentication service. These emails can be modified using email templates.
Nhost Auth sends out transactional emails as part of the authentication service. These emails can be modified using email templates.
The following email templates are available:
@@ -70,8 +70,6 @@ As you see, the format is:
nhost/emails/{two-letter-language-code}/{email-template}/[subject.txt, body.html]
```
Default templates for English (`en`) and French (`fr`) are automatically generated when the project is initialized with the [CLI](/cli).
## Languages
The user's language is what decides what template to send. The user's language is stored in the `auth.users` table in the `locale` column. This `locale` column contains a two-letter language code in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format.

View File

@@ -22,3 +22,65 @@ Nhost Authentication lets you authenticate users using different sign-in methods
- [Spotify](/authentication/sign-in-with-spotify)
- [Twitch](/authentication/sign-in-with-twitch)
- [WorkOS](/authentication/sign-in-with-workos)
## Client URL
Client URL is the URL of your frontend application. The Client URL is used to redirect the user after interacting with any authentication operation, like signing in or resetting their password.
## Allowed Redirect URLs
Allowed Redirect URLs are the URLs of your frontend application that are allowed to redirect the user after interacting with any authentication operation, like signing in or resetting their password. This is useful if you have multiple frontend applications that are using the same Nhost backend or if you want to redirect the user to a specific URL after interacting with an authentication operation.
As an example, for a staging project, you can set the Client URL to `https://staging.example.com` and Allowed Redirect URLs to `https://*.vercel.app`. This way, the user can be redirected to any Vercel deployment of your frontend application.
## Allowed Emails and Domains
Allowed Emails and Domains are used to restrict the sign-up an sign-in process to specific email addresses and domains.
If both allowed emails and allowed domains are set a user can only sign up if their email address matches one of the allowed emails or one of the allowed domains.
## Blocked Emails and Domains
Blocked Emails and Domains are used to block specific email addresses and domains from signing up and singin in.
Note that even if a user's email address matches any allowed email or domain, they will still be blocked if their email address matches any blocked email or domain.
## Multi-factor Authentication
By enabling Multi-factor Authentication (MFA), you can allow users to verify their identity using a second factor during the sign-in process. We currently support Authenticator Apps (TOTP) for MFA.
Once MFA is enabled, a user can enable MFA for their account by scanning a QR code with their Authenticator App. After that, they will be prompted to enter a code generated by their Authenticator App during the sign-in process.
We'll be adding more support in our SDKs and documentation around MFA soon.
## Gravatar
If Gravatar is enabled, Nhost Auth will use the user's email address to fetch their Gravatar profile picture. If the user doesn't have a Gravatar profile picture, a default image will be used.
There are two options for Gravatars:
### Default Image
If the user doesn't have a Gravatar profile picture, a default image will be used. You can choose between the following options:
- `404`: Do not load any image if none is associated with the email hash, instead return an HTTP 404 (File Not Found) response.
- `mp`: (mystery-person) a simple, cartoon-style silhouetted outline of a person (does not vary by email hash).
- `identicon`: a geometric pattern based on an email hash.
- `monsterid`: a generated 'monster' with different colors, faces, etc.
- `wavatar`: generated faces with differing features and backgrounds.
- `retro`: awesome generated, 8-bit arcade-style pixelated faces.
- `robohash`: a generated robot with different colors, faces, etc.
- `blank`: a transparent PNG image.
### Rating
Gravatar images are rated by default. You can choose between the following options:
- `g`: suitable for display on all websites with any audience type.
- `pg`: may contain rude gestures, provocatively dressed individuals, lesser swear words or mild violence.
- `r`: may contain such things as harsh profanity, intense violence, nudity, or hard drug use.
- `x`: may contain hardcore sexual imagery or extremely disturbing violence.
## Disable New Users
If set, newly registered users are disabled and won't be able to sign in. This is useful if you want to manually approve new users before they can sign in.

View File

@@ -5,13 +5,11 @@ slug: /authentication/sign-in-with-email-and-password
image: /img/og/sign-in-with-email-and-password.png
---
Follow this guide to sign in users with email and password.
The email and password sign-in method is enabled by default for all Nhost projects.
The Email and Password sign-in method is always enabled for all Nhost projects.
## Sign Up
Users must first sign up to be able to sign in with Email and Password.
Users must first sign up to be able to sign in.
**Example:** Sign up users using the [Nhost JavaScript client](/reference/javascript).
@@ -26,7 +24,7 @@ If you've turned on email verification in your project's **Authentication Settin
## Sign In
Once a user has been signed up (and optionally verified), you can sign them in.
After the user has successfully signed up, they can sign in.
**Example:** Sign in users using the [Nhost JavaScript client](/reference/javascript).
@@ -37,8 +35,10 @@ await nhost.auth.signIn({
})
```
## Verified Emails
## Email Verification
You can decide if only verified emails should be able to sign in or not. Modify the **Only allow users with verified emails to sign in.** setting in the **Authentication Settings** section under **Users** in your Nhost project.
If you want to require users to verify their email before they can sign in, you can enable this under **Settings -> Sign-In Methods -> Email and Password** by checking the **Require Verified Emails** checkbox.
An email-verification email is automatically sent to the user during sign-up if your project only allows to sign in users with verified emails. You can also manually send the verification email to the user using [`nhost.auth.sendVerificationEmail()`](/reference/javascript/auth/send-verification-email).
If **Require Verified Emails** is enabled, users automatically get a verification email when they sign up. The user must click the verification link in the email before they can sign in. It's possible to edit the ["email-verify" email template](/authentication/email-templates).
It's possible to manually send a verification email to the user using [`nhost.auth.sendVerificationEmail()`](/reference/javascript/auth/send-verification-email).

View File

@@ -5,15 +5,15 @@ slug: /authentication/sign-in-with-magic-link
image: /img/og/sign-in-with-magic-link.png
---
Follow this guide to sign in users with Magic Link, also called passwordless email.
Nhost allows you to sign in users with a Magic Link, which is a way to sign in users so they don't have to remember a password.
The Magic Link sign-in method enables you to sign in users using an email address, without requiring a password.
When users sign in using this sign-in method, they'll enter their email address and then receive an email with a (magic) link. When the user clicks on the (magic) link, they get automatically signed in to your app.
## Setup
The sign-in method is called Magic Link because the user gets "magically" signed in without having to enter a password.
Enable the Magic Link sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Magic Link**.
## Configuration
![Magic Link Setup with Nhost](/img/authentication/magic-link/magic-link-setup.png)
Enable the Magic Link sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Magic Link**.
## Sign In
@@ -30,4 +30,10 @@ nhost.auth.signIn({
})
```
If you want to change the email for your magic link emails, you can do so by changing the [email templates](/authentication/email-templates).
There is no sign up method for Magic Link. Users will be automatically created when they sign in for the first time.
Users who have signed up with email and password can also sign in with Magic Link.
## Email
It's possible to edit the ["signin-passwordless" email template](/authentication/email-templates).

View File

@@ -7,11 +7,11 @@ image: /img/og/sign-in-with-phone-number-sms.png
Follow this guide to sign in users with a phone number (SMS).
## Setup
## Configuration
You need a [Twilio account](https://www.twilio.com/try-twilio) to use this feature because all SMS are sent through Twilio.
Enable the Phone Number (SMS) sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Passwordless SMS**.
Enable the Phone Number (SMS) sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Phone Number (SMS)**.
You need to insert the following settings in the Nhost dashboard from Twilio:
@@ -19,10 +19,6 @@ You need to insert the following settings in the Nhost dashboard from Twilio:
- Auth Token
- Messaging Service SID (or a Twilio phone number)
<video width="99%" autoPlay muted loop controls="true" style={{ marginBottom: '15px' }}>
<source src="/videos/enable-sms-sign-in.mp4" type="video/mp4" />
</video>
## Sign In
To sign in users with a phone number is a two-step process:
@@ -44,7 +40,7 @@ await nhost.auth.signIn({
})
```
The first time a user signs in using a phone number, the user is created. That means you don't need to sign up the user before signin in the user.
The first time a user signs in using a phone number, the user is created. That means you don't need to sign up users before signing in users.
:::info

View File

@@ -16,81 +16,105 @@ Examples of security keys:
You can read more about this feature in our [blog post](https://nhost.io/blog/webauthn-sign-in-method)
## Setup
## Configuration
Enable the Security Key sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Security Keys**.
Enable the Security Key sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Security Keys**.
You need to make sure you also set a valid client URL under **Users** -> **Authentication Settings** -> **Client URL**.
<video width="99%" autoPlay muted loop controls="true" style={{ marginBottom: '15px' }}>
<source src="/videos/enable-security-keys-sign-in.mp4" type="video/mp4" />
</video>
You need to make sure you also set a valid client URL under **Settings -> Authentication -> Client URL**.
## Sign Up
Signing up with a security key uses the same method as signing up with an email and a password. Instead of a `password` parameter, you need to set the `securityKey` parameter to `true`:
Users must use an email address to sign up with a security key.
Here's an example of how to sign up a user with a security key with our [JavaScript SDK](/reference/javascript):
**Example:**: Sign up with a security key:
```tsx
const { error, session } = await nhost.auth.signUp({
email: 'joe@example.com',
securityKey: true
})
// Something unexpected happened, for instance, the user canceled the process
if (error) {
// Something unexpected happened, for instance, the user canceled their registration
console.log(error)
} else if (session) {
// Sign up is complete!
console.log(session.user)
} else {
return
}
// if there is no error and no session, the user needs to verify their email address.
if (!session) {
console.log(
'You need to verify your email address by clicking the link in the email we sent you.'
)
return
}
//Sign-up is complete!
console.log(session.user)
```
## Sign In
Once a user added a security key, they can use it to sign in:
Once a user signed up with a security key, and verfied their email if needed, they can use it to sign in.
**Example:** Sign in with a security key:
```tsx
const { error, session } = await nhost.auth.signIn({
email,
email: 'joe@example.com',
securityKey: true
})
if (session) {
// User is signed in
} else {
// Something unexpected happened, for instance, the user canceled the process
if (error) {
console.log(error)
return
}
if (!session) {
// Something unexpected happened
console.log(error)
return
}
// User is signed in
```
## Add a Security Key
Any signed-in user with a valid email can add a security key when the feature is enabled. For instance, someone who signed up with an email and a password can add a security key and thus use it for their later sign-in!
Any signed-in user with a verified email can add a security key to their user account. Once a security key is added, the user can use it their email and the security key to sign in.
Users can use multiple devices to sign in to their account. They can add as many security keys as they like.
It's possible to add multiple security keys to a user account.
**Example:** Add a security key to a user account:
```tsx
const { key, error } = await nhost.auth.addSecurityKey()
if (key) {
// Successfully added a new security key
console.log(key.id)
} else {
// Somethine unexpected happened
// Something unexpected happened
if (error) {
console.log(error)
return
}
// Successfully added a new security key
console.log(key.id)
```
A nickname can be added for each security key to make them easy to identify:
A nickname can be associated with each security key to make it easier to manage security keys in the future.
**Example:** Add a security key with a nickname:
```tsx
await nhost.auth.addSecurityKey('my macbook')
await nhost.auth.addSecurityKey('iPhone')
```
## List or Remove Security Keys
To list and to remove security keys can be achieved over GraphQL after setting the correct Hasura permissions to the `auth.security_keys` table:
To list and remove security keys, use GraphQL and set permissions on the `auth.security_keys` table:
**Example:** Get all security keys for a user:
```graphql
query securityKeys($userId: uuid!) {
@@ -99,6 +123,11 @@ query securityKeys($userId: uuid!) {
nickname
}
}
```
**Example:** Remove a security key:
```graphql
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id

View File

@@ -9,7 +9,7 @@ Nhost Authentication supports the following sign-in methods:
- [Email and Password](/authentication/sign-in-with-email-and-password)
- [Magic Link](/authentication/sign-in-with-magic-link)
- [Phone Number (SMS)](/authentication/sign-in-with-phone-number-sms)
- [Security Keys (WebAuthn)](/authentication/sign-in-with-phone-number-sms)
- [Security Keys (WebAuthn)](/authentication/sign-in-with-security-keys)
- [Apple](/authentication/sign-in-with-apple)
- [Discord](/authentication/sign-in-with-discord)
- [Facebook](/authentication/sign-in-with-facebook)

View File

@@ -1,20 +1,18 @@
---
title: Social Providers Configuration
sidebar_label: Social Providers Configuration
title: Social Providers
sidebar_label: Social Providers
sidebar_position: 10
---
## Enabling Social Sign-In Provider
To start with social sign-in, select your project in Nhost Dashboard and go to **Users** → **Authentication Settings**.
You need to set the Client ID and Client Secret for each provider that you want to enable.
To start with social sign-in, select your project in Nhost Dashboard and go to **Settings -> Sign-In Methods**.
## Implementing sign-in experience
Use the [Nhost JavaScript SDK](/reference/javascript) and the `signIn()` method to implement social sign-in for your project.
Here's an example of how to implement sign-in with GitHub:
**Example**: Sign in a user with [GitHub](/authentication/sign-in-with-github).
```js
nhost.auth.signIn({
@@ -22,19 +20,21 @@ nhost.auth.signIn({
})
```
Users are redirected to your Nhost project's **client URL** by default. By default, your Nhost project's client URL is set to `http://localhost:3000`. You can change the value of your client URL in the Nhost console by going to **Users** → **Authentication Settings** → **Client URL**.
During the sign-in flow, the user is redirected to the provider's website to authenticate. After the user authenticates, they are redirected back to your Nhost project's [**Client URL**](/authentication#client-url) by default. You can change where the user gets redirected to after authentication by passing the `redirectTo` option.
Here is an example of how to redirect to another host or path:
**Example:** Redirect the user to `https://staging.example.com/welcome` after they complete the sign-in flow.
```js
nhost.auth.signIn({
provider: '<provider>'
provider: 'github'
options: {
redirectTo: "<host>/<slug>" // Example: "https://example.com/dashboard"
redirectTo: "https://staging.example.com/welcome",
},
})
```
In the example above, it's important to note that the `redirectTo` URL must be part of the [Allowed Redirect URLs](/authentication#allowed-redirect-urls) of your Nhost project.
## Provider OAuth scopes
Scopes are a mechanism in OAuth to allow or limit an application's access to a user's account.

View File

@@ -5,35 +5,7 @@ sidebar_position: 1
image: /img/og/users.png
---
Users are stored in the database in the `auth.users` table.
## Get User Information using GraphQL
**Example:** Get all users.
```graphql
query {
users {
id
displayName
email
metadata
}
}
```
**Example:** Get a single user.
```graphql
query {
user(id: "<user-id>") {
id
displayName
email
metadata
}
}
```
Users are stored in the `auth.users` table in the [database](/database).
## Creating Users
@@ -133,6 +105,34 @@ await nhost.auth.signUp({
})
```
## Get User Information using GraphQL
**Example:** Get all users.
```graphql
query {
users {
id
displayName
email
metadata
}
}
```
**Example:** Get a single user.
```graphql
query {
user(id: "<user-id>") {
id
displayName
email
metadata
}
}
```
## Import Users
If you have users in a different system, you can import them into Nhost. When importing users you should insert the users directly into the database instead of using the authentication endpoints (`/signup/email-password`) to avoid sending unnecessary transactional emails.

View File

@@ -0,0 +1,4 @@
{
"label": "Remote Schemas",
"position": 11
}

View File

@@ -0,0 +1,252 @@
---
title: Stripe GraphQL API
sidebar_label: Stripe
sidebar_position: 2
image: /img/og/graphql.png
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
This package creates a Stripe GraphQL API, allowing for interaction with data at Stripe.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer:
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
query {
stripe {
customer(id: "cus_xxx") {
id
name
invoices {
data {
id
created
paid
hostedInvoiceUrl
}
}
}
}
}
```
</TabItem>
<TabItem value="response" label="Response">
```json
{
"data": {
"stripe": {
"customer": {
"id": "cus_xxx",
"name": "joe@example.com",
"invoices": {
"data": [
{
"id": "in_1MUmwnCCF9wuB4xxxxxxxx",
"created": 1674806769,
"paid": true,
"hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_xxxxxxx/test_YWNjdF8xS25xV1lDQ0Y5d3VCNGZYLF9ORkhWxxxxxxxxxxxx?s=ap"
}
]
}
}
}
}
}
```
</TabItem>
</Tabs>
It's recommended to add the Stripe GraphQL API as a [Remote Schema in Hasura](https://hasura.io/docs/latest/remote-schemas/index/) and connect data from your database with data in Stripe. By doing so, it's possible to request data from your database and Stripe in a single GraphQL query.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer. Note that the user data is fetched from your database and the Stripe customer data is fetched from Stripe:
```graphql
query {
users {
# User in your database
id
displayName
userData {
stripeCustomerId # Customer's Stripe Customer Id
stripeCustomer {
# Data from Stripe
id
name
paymentMethods {
id
card {
brand
last4
}
}
}
}
}
}
```
## Get Started
Install the package:
<Tabs groupId="package-manager">
<TabItem value="npm" label="npm" default>
```bash
npm install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @nhost/stripe-graphql-js
```
</TabItem>
</Tabs>
## Serverless Function
Create a new [Serverless Function](/serverless-functions): `functions/graphql/stripe.ts`:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const server = createStripeGraphQLServer()
export default server
```
> You can run the Stripe GraphQL API in any Node.js environment because it's built using [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga).
## Stripe Secret Key
Add `STRIPE_SECRET_KEY` as an environment variable.
If you're using Nhost, add `STRIPE_SECRET_KEY` to `.env.development` like this:
```
STRIPE_SECRET_KEY=sk_test_***
```
And add the production key (`sk_live_***`) to [environment variables](/platform/environment-variables) in the Nhost dashboard.
Learn more about [Stripe API keys](https://stripe.com/docs/keys#obtain-api-keys).
## Start Nhost
```
nhost up
```
Learn more about the [Nhost CLI](/cli).
## Test
Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe)
## Remote Schema
Add the Stripe GraphQL API as a Remote Schema in Hasura.
**URL**
```
{{NHOST_FUNCTIONS_URL}}/graphql/stripe
```
**Headers**
```
x-nhost-webhook-secret: NHOST_WEBHOOK_SECRET (From env var)
```
> The `NHOST_WEBHOOK_SECRET` is used to verify that the request is coming from Nhost. The environment variable is a [system environment variable](/platform/environment-variables#system-environment-variables) and is always available.
![Hasura Remote Schema](/img/graphql/remote-schemas/stripe/remote-schema.png)
## Permissions
Here's a minimal example without any custom permissions. Only requests using the `x-hasura-admin-secret` header will work:
```js
const server = createStripeGraphQLServer()
```
For more granular permissions, you can pass an `isAllowed` function to the `createStripeGraphQLServer`. The `isAllowed` function takes a `stripeCustomerId` and [`context`](#context) as parameters and runs every time the GraphQL server makes a request to Stripe to get or modify data for a specific Stripe customer.
Here is an example of an `isAllowed` function:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context
// allow all requests if they have a valid `x-hasura-admin-secret`
if (isAdmin) {
return true
}
// get user id
const userId = userClaims['x-hasura-user-id']
// check if the user is signed in
if (!userId) {
return false
}
// get more user information from the database
const { user } = await gqlSDK.getUser({
id: userId
})
if (!user) {
return false
}
// check if the user is part of a workspace with the `stripeCustomerId`
return user.workspaceMembers.some((workspaceMember) => {
return workspaceMember.workspace.stripeCustomerId === stripeCustomerId
})
}
const server = createStripeGraphQLServer({ isAllowed })
export default server
```
### Context
The `context` object contains:
- `userClaims` - verified JWT claims from the user's access token.
- `isAdmin` - `true` if the request was made using a valid `x-hasura-admin-secret` header.
- `request` - [Fetch API Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) that represents the incoming HTTP request in platform-independent way. It can be useful for accessing headers to authenticate a user
- `query` - the DocumentNode that was parsed from the GraphQL query string
- `operationName` - the operation name selected from the incoming query
- `variables` - the variables that were defined in the query
- `extensions` - the extensions that were received from the client
Read more about the [default context from GraphQL Yoga](https://www.the-guild.dev/graphql/yoga-server/docs/features/context#default-context).
## Source Code
The source code is available on [GitHub](https://github.com/nhost/nhost/tree/main/integrations/stripe-graphql-js).

View File

@@ -11,9 +11,9 @@ With Nhost, you can deploy Serverless Functions to execute custom code. Each Ser
Serverless functions can be used to handle [event triggers](/database/event-triggers), form submissions, integrations (e.g. Stripe, Slack, etc), and more.
## Creating a Serverless Function
## Create a Serverless Function
Every `.js` (JavaScript) and `.ts` (TypeScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
Every `.ts` (TypeScript) and `.js` (JavaScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
<Tabs groupId="language">
<TabItem value="ts" label="TypeScript" default>

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.0.10",
"version": "0.0.12",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -1,5 +1,20 @@
# @nhost-examples/serverless-functions
## 0.0.6
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
- Updated dependencies [e146d32e]
- @nhost/stripe-graphql-js@1.0.1
## 0.0.5
### Patch Changes
- Updated dependencies [e6dad4d6]
- @nhost/stripe-graphql-js@1.0.0
## 0.0.4
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "@nhost-examples/serverless-functions",
"private": true,
"version": "0.0.4",
"version": "0.0.6",
"devDependencies": {
"@types/express": "^4.17.13"
},
"dependencies": {
"@graphql-yoga/node": "^2.13.13",
"@nhost/stripe-graphql-js": "^0.0.8",
"@nhost/stripe-graphql-js": "^1.0.1",
"@pothos/core": "^3.21.0",
"cross-fetch": "^3.1.5",
"graphql": "15.7.2",

View File

@@ -1,5 +1,17 @@
# @nhost/stripe-graphql-js
## 1.0.1
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
## 1.0.0
### Major Changes
- e6dad4d6: Added remote schemas
## 0.0.8
### Patch Changes

View File

@@ -1,5 +1,5 @@
<h1 align="center">@nhost/stripe-graphql-js</h1>
<h2 align="center">Stripe GraphQL API</h2>
<h1 align="center">Stripe GraphQL API</h1>
<h2 align="center">@nhost/stripe-graphql-js</h2>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/v/@nhost/stripe-graphql-js">
@@ -9,179 +9,9 @@
</a>
</p>
This package creates a Stripe GraphQL API.
## Documentation
```graphql
query {
stripe {
customer(id: "cus_xxx" {
id
name
invoices {
data {
id
created
paid
hostedInvoiceUrl
}
}
}
}
}
```
You can also add the Stripe GraphQL API as a Hasura Remote Schema and connect data from your database and Stripe. This allows you to request data from your database and Stripe in a single GraphQL query:
```graphql
query {
users {
# User in your database
id
displayName
userData {
stripeCustomerId # Customer's Stripe Customer Id
stripeCustomer {
# Data from Stripe
id
name
paymentMethods {
id
card {
brand
last4
}
}
}
}
}
}
```
## Install
```bash
npm install @nhost/stripe-graphql-js
```
## Quick Start
### Serverless Function Setup
Create a new [Serverless Function](https://docs.nhost.io/platform/serverless-functions) `functions/graphql/stripe.ts`:
```js
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const server = createStripeGraphQLServer()
export default server
```
> You can run the Stripe GraphQL API in any JS environment because it's built using [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga).
### Stripe Secret Key
Add `STRIPE_SECRET_KEY` as an environment variable. If you're using Nhost, add `STRIPE_SECRET_KEY` to `.env.development` like this:
```
STRIPE_SECRET_KEY=sk_test_xxx
```
Learn more about [Stripe API keys](https://stripe.com/docs/keys#obtain-api-keys).
### Start Nhost
```
nhost up
```
Learn more about the [Nhost CLI](https://docs.nhost.io/platform/cli).
### Test
Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe)
### Remote Schema
Add the Stripe GraphQL API as a Remote Schema in Hasura.
**URL**
```
{{NHOST_BACKEND_URL}}/v1/functions/graphql/stripe
```
**Headers**
```
x-nhost-webhook-secret: NHOST_WEBHOOK_SECRET (from env var)
```
![Hasura Remote Schema](./assets//hasura-remote-schema.png)
## Permissions
Here's a minimal example without any custom permissions. Only requests using the `x-hasura-admin-secret` header will work:
```js
const server = createStripeGraphQLServer()
```
For more granular permissions, you can pass an `isAllowed` function to the `createStripeGraphQLServer`. The `isAllowed` function takes a `stripeCustomerId` and [`context`](#context) as parameters and runs every time the GraphQL server makes a request to Stripe to get or modify data for a specific Stripe customer.
Here is an example of an `isAllowed` function:
```js
const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context
// allow requests if it has a valid `x-hasura-admin-secret`
if (isAdmin) {
return true
}
// get user id
const userId = userClaims['x-hasura-user-id']
// check if user is signed in
if (!userId) {
return false;
}
// get more user information from the database
const { user } = await gqlSDK.getUser({
id: userId,
});
if (!user) {
return false;
}
// check if the user is part of a workspace with the `stripeCustomerId`
return user.workspaceMembers
.some((workspaceMember) => {
return workspaceMember.workspace.stripeCustomerId === stripeCustomerId;
});
}
```
### Context
The `context` object contains:
- `userClaims` - verified JWT claims from the user's access token.
- `isAdmin` - `true` if the request was made using a valid `x-hasura-admin-secret` header.
- `request` - [Fetch API Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) that represents the incoming HTTP request in platform-independent way. It can be useful for accessing headers to authenticate a user
- `query` - the DocumentNode that was parsed from the GraphQL query string
- `operationName` - the operation name selected from the incoming query
- `variables` - the variables that were defined in the query
- `extensions` - the extensions that were received from the client
Read more about the [default context from GraphQL Yoga](https://www.the-guild.dev/graphql/yoga-server/docs/features/context#default-context).
[https://docs.nhost.io/graphql/remote-schemas/stripe](https://docs.nhost.io/graphql/remote-schemas/stripe).
## Development

View File

@@ -15,4 +15,6 @@ const server = createStripeGraphQLServer({
graphiql: true
})
server.start()
server.listen(4000, () => {
console.info('Stripe GraphQL API server is running on http://localhost:4000')
})

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/stripe-graphql-js",
"version": "0.0.8",
"version": "1.0.1",
"description": "Stripe GraphQL API",
"license": "MIT",
"keywords": [
@@ -40,18 +40,18 @@
"verify:fix": "run-p prettier:fix lint:fix"
},
"dependencies": {
"@graphql-yoga/node": "^2.13.13",
"@pothos/core": "^3.21.0",
"graphql": "16.6.0",
"graphql-scalars": "^1.18.0",
"graphql-yoga": "^3.4.0",
"jsonwebtoken": "^9.0.0",
"stripe": "^10.10.0"
"stripe": "^11.8.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^18.11.9",
"dotenv": "^16.0.3",
"ts-node-dev": "^2.0.0",
"typescript": "^4.8.4"
"typescript": "^4.7.4"
}
}

View File

@@ -1,6 +1,6 @@
import Stripe from 'stripe'
import { GraphQLYogaError } from '@graphql-yoga/node'
import { GraphQLError } from 'graphql'
import { builder } from '../builder'
import { stripe } from '../utils'
@@ -12,7 +12,7 @@ builder.objectType('Stripe', {
resolve: async (_parent, _, context) => {
const { isAdmin } = context
if (!isAdmin) throw new GraphQLYogaError('Not allowed')
if (!isAdmin) throw new GraphQLError('Not allowed')
const connectedAccounts = await stripe.accounts.list()
@@ -29,7 +29,7 @@ builder.objectType('Stripe', {
resolve: async (_parent, { id }, context) => {
const { isAdmin } = context
if (!isAdmin) throw new GraphQLYogaError('Not allowed')
if (!isAdmin) throw new GraphQLError('Not allowed')
const connectedAccount = await stripe.accounts.retrieve(id)
@@ -46,14 +46,14 @@ builder.objectType('Stripe', {
resolve: async (_parent, { id }, context) => {
const { isAllowed } = context
if (!await isAllowed(id, context)) {
throw new GraphQLYogaError('Not allowed')
if (!(await isAllowed(id, context))) {
throw new GraphQLError('Not allowed')
}
const customer = await stripe.customers.retrieve(id)
if (customer.deleted) {
throw new GraphQLYogaError('The Stripe customer is deleted')
throw new GraphQLError('The Stripe customer is deleted')
}
return customer
@@ -129,8 +129,8 @@ builder.objectType('StripeMutations', {
resolve: async (_, { customer, configuration, locale, returnUrl }, context) => {
const { isAllowed } = context
if (!await isAllowed(customer, context)) {
throw new GraphQLYogaError('Not allowed')
if (!(await isAllowed(customer, context))) {
throw new GraphQLError('Not allowed')
}
const session = await stripe.billingPortal.sessions.create({

View File

@@ -1,4 +1,5 @@
import { createServer, YogaInitialContext } from '@graphql-yoga/node'
import { createServer } from 'node:http'
import { createYoga, YogaInitialContext } from 'graphql-yoga'
import { schema } from './schema'
import { Context, CreateServerProps } from './types'
@@ -45,12 +46,15 @@ const createStripeGraphQLServer = (params?: CreateServerProps) => {
}
}
return createServer({
const yoga = createYoga({
cors,
graphiql,
context,
schema
schema,
graphqlEndpoint: '*'
})
return createServer(yoga)
}
export { createStripeGraphQLServer, schema }

View File

@@ -1,6 +1,6 @@
import type Stripe from 'stripe'
import type { CORSOptions, YogaInitialContext } from '@graphql-yoga/node'
import type { CORSOptions, YogaInitialContext } from 'graphql-yoga'
export type StripeGraphQLContext = {
isAllowed: (stripeCustomerId: string, context: Context) => boolean | Promise<boolean>

View File

@@ -8,7 +8,7 @@ if (!process.env.STRIPE_SECRET_KEY) {
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
apiVersion: '2022-11-15'
})
export const getUserClaims = (req: Request): UserHasuraClaims | undefined => {

473
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff