Compare commits
7 Commits
auth@0.43.
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8b243571 | ||
|
|
c9967b1a6d | ||
|
|
7f72aadff9 | ||
|
|
8faf9565bb | ||
|
|
7ac3f12852 | ||
|
|
184a3ed190 | ||
|
|
372c4e32d4 |
2
.github/workflows/ci_update_changelog.yaml
vendored
2
.github/workflows/ci_update_changelog.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
cd ${{ matrix.project }}
|
||||
TAG_NAME=$(make release-tag-name)
|
||||
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
||||
if git tag | grep -q "$TAG_NAME@$VERSION"; then
|
||||
if git tag | grep -qx "$TAG_NAME@$VERSION"; then
|
||||
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
|
||||
else
|
||||
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
## [@nhost/dashboard@2.41.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
|
||||
- *(dashboard)* Get github repositories from github itself (#3640)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(dashboard)* Update SQL editor to use correct hasura migrations API URL (#3645)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
@@ -15,7 +15,7 @@ function getCspHeader() {
|
||||
return [
|
||||
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
|
||||
"script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com",
|
||||
"connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com",
|
||||
"connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com api.github.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
||||
"font-src 'self' data:",
|
||||
@@ -126,4 +126,4 @@ module.exports = withBundleAnalyzer({
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export default function SocialProvidersSettings() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return nhost.auth.signInProviderURL('github', {
|
||||
connect: token,
|
||||
redirectTo: `${window.location.origin}/account`,
|
||||
redirectTo: `${window.location.origin}/account?signinProvider=github`,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -3,18 +3,21 @@ import {
|
||||
useGithubAuthentication,
|
||||
type UseGithubAuthenticationHookProps,
|
||||
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SiGithub } from '@icons-pack/react-simple-icons';
|
||||
|
||||
interface Props extends UseGithubAuthenticationHookProps {
|
||||
buttonText?: string;
|
||||
withAnonId?: boolean;
|
||||
redirectTo?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function GithubAuthButton({
|
||||
buttonText = 'Continue with GitHub',
|
||||
withAnonId = false,
|
||||
redirectTo,
|
||||
className,
|
||||
}: Props) {
|
||||
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
|
||||
withAnonId,
|
||||
@@ -22,7 +25,10 @@ function GithubAuthButton({
|
||||
});
|
||||
return (
|
||||
<Button
|
||||
className="gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
|
||||
className={cn(
|
||||
'gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
onClick={() => signInWithGithub()}
|
||||
|
||||
@@ -30,8 +30,8 @@ function useGithubAuthentication({
|
||||
};
|
||||
}
|
||||
|
||||
const redirectURl = nhost.auth.signInProviderURL('github', options);
|
||||
window.location.href = redirectURl;
|
||||
const redirectURL = nhost.auth.signInProviderURL('github', options);
|
||||
window.location.href = redirectURL;
|
||||
},
|
||||
{
|
||||
onError: () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/component
|
||||
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||
|
||||
function SignInWithGithub() {
|
||||
const redirectTo = useHostName();
|
||||
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||
return (
|
||||
<GithubAuthButton
|
||||
redirectTo={redirectTo}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
||||
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||
|
||||
function SignUpWithGithub() {
|
||||
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||
return (
|
||||
<GithubAuthButton
|
||||
redirectTo={redirectTo}
|
||||
buttonText="Sign Up with GitHub"
|
||||
errorText="An error occurred while trying to sign up using GitHub. Please try again."
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ErrorMessage } from '@/components/presentational/ErrorMessage';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
||||
@@ -11,14 +12,33 @@ import { Link } from '@/components/ui/v2/Link';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
||||
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||
import { EditRepositorySettings } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
||||
import { useGetGithubRepositoriesQuery } from '@/generated/graphql';
|
||||
import {
|
||||
getGitHubToken,
|
||||
saveGitHubToken,
|
||||
} from '@/features/orgs/projects/git/common/utils';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useGetAuthUserProvidersQuery } from '@/generated/graphql';
|
||||
import { useAccessToken } from '@/hooks/useAccessToken';
|
||||
import { GitHubAPIError, listGitHubInstallationRepos } from '@/lib/github';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { Divider } from '@mui/material';
|
||||
import debounce from 'lodash.debounce';
|
||||
import NavLink from 'next/link';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export type ConnectGitHubModalState = 'CONNECTING' | 'EDITING';
|
||||
export type ConnectGitHubModalState =
|
||||
| 'CONNECTING'
|
||||
| 'EDITING'
|
||||
| 'EXPIRED_GITHUB_SESSION'
|
||||
| 'GITHUB_CONNECTION_REQUIRED';
|
||||
|
||||
export interface ConnectGitHubModalProps {
|
||||
/**
|
||||
@@ -28,18 +48,153 @@ export interface ConnectGitHubModalProps {
|
||||
close?: VoidFunction;
|
||||
}
|
||||
|
||||
interface GitHubData {
|
||||
githubAppInstallations: Array<{
|
||||
id: number;
|
||||
accountLogin?: string;
|
||||
accountAvatarUrl?: string;
|
||||
}>;
|
||||
githubRepositories: Array<{
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
githubAppInstallation: {
|
||||
accountLogin?: string;
|
||||
accountAvatarUrl?: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
const [filter, setFilter] = useState('');
|
||||
const [ConnectGitHubModalState, setConnectGitHubModalState] =
|
||||
useState<ConnectGitHubModalState>('CONNECTING');
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||
const [githubData, setGithubData] = useState<GitHubData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { project, loading: loadingProject } = useProject();
|
||||
const { org, loading: loadingOrg } = useCurrentOrg();
|
||||
const hostname = useHostName();
|
||||
const token = useAccessToken();
|
||||
const {
|
||||
data,
|
||||
loading: loadingGithubConnected,
|
||||
error: errorGithubConnected,
|
||||
} = useGetAuthUserProvidersQuery();
|
||||
|
||||
const { data, loading, error, startPolling } =
|
||||
useGetGithubRepositoriesQuery();
|
||||
const githubProvider = data?.authUserProviders?.find(
|
||||
(item) => item.providerId === 'github',
|
||||
);
|
||||
|
||||
const getGitHubConnectUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return nhost.auth.signInProviderURL('github', {
|
||||
connect: token,
|
||||
redirectTo: `${window.location.origin}?signinProvider=github&state=signin-refresh:${org.slug}:${project?.subdomain}`,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(2000);
|
||||
}, [startPolling]);
|
||||
if (loadingGithubConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchGitHubData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (isEmptyValue(githubProvider)) {
|
||||
setConnectGitHubModalState('GITHUB_CONNECTION_REQUIRED');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const githubToken = getGitHubToken();
|
||||
|
||||
if (
|
||||
!githubToken?.authUserProviderId ||
|
||||
githubProvider!.id !== githubToken.authUserProviderId
|
||||
) {
|
||||
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { refreshToken, expiresAt: expiresAtString } = githubToken;
|
||||
let accessToken = githubToken?.accessToken;
|
||||
|
||||
const expiresAt = new Date(expiresAtString).getTime();
|
||||
|
||||
const currentTime = Date.now();
|
||||
const expiresAtMargin = 60 * 1000;
|
||||
if (expiresAt - currentTime < expiresAtMargin) {
|
||||
if (!refreshToken) {
|
||||
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshResponse = await nhost.auth.refreshProviderToken(
|
||||
'github',
|
||||
{ refreshToken },
|
||||
);
|
||||
|
||||
if (!refreshResponse.body) {
|
||||
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
saveGitHubToken({
|
||||
...refreshResponse.body,
|
||||
authUserProviderId: githubProvider!.id,
|
||||
});
|
||||
|
||||
accessToken = refreshResponse.body.accessToken;
|
||||
}
|
||||
|
||||
const installations = await listGitHubInstallationRepos(accessToken);
|
||||
|
||||
const transformedData = {
|
||||
githubAppInstallations: installations.map((item) => ({
|
||||
id: item.installation.id,
|
||||
accountLogin: item.installation.account?.login,
|
||||
accountAvatarUrl: item.installation.account?.avatar_url,
|
||||
})),
|
||||
githubRepositories: installations.flatMap((item) =>
|
||||
item.repositories.map((repo) => ({
|
||||
id: repo.id,
|
||||
node_id: repo.node_id,
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
githubAppInstallation: {
|
||||
accountLogin: item.installation.account?.login,
|
||||
accountAvatarUrl: item.installation.account?.avatar_url,
|
||||
},
|
||||
})),
|
||||
),
|
||||
};
|
||||
|
||||
setGithubData(transformedData);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Error fetching GitHub data:', err);
|
||||
if (err instanceof GitHubAPIError && err.status === 401) {
|
||||
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(err?.message, getToastStyleProps());
|
||||
close?.();
|
||||
}
|
||||
};
|
||||
|
||||
fetchGitHubData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [githubProvider, loadingGithubConnected]);
|
||||
|
||||
const handleSelectAnotherRepository = () => {
|
||||
setSelectedRepoId(null);
|
||||
@@ -56,13 +211,91 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (errorGithubConnected instanceof Error) {
|
||||
return (
|
||||
<div className="px-1 md:w-[653px]">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto text-center">
|
||||
<div className="mx-auto h-8 w-8">
|
||||
<GitHubIcon className="h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text className="mt-2.5 text-center text-lg font-medium">
|
||||
Error fetching GitHub data
|
||||
</Text>
|
||||
<ErrorMessage>{errorGithubConnected.message}</ErrorMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (loading || loadingProject || loadingOrg || loadingGithubConnected) {
|
||||
return (
|
||||
<ActivityIndicator delay={500} label="Loading GitHub repositories..." />
|
||||
<div className="px-1 md:w-[653px]">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto text-center">
|
||||
<div className="mx-auto h-8 w-8">
|
||||
<GitHubIcon className="h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="mt-2.5 text-center text-lg font-medium">
|
||||
Loading repositories...
|
||||
</Text>
|
||||
<Text className="text-center text-xs font-normal" color="secondary">
|
||||
Fetching your GitHub repositories
|
||||
</Text>
|
||||
<div className="mb-2 mt-6 flex w-full">
|
||||
<Input placeholder="Search..." fullWidth disabled value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-import items-center justify-center border-y">
|
||||
<ActivityIndicator delay={0} label="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ConnectGitHubModalState === 'GITHUB_CONNECTION_REQUIRED') {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
|
||||
<p className="text-center text-foreground">
|
||||
You need to connect your GitHub account to continue.
|
||||
</p>
|
||||
<NavLink
|
||||
href={getGitHubConnectUrl()}
|
||||
passHref
|
||||
rel="noreferrer noopener"
|
||||
legacyBehavior
|
||||
>
|
||||
<Button
|
||||
className="w-full max-w-72"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
startIcon={<GitHubIcon />}
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (ConnectGitHubModalState === 'EXPIRED_GITHUB_SESSION') {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
|
||||
<p className="text-center text-foreground">
|
||||
Please sign in with GitHub to continue.
|
||||
</p>
|
||||
<GithubAuthButton
|
||||
redirectTo={`${hostname}?signinProvider=github&state=signin-refresh:${org.slug}:${project!.subdomain}`}
|
||||
buttonText="Sign in with GitHub"
|
||||
className="w-full max-w-72 gap-2 !bg-primary !text-white disabled:!text-white disabled:!text-opacity-60 dark:!bg-white dark:!text-black dark:disabled:!text-black"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,25 +311,27 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const { githubAppInstallations } = data || {};
|
||||
const { githubAppInstallations } = githubData || {};
|
||||
|
||||
const filteredGitHubAppInstallations = data?.githubAppInstallations.filter(
|
||||
(githubApp) => !!githubApp.accountLogin,
|
||||
);
|
||||
const filteredGitHubAppInstallations =
|
||||
githubData?.githubAppInstallations.filter(
|
||||
(githubApp) => !!githubApp.accountLogin,
|
||||
);
|
||||
|
||||
const filteredGitHubRepositories = data?.githubRepositories.filter(
|
||||
const filteredGitHubRepositories = githubData?.githubRepositories.filter(
|
||||
(repo) => !!repo.githubAppInstallation,
|
||||
);
|
||||
|
||||
const filteredGitHubAppInstallationsNullValues =
|
||||
data?.githubAppInstallations.filter((githubApp) => !!githubApp.accountLogin)
|
||||
.length === 0;
|
||||
githubData?.githubAppInstallations.filter(
|
||||
(githubApp) => !!githubApp.accountLogin,
|
||||
).length === 0;
|
||||
|
||||
const faultyGitHubInstallation =
|
||||
githubAppInstallations?.length === 0 ||
|
||||
filteredGitHubAppInstallationsNullValues;
|
||||
|
||||
const noRepositoriesAdded = data?.githubRepositories.length === 0;
|
||||
const noRepositoriesAdded = githubData?.githubRepositories.length === 0;
|
||||
|
||||
if (faultyGitHubInstallation) {
|
||||
return (
|
||||
@@ -115,11 +350,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
||||
// Both `target` and `rel` are available when `href` is set. This is
|
||||
// a limitation of MUI.
|
||||
// @ts-ignore
|
||||
target="_blank"
|
||||
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||
rel="noreferrer noopener"
|
||||
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
||||
>
|
||||
@@ -179,8 +410,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
</List>
|
||||
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
||||
target="_blank"
|
||||
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||
rel="noreferrer noopener"
|
||||
underline="hover"
|
||||
className="grid grid-flow-col items-center justify-start gap-1"
|
||||
@@ -199,8 +429,8 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
className="text-center text-xs font-normal"
|
||||
color="secondary"
|
||||
>
|
||||
Showing repositories from {data?.githubAppInstallations.length}{' '}
|
||||
GitHub account(s)
|
||||
Showing repositories from{' '}
|
||||
{githubData?.githubAppInstallations.length} GitHub account(s)
|
||||
</Text>
|
||||
<div className="mb-2 mt-6 flex w-full">
|
||||
<Input
|
||||
@@ -226,7 +456,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => setSelectedRepoId(repo.id)}
|
||||
onClick={() => setSelectedRepoId(repo.node_id)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
@@ -268,8 +498,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||
Do you miss a repository, or do you need to connect another GitHub
|
||||
account?{' '}
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
||||
target="_blank"
|
||||
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||
rel="noreferrer noopener"
|
||||
className="text-xs font-medium"
|
||||
underline="hover"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
export interface EditRepositorySettingsProps {
|
||||
close?: () => void;
|
||||
openConnectGithubModal?: () => void;
|
||||
selectedRepoId?: string;
|
||||
selectedRepoId: string;
|
||||
connectGithubModalState?: ConnectGitHubModalState;
|
||||
handleSelectAnotherRepository?: () => void;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
|
||||
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useUpdateApplicationMutation } from '@/generated/graphql';
|
||||
import { useConnectGithubRepoMutation } from '@/generated/graphql';
|
||||
import { analytics } from '@/lib/segment';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface EditRepositorySettingsModalProps {
|
||||
selectedRepoId?: string;
|
||||
selectedRepoId: string;
|
||||
close?: () => void;
|
||||
handleSelectAnotherRepository?: () => void;
|
||||
}
|
||||
@@ -33,45 +33,29 @@ export default function EditRepositorySettingsModal({
|
||||
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateApp, { loading }] = useUpdateApplicationMutation();
|
||||
const [connectGithubRepo, { loading }] = useConnectGithubRepoMutation();
|
||||
|
||||
const handleEditGitHubIntegration = async (
|
||||
data: EditRepositorySettingsFormData,
|
||||
) => {
|
||||
try {
|
||||
if (!project?.githubRepository || selectedRepoId) {
|
||||
await updateApp({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
app: {
|
||||
githubRepositoryId: selectedRepoId,
|
||||
repositoryProductionBranch: data.productionBranch,
|
||||
nhostBaseFolder: data.repoBaseFolder,
|
||||
},
|
||||
},
|
||||
});
|
||||
await connectGithubRepo({
|
||||
variables: {
|
||||
appID: project?.id,
|
||||
githubNodeID: selectedRepoId,
|
||||
productionBranch: data.productionBranch,
|
||||
baseFolder: data.repoBaseFolder,
|
||||
},
|
||||
});
|
||||
|
||||
if (selectedRepoId) {
|
||||
analytics.track('Project Connected to GitHub', {
|
||||
projectId: project?.id,
|
||||
projectName: project?.name,
|
||||
projectSubdomain: project?.subdomain,
|
||||
repositoryId: selectedRepoId,
|
||||
productionBranch: data.productionBranch,
|
||||
baseFolder: data.repoBaseFolder,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await updateApp({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
app: {
|
||||
repositoryProductionBranch: data.productionBranch,
|
||||
nhostBaseFolder: data.repoBaseFolder,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
analytics.track('Project Connected to GitHub', {
|
||||
projectId: project?.id,
|
||||
projectName: project?.name,
|
||||
projectSubdomain: project?.subdomain,
|
||||
repositoryId: selectedRepoId,
|
||||
productionBranch: data.productionBranch,
|
||||
baseFolder: data.repoBaseFolder,
|
||||
});
|
||||
|
||||
await refetchProject();
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation ConnectGithubRepo(
|
||||
$appID: uuid!
|
||||
$githubNodeID: String!
|
||||
$productionBranch: String!
|
||||
$baseFolder: String!
|
||||
) {
|
||||
connectGithubRepo(
|
||||
appID: $appID
|
||||
githubNodeID: $githubNodeID
|
||||
productionBranch: $productionBranch
|
||||
baseFolder: $baseFolder
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal';
|
||||
|
||||
export default function useGitHubModal() {
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { openAlertDialog, closeAlertDialog } = useDialog();
|
||||
|
||||
function openGitHubModal() {
|
||||
openAlertDialog({
|
||||
title: 'Connect GitHub Repository',
|
||||
payload: <ConnectGitHubModal />,
|
||||
payload: <ConnectGitHubModal close={closeAlertDialog} />,
|
||||
props: {
|
||||
hidePrimaryAction: true,
|
||||
hideSecondaryAction: true,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import type { ProviderSession } from '@nhost/nhost-js/auth';
|
||||
|
||||
const githubProviderTokenKey = 'nhost_provider_tokens_github';
|
||||
|
||||
export type GitHubProviderToken = ProviderSession & {
|
||||
authUserProviderId?: string;
|
||||
};
|
||||
|
||||
export function saveGitHubToken(token: GitHubProviderToken) {
|
||||
localStorage.setItem(githubProviderTokenKey, JSON.stringify(token));
|
||||
}
|
||||
|
||||
export function getGitHubToken() {
|
||||
const token = localStorage.getItem(githubProviderTokenKey);
|
||||
return isNotEmptyValue(token)
|
||||
? (JSON.parse(token) as GitHubProviderToken)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function clearGitHubToken() {
|
||||
localStorage.removeItem(githubProviderTokenKey);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './githubTokens';
|
||||
99
dashboard/src/lib/github.ts
Normal file
99
dashboard/src/lib/github.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Custom error class for GitHub API errors that preserves HTTP status codes
|
||||
*/
|
||||
export class GitHubAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public statusText: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'GitHubAPIError';
|
||||
}
|
||||
}
|
||||
|
||||
interface GitHubAppInstallation {
|
||||
id: number;
|
||||
account?: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all GitHub App installations accessible to the user
|
||||
* @param accessToken - The GitHub OAuth access token
|
||||
* @returns Array of app installations
|
||||
*/
|
||||
export async function listGitHubAppInstallations(accessToken: string): Promise<GitHubAppInstallation[]> {
|
||||
const response = await fetch('https://api.github.com/user/installations', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
cache: 'no-cache',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new GitHubAPIError(
|
||||
`Failed to list installations: ${response.statusText}`,
|
||||
response.status,
|
||||
response.statusText
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.installations;
|
||||
}
|
||||
|
||||
interface GitHubRepo {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all repositories accessible through GitHub App installations
|
||||
* @param accessToken - The GitHub OAuth access token
|
||||
* @returns Array of repositories grouped by installation
|
||||
*/
|
||||
export async function listGitHubInstallationRepos(accessToken: string) {
|
||||
const installations = await listGitHubAppInstallations(accessToken);
|
||||
|
||||
const reposByInstallation = await Promise.all(
|
||||
installations.map(async (installation) => {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/user/installations/${installation.id}/repositories`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
cache: 'no-cache',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new GitHubAPIError(
|
||||
`Failed to list repos for installation ${installation.id}: ${response.statusText}`,
|
||||
response.status,
|
||||
response.statusText
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
installation,
|
||||
repositories: data.repositories as GitHubRepo[],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return reposByInstallation;
|
||||
}
|
||||
@@ -77,12 +77,12 @@ function MyApp({
|
||||
|
||||
<CacheProvider value={emotionCache}>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<AuthProvider>
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-and-network"
|
||||
nhost={nhost}
|
||||
connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'}
|
||||
>
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-and-network"
|
||||
nhost={nhost}
|
||||
connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'}
|
||||
>
|
||||
<AuthProvider>
|
||||
<UIProvider>
|
||||
<Toaster position="bottom-center" />
|
||||
<ThemeProvider
|
||||
@@ -106,8 +106,8 @@ function MyApp({
|
||||
</RetryableErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</UIProvider>
|
||||
</NhostApolloProvider>
|
||||
</AuthProvider>
|
||||
</AuthProvider>
|
||||
</NhostApolloProvider>
|
||||
</NhostProvider>
|
||||
</CacheProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
|
||||
import { useAuth } from '@/providers/Auth';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function authProtected<P extends JSX.IntrinsicAttributes>(
|
||||
Comp: ComponentType<P>,
|
||||
) {
|
||||
return function AuthProtected(props: P) {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/signin');
|
||||
}, [isLoading, isAuthenticated, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return <Comp {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [state, setState] = useState({
|
||||
error: null,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { installation_id: installationId } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
async function installGithubApp() {
|
||||
try {
|
||||
await nhost.functions.fetch('/client/github-app-installation', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
installationId,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
setState({
|
||||
error,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
window.close();
|
||||
}
|
||||
|
||||
// run in async manner
|
||||
installGithubApp();
|
||||
}, [installationId]);
|
||||
|
||||
if (state.loading) {
|
||||
return <ActivityIndicator delay={500} label="Loading..." />;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw state.error;
|
||||
}
|
||||
|
||||
return <div>GitHub connection completed. You can close this tab.</div>;
|
||||
}
|
||||
|
||||
export default authProtected(Page);
|
||||
@@ -1,12 +1,38 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
|
||||
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
|
||||
import { useGitHubModal } from '@/features/orgs/projects/git/common/hooks/useGitHubModal';
|
||||
import { BaseDirectorySettings } from '@/features/orgs/projects/git/settings/components/BaseDirectorySettings';
|
||||
import { DeploymentBranchSettings } from '@/features/orgs/projects/git/settings/components/DeploymentBranchSettings';
|
||||
import { GitConnectionSettings } from '@/features/orgs/projects/git/settings/components/GitConnectionSettings';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, type ReactElement } from 'react';
|
||||
|
||||
export default function GitSettingsPage() {
|
||||
const router = useRouter();
|
||||
const { pathname, replace, isReady: isRouterReady } = router;
|
||||
const { 'github-modal': githubModal, ...remainingQuery } = router.query;
|
||||
const { openGitHubModal } = useGitHubModal();
|
||||
|
||||
const removeQueryParamsFromURL = useCallback(() => {
|
||||
replace({ pathname, query: remainingQuery }, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}, [replace, remainingQuery, pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRouterReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof githubModal === 'string') {
|
||||
removeQueryParamsFromURL();
|
||||
|
||||
openGitHubModal();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [githubModal, isRouterReady]);
|
||||
|
||||
return (
|
||||
<Container
|
||||
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import {
|
||||
clearGitHubToken,
|
||||
saveGitHubToken,
|
||||
type GitHubProviderToken,
|
||||
} from '@/features/orgs/projects/git/common/utils';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { useNhostClient } from '@/providers/nhost/';
|
||||
import { useGetAuthUserProvidersLazyQuery } from '@/utils/__generated__/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { type Session } from '@nhost/nhost-js/auth';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { AuthContext, type AuthContextType } from './AuthContext';
|
||||
|
||||
function AuthProvider({ children }: PropsWithChildren) {
|
||||
const nhost = useNhostClient();
|
||||
const [getAuthUserProviders] = useGetAuthUserProvidersLazyQuery();
|
||||
const {
|
||||
query,
|
||||
isReady: isRouterReady,
|
||||
@@ -21,7 +29,15 @@ function AuthProvider({ children }: PropsWithChildren) {
|
||||
pathname,
|
||||
push,
|
||||
} = useRouter();
|
||||
const { refreshToken, error, errorDescription, ...remainingQuery } = query;
|
||||
const {
|
||||
refreshToken,
|
||||
error,
|
||||
errorDescription,
|
||||
signinProvider,
|
||||
state,
|
||||
provider_state: providerState,
|
||||
...remainingQuery
|
||||
} = query;
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
@@ -55,7 +71,6 @@ function AuthProvider({ children }: PropsWithChildren) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
// reset state if we have just signed out
|
||||
setIsSigningOut(false);
|
||||
if (refreshToken && typeof refreshToken === 'string') {
|
||||
const sessionResponse = await nhost.auth.refreshToken({
|
||||
@@ -63,24 +78,84 @@ function AuthProvider({ children }: PropsWithChildren) {
|
||||
});
|
||||
setSession(sessionResponse.body);
|
||||
removeQueryParamsFromURL();
|
||||
|
||||
if (sessionResponse.body && signinProvider === 'github') {
|
||||
try {
|
||||
const providerTokensResponse =
|
||||
await nhost.auth.getProviderTokens(signinProvider);
|
||||
if (providerTokensResponse.body) {
|
||||
const { data } = await getAuthUserProviders();
|
||||
const githubProvider = data?.authUserProviders?.find(
|
||||
(provider) => provider.providerId === 'github',
|
||||
);
|
||||
const newGitHubToken: GitHubProviderToken =
|
||||
providerTokensResponse.body;
|
||||
if (isNotEmptyValue(githubProvider?.id)) {
|
||||
newGitHubToken.authUserProviderId = githubProvider!.id;
|
||||
}
|
||||
saveGitHubToken(newGitHubToken);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch provider tokens:', err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const currentSession = nhost.getUserSession();
|
||||
setSession(currentSession);
|
||||
}
|
||||
|
||||
// handle OAuth redirect errors (e.g., error=unverified-user)
|
||||
if (
|
||||
state &&
|
||||
typeof state === 'string' &&
|
||||
state.startsWith('signin-refresh:')
|
||||
) {
|
||||
const [, orgSlug, projectSubdomain] = state.split(':');
|
||||
removeQueryParamsFromURL();
|
||||
await push(
|
||||
`/orgs/${orgSlug}/projects/${projectSubdomain}/settings/git?github-modal`,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
if (error === 'unverified-user') {
|
||||
removeQueryParamsFromURL();
|
||||
await push('/email/verify');
|
||||
} else {
|
||||
const description =
|
||||
typeof errorDescription === 'string'
|
||||
? errorDescription
|
||||
: 'An error occurred during the sign-in process. Please try again.';
|
||||
toast.error(description, getToastStyleProps());
|
||||
removeQueryParamsFromURL();
|
||||
await push('/signin');
|
||||
switch (error) {
|
||||
case 'unverified-user': {
|
||||
removeQueryParamsFromURL();
|
||||
await push('/email/verify');
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the state isn't handled by Hasura auth, it returns `invalid-state`.
|
||||
* However, we check the provider_state search param to see if it has this format:
|
||||
* `install-github-app:<org-slug>:<project-subdomain>`.
|
||||
* If it has this format, that means that we connected to GitHub in `/settings/git`,
|
||||
* thus we need to show the connect GitHub modal again.
|
||||
* Otherwise, we fall through to default error handling.
|
||||
*/
|
||||
case 'invalid-state': {
|
||||
if (
|
||||
isNotEmptyValue(providerState) &&
|
||||
typeof providerState === 'string' &&
|
||||
providerState.startsWith('install-github-app:')
|
||||
) {
|
||||
const [, orgSlug, projectSubdomain] = providerState.split(':');
|
||||
removeQueryParamsFromURL();
|
||||
await push(
|
||||
`/orgs/${orgSlug}/projects/${projectSubdomain}/settings/git?github-modal`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
// Fall through to default error handling if state search param is invalid
|
||||
}
|
||||
default: {
|
||||
const description =
|
||||
typeof errorDescription === 'string'
|
||||
? errorDescription
|
||||
: 'An error occurred during the sign-in process. Please try again.';
|
||||
toast.error(description, getToastStyleProps());
|
||||
removeQueryParamsFromURL();
|
||||
await push('/signin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +178,7 @@ function AuthProvider({ children }: PropsWithChildren) {
|
||||
nhost.auth.signOut({
|
||||
refreshToken: session!.refreshToken,
|
||||
});
|
||||
clearGitHubToken();
|
||||
|
||||
await push('/signin');
|
||||
},
|
||||
|
||||
@@ -108,16 +108,16 @@ function Providers({ children }: PropsWithChildren<{}>) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider value={emotionCache}>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<AuthProvider>
|
||||
<ApolloProvider client={mockClient}>
|
||||
<ApolloProvider client={mockClient}>
|
||||
<AuthProvider>
|
||||
<UIProvider>
|
||||
<Toaster position="bottom-center" />
|
||||
<ThemeProvider theme={theme}>
|
||||
<DialogProvider>{children}</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</UIProvider>
|
||||
</ApolloProvider>
|
||||
</AuthProvider>
|
||||
</AuthProvider>
|
||||
</ApolloProvider>
|
||||
</NhostProvider>
|
||||
</CacheProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
82
dashboard/src/utils/__generated__/graphql.ts
generated
82
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -3118,7 +3118,9 @@ export type ConfigSystemConfigPostgres = {
|
||||
database: Scalars['String'];
|
||||
disk?: Maybe<ConfigSystemConfigPostgresDisk>;
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
encryptColumnKey?: Maybe<Scalars['String']>;
|
||||
majorVersion?: Maybe<Scalars['String']>;
|
||||
oldEncryptColumnKey?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresComparisonExp = {
|
||||
@@ -3129,7 +3131,9 @@ export type ConfigSystemConfigPostgresComparisonExp = {
|
||||
database?: InputMaybe<ConfigStringComparisonExp>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
encryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
|
||||
majorVersion?: InputMaybe<ConfigStringComparisonExp>;
|
||||
oldEncryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresConnectionString = {
|
||||
@@ -3193,7 +3197,9 @@ export type ConfigSystemConfigPostgresInsertInput = {
|
||||
database: Scalars['String'];
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
encryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||
majorVersion?: InputMaybe<Scalars['String']>;
|
||||
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
@@ -3201,7 +3207,9 @@ export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
database?: InputMaybe<Scalars['String']>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
encryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||
majorVersion?: InputMaybe<Scalars['String']>;
|
||||
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigUpdateInput = {
|
||||
@@ -4426,6 +4434,7 @@ export type Apps = {
|
||||
billingDedicatedCompute?: Maybe<Billing_Dedicated_Compute>;
|
||||
/** An object relationship */
|
||||
billingSubscriptions?: Maybe<Billing_Subscriptions>;
|
||||
/** main entrypoint to the configuration */
|
||||
config?: Maybe<ConfigConfig>;
|
||||
createdAt: Scalars['timestamptz'];
|
||||
/** An object relationship */
|
||||
@@ -11094,6 +11103,7 @@ export type Deployments = {
|
||||
commitSHA: Scalars['String'];
|
||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||
commitUserName?: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['timestamptz'];
|
||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||
/** An array relationship */
|
||||
deploymentLogs: Array<DeploymentLogs>;
|
||||
@@ -11191,6 +11201,7 @@ export type Deployments_Bool_Exp = {
|
||||
commitSHA?: InputMaybe<String_Comparison_Exp>;
|
||||
commitUserAvatarUrl?: InputMaybe<String_Comparison_Exp>;
|
||||
commitUserName?: InputMaybe<String_Comparison_Exp>;
|
||||
createdAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
deploymentEndedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
deploymentLogs?: InputMaybe<DeploymentLogs_Bool_Exp>;
|
||||
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Bool_Exp>;
|
||||
@@ -11222,6 +11233,7 @@ export type Deployments_Insert_Input = {
|
||||
commitSHA?: InputMaybe<Scalars['String']>;
|
||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||
commitUserName?: InputMaybe<Scalars['String']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentLogs?: InputMaybe<DeploymentLogs_Arr_Rel_Insert_Input>;
|
||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
@@ -11246,6 +11258,7 @@ export type Deployments_Max_Fields = {
|
||||
commitSHA?: Maybe<Scalars['String']>;
|
||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||
commitUserName?: Maybe<Scalars['String']>;
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentStatus?: Maybe<Scalars['String']>;
|
||||
@@ -11268,6 +11281,7 @@ export type Deployments_Max_Order_By = {
|
||||
commitSHA?: InputMaybe<Order_By>;
|
||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||
commitUserName?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||
deploymentStatus?: InputMaybe<Order_By>;
|
||||
@@ -11291,6 +11305,7 @@ export type Deployments_Min_Fields = {
|
||||
commitSHA?: Maybe<Scalars['String']>;
|
||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||
commitUserName?: Maybe<Scalars['String']>;
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
||||
deploymentStatus?: Maybe<Scalars['String']>;
|
||||
@@ -11313,6 +11328,7 @@ export type Deployments_Min_Order_By = {
|
||||
commitSHA?: InputMaybe<Order_By>;
|
||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||
commitUserName?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||
deploymentStatus?: InputMaybe<Order_By>;
|
||||
@@ -11359,6 +11375,7 @@ export type Deployments_Order_By = {
|
||||
commitSHA?: InputMaybe<Order_By>;
|
||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||
commitUserName?: InputMaybe<Order_By>;
|
||||
createdAt?: InputMaybe<Order_By>;
|
||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Order_By>;
|
||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||
@@ -11393,6 +11410,8 @@ export enum Deployments_Select_Column {
|
||||
/** column name */
|
||||
CommitUserName = 'commitUserName',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
DeploymentEndedAt = 'deploymentEndedAt',
|
||||
/** column name */
|
||||
DeploymentStartedAt = 'deploymentStartedAt',
|
||||
@@ -11427,6 +11446,7 @@ export type Deployments_Set_Input = {
|
||||
commitSHA?: InputMaybe<Scalars['String']>;
|
||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||
commitUserName?: InputMaybe<Scalars['String']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentStatus?: InputMaybe<Scalars['String']>;
|
||||
@@ -11457,6 +11477,7 @@ export type Deployments_Stream_Cursor_Value_Input = {
|
||||
commitSHA?: InputMaybe<Scalars['String']>;
|
||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||
commitUserName?: InputMaybe<Scalars['String']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
deploymentStatus?: InputMaybe<Scalars['String']>;
|
||||
@@ -11485,6 +11506,8 @@ export enum Deployments_Update_Column {
|
||||
/** column name */
|
||||
CommitUserName = 'commitUserName',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
DeploymentEndedAt = 'deploymentEndedAt',
|
||||
/** column name */
|
||||
DeploymentStartedAt = 'deploymentStartedAt',
|
||||
@@ -13067,6 +13090,7 @@ export type Mutation_Root = {
|
||||
billingUpgradeFreeOrganization: Scalars['String'];
|
||||
billingUploadReports: Scalars['Boolean'];
|
||||
changeDatabaseVersion: Scalars['Boolean'];
|
||||
connectGithubRepo: Scalars['Boolean'];
|
||||
/** delete single row from the table: "announcements_read" */
|
||||
deleteAnnouncementRead?: Maybe<Announcements_Read>;
|
||||
/** delete data from the table: "announcements_read" */
|
||||
@@ -13968,6 +13992,15 @@ export type Mutation_RootChangeDatabaseVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootConnectGithubRepoArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
baseFolder: Scalars['String'];
|
||||
githubNodeID: Scalars['String'];
|
||||
productionBranch: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteAnnouncementReadArgs = {
|
||||
id: Scalars['uuid'];
|
||||
@@ -27517,6 +27550,16 @@ export type ResetDatabasePasswordMutationVariables = Exact<{
|
||||
|
||||
export type ResetDatabasePasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean };
|
||||
|
||||
export type ConnectGithubRepoMutationVariables = Exact<{
|
||||
appID: Scalars['uuid'];
|
||||
githubNodeID: Scalars['String'];
|
||||
productionBranch: Scalars['String'];
|
||||
baseFolder: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ConnectGithubRepoMutation = { __typename?: 'mutation_root', connectGithubRepo: boolean };
|
||||
|
||||
export type GetHasuraSettingsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -29446,6 +29489,45 @@ export function useResetDatabasePasswordMutation(baseOptions?: Apollo.MutationHo
|
||||
export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>;
|
||||
export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>;
|
||||
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
|
||||
export const ConnectGithubRepoDocument = gql`
|
||||
mutation ConnectGithubRepo($appID: uuid!, $githubNodeID: String!, $productionBranch: String!, $baseFolder: String!) {
|
||||
connectGithubRepo(
|
||||
appID: $appID
|
||||
githubNodeID: $githubNodeID
|
||||
productionBranch: $productionBranch
|
||||
baseFolder: $baseFolder
|
||||
)
|
||||
}
|
||||
`;
|
||||
export type ConnectGithubRepoMutationFn = Apollo.MutationFunction<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useConnectGithubRepoMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useConnectGithubRepoMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useConnectGithubRepoMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [connectGithubRepoMutation, { data, loading, error }] = useConnectGithubRepoMutation({
|
||||
* variables: {
|
||||
* appID: // value for 'appID'
|
||||
* githubNodeID: // value for 'githubNodeID'
|
||||
* productionBranch: // value for 'productionBranch'
|
||||
* baseFolder: // value for 'baseFolder'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useConnectGithubRepoMutation(baseOptions?: Apollo.MutationHookOptions<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>(ConnectGithubRepoDocument, options);
|
||||
}
|
||||
export type ConnectGithubRepoMutationHookResult = ReturnType<typeof useConnectGithubRepoMutation>;
|
||||
export type ConnectGithubRepoMutationResult = Apollo.MutationResult<ConnectGithubRepoMutation>;
|
||||
export type ConnectGithubRepoMutationOptions = Apollo.BaseMutationOptions<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>;
|
||||
export const GetHasuraSettingsDocument = gql`
|
||||
query GetHasuraSettings($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
## [@nhost/nhost-js@4.1.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(nhost-js)* Added pushChainFunction to functions and graphql clients (#3610)
|
||||
- *(nhost-js)* Added various middlewares to work with headers and customizable createNhostClient (#3612)
|
||||
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(dashboard)* Run audit and lint in dashboard (#3578)
|
||||
- *(nhost-js)* Improvements to Session guard to avoid conflict with ProviderSession (#3662)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(nhost-js)* Generate code from local API definitions (#3583)
|
||||
- *(docs)* Udpated README.md and CONTRIBUTING.md (#3587)
|
||||
- *(nhost-js)* Regenerate types (#3648)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
## [auth@0.43.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(auth)* Encrypt TOTP secret (#3619)
|
||||
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
|
||||
- *(auth)* If the callback state is wrong send back to the redirectTo as provider_state (#3649)
|
||||
- *(internal/lib)* Common oapi middleware for go services (#3663)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(auth)* Dont mutate client URL (#3660)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(docs)* Fix broken link in openapi spec and minor mistakes in postmark integration info (#3621)
|
||||
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## [storage@0.9.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(internal/lib)* Common oapi middleware for go services (#3663)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
Reference in New Issue
Block a user