Compare commits
58 Commits
@nhost/das
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fb316655 | ||
|
|
9c4f350508 | ||
|
|
eb3ba21afc | ||
|
|
a84ac62999 | ||
|
|
f32bfed9a9 | ||
|
|
55632506e4 | ||
|
|
e146d32e69 | ||
|
|
df8a13997c | ||
|
|
c488fd4c0c | ||
|
|
a6e8569822 | ||
|
|
a8023c9a3f | ||
|
|
59347fcd4b | ||
|
|
5b65cac91e | ||
|
|
331ae02e2d | ||
|
|
3b8b3be393 | ||
|
|
9fb4d82d86 | ||
|
|
37d836b9b6 | ||
|
|
3c1996b13b | ||
|
|
e3d90fd5d2 | ||
|
|
af016e1caa | ||
|
|
963f9b5e85 | ||
|
|
ed4c780115 | ||
|
|
260997c6fe | ||
|
|
941f0f5755 | ||
|
|
75344b2bc0 | ||
|
|
65da426e8b | ||
|
|
a711727e94 | ||
|
|
8e8f6fd9c9 | ||
|
|
a1c2e5d8ee | ||
|
|
5c276ae844 | ||
|
|
f34702f3c5 | ||
|
|
6cb70eee01 | ||
|
|
9395c9687f | ||
|
|
eb1eb934a4 | ||
|
|
c62fed2c9a | ||
|
|
16fe1a47da | ||
|
|
0f04e8b8b8 | ||
|
|
e6dad4d696 | ||
|
|
bcb3b79add | ||
|
|
fe658231b4 | ||
|
|
a1188b7d98 | ||
|
|
cd4bdc581d | ||
|
|
4e2f8ccd52 | ||
|
|
8a6d8c7534 | ||
|
|
fa75409f09 | ||
|
|
74662052ae | ||
|
|
2de904c865 | ||
|
|
37ab5fe878 | ||
|
|
be9af96fa7 | ||
|
|
31abbe5f30 | ||
|
|
268b461d5b | ||
|
|
58af592cfa | ||
|
|
0e9d623c69 | ||
|
|
412a290646 | ||
|
|
123add38a4 | ||
|
|
5bdd31ad36 | ||
|
|
5121851c8b | ||
|
|
a21aa05b5a |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type FunctionLog = {
|
||||
name: string;
|
||||
language: string;
|
||||
logs: { date: string; message: string; createdAt: string }[];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
export type FunctionResponseLog = {
|
||||
functionPath: string;
|
||||
createdAt: string;
|
||||
message: string;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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({
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function DisableNewUsersSettings() {
|
||||
<SettingsContainer
|
||||
title="Disable New Users"
|
||||
description="If set, newly registered users are disabled and won’t 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,11 +85,7 @@ export default function CreateRoleForm({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BaseRoleForm
|
||||
submitButtonText="Create"
|
||||
onSubmit={handleSubmit}
|
||||
{...props}
|
||||
/>
|
||||
<BaseRoleForm submitButtonText="Add" onSubmit={handleSubmit} {...props} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 "
|
||||
<strong>{originalRole.name}</strong>" role? This cannot be
|
||||
undone.
|
||||
Are you sure you want to delete the allowed role "
|
||||
<strong>{originalRole.name}</strong>"?.
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,8 +67,8 @@ export default function CreateUserForm({
|
||||
} = form;
|
||||
|
||||
const baseAuthUrl = generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
currentApplication.region.awsName,
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region?.awsName,
|
||||
'auth',
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function useFiles({
|
||||
currentApplication.subdomain,
|
||||
currentApplication.region.awsName,
|
||||
'storage',
|
||||
)}/${file.id}`;
|
||||
)}/files/${file.id}`;
|
||||
|
||||
const fetchParams = new URLSearchParams();
|
||||
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
4
docs/docs/graphql/remote-schemas/_category_.json
Normal file
4
docs/docs/graphql/remote-schemas/_category_.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Remote Schemas",
|
||||
"position": 11
|
||||
}
|
||||
252
docs/docs/graphql/remote-schemas/stripe.mdx
Normal file
252
docs/docs/graphql/remote-schemas/stripe.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
BIN
docs/static/img/graphql/remote-schemas/stripe/remote-schema.png
vendored
Normal file
BIN
docs/static/img/graphql/remote-schemas/stripe/remote-schema.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
473
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user