Compare commits
11 Commits
cli@1.35.0
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8b243571 | ||
|
|
c9967b1a6d | ||
|
|
7f72aadff9 | ||
|
|
8faf9565bb | ||
|
|
7ac3f12852 | ||
|
|
184a3ed190 | ||
|
|
372c4e32d4 | ||
|
|
a68d261d8e | ||
|
|
55bda3f56b | ||
|
|
2311e1dd77 | ||
|
|
824ee142c4 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -32,6 +32,7 @@ Where `PKG` is:
|
|||||||
- `deps`: For changes to dependencies
|
- `deps`: For changes to dependencies
|
||||||
- `docs`: For changes to the documentation
|
- `docs`: For changes to the documentation
|
||||||
- `examples`: For changes to the examples
|
- `examples`: For changes to the examples
|
||||||
|
- `internal/lib`: For changes to Nhost's common libraries (internal)
|
||||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||||
- `nixops`: For changes to the NixOps
|
- `nixops`: For changes to the NixOps
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ runs:
|
|||||||
|
|
||||||
# Define valid types and packages
|
# Define valid types and packages
|
||||||
VALID_TYPES="feat|fix|chore"
|
VALID_TYPES="feat|fix|chore"
|
||||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
|
||||||
|
|
||||||
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
||||||
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
||||||
|
|||||||
1
.github/workflows/auth_checks.yaml
vendored
1
.github/workflows/auth_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
- '.golangci.yaml'
|
- '.golangci.yaml'
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
|
- 'internal/lib/**'
|
||||||
- 'vendor/**'
|
- 'vendor/**'
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
|
|||||||
2
.github/workflows/ci_update_changelog.yaml
vendored
2
.github/workflows/ci_update_changelog.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
cd ${{ matrix.project }}
|
cd ${{ matrix.project }}
|
||||||
TAG_NAME=$(make release-tag-name)
|
TAG_NAME=$(make release-tag-name)
|
||||||
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
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"
|
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
|
||||||
else
|
else
|
||||||
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
||||||
|
|||||||
1
.github/workflows/storage_checks.yaml
vendored
1
.github/workflows/storage_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
- '.golangci.yaml'
|
- '.golangci.yaml'
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
|
- 'internal/lib/**'
|
||||||
- 'vendor/**'
|
- 'vendor/**'
|
||||||
|
|
||||||
# storage
|
# storage
|
||||||
|
|||||||
@@ -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
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function getCspHeader() {
|
|||||||
return [
|
return [
|
||||||
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
|
"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",
|
"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'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
@@ -126,4 +126,4 @@ module.exports = withBundleAnalyzer({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,7 @@ export default function SocialProvidersSettings() {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
return nhost.auth.signInProviderURL('github', {
|
return nhost.auth.signInProviderURL('github', {
|
||||||
connect: token,
|
connect: token,
|
||||||
redirectTo: `${window.location.origin}/account`,
|
redirectTo: `${window.location.origin}/account?signinProvider=github`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ import {
|
|||||||
useGithubAuthentication,
|
useGithubAuthentication,
|
||||||
type UseGithubAuthenticationHookProps,
|
type UseGithubAuthenticationHookProps,
|
||||||
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
|
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { SiGithub } from '@icons-pack/react-simple-icons';
|
import { SiGithub } from '@icons-pack/react-simple-icons';
|
||||||
|
|
||||||
interface Props extends UseGithubAuthenticationHookProps {
|
interface Props extends UseGithubAuthenticationHookProps {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
withAnonId?: boolean;
|
withAnonId?: boolean;
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GithubAuthButton({
|
function GithubAuthButton({
|
||||||
buttonText = 'Continue with GitHub',
|
buttonText = 'Continue with GitHub',
|
||||||
withAnonId = false,
|
withAnonId = false,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
|
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
|
||||||
withAnonId,
|
withAnonId,
|
||||||
@@ -22,7 +25,10 @@ function GithubAuthButton({
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Button
|
<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}
|
disabled={isLoading}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={() => signInWithGithub()}
|
onClick={() => signInWithGithub()}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function useGithubAuthentication({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectURl = nhost.auth.signInProviderURL('github', options);
|
const redirectURL = nhost.auth.signInProviderURL('github', options);
|
||||||
window.location.href = redirectURl;
|
window.location.href = redirectURL;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/component
|
|||||||
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||||
|
|
||||||
function SignInWithGithub() {
|
function SignInWithGithub() {
|
||||||
const redirectTo = useHostName();
|
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||||
return (
|
return (
|
||||||
<GithubAuthButton
|
<GithubAuthButton
|
||||||
redirectTo={redirectTo}
|
redirectTo={redirectTo}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
|
||||||
|
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
|
||||||
|
|
||||||
function SignUpWithGithub() {
|
function SignUpWithGithub() {
|
||||||
|
const redirectTo = `${useHostName()}?signinProvider=github`;
|
||||||
return (
|
return (
|
||||||
<GithubAuthButton
|
<GithubAuthButton
|
||||||
|
redirectTo={redirectTo}
|
||||||
buttonText="Sign Up with GitHub"
|
buttonText="Sign Up with GitHub"
|
||||||
errorText="An error occurred while trying to sign up using GitHub. Please try again."
|
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 { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
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 { List } from '@/components/ui/v2/List';
|
||||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
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 { 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 { Divider } from '@mui/material';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import NavLink from 'next/link';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { Fragment, useEffect, useMemo, useState } 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 {
|
export interface ConnectGitHubModalProps {
|
||||||
/**
|
/**
|
||||||
@@ -28,18 +48,153 @@ export interface ConnectGitHubModalProps {
|
|||||||
close?: VoidFunction;
|
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) {
|
export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [ConnectGitHubModalState, setConnectGitHubModalState] =
|
const [ConnectGitHubModalState, setConnectGitHubModalState] =
|
||||||
useState<ConnectGitHubModalState>('CONNECTING');
|
useState<ConnectGitHubModalState>('CONNECTING');
|
||||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
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 } =
|
const githubProvider = data?.authUserProviders?.find(
|
||||||
useGetGithubRepositoriesQuery();
|
(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(() => {
|
useEffect(() => {
|
||||||
startPolling(2000);
|
if (loadingGithubConnected) {
|
||||||
}, [startPolling]);
|
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 = () => {
|
const handleSelectAnotherRepository = () => {
|
||||||
setSelectedRepoId(null);
|
setSelectedRepoId(null);
|
||||||
@@ -56,13 +211,91 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
|
|
||||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||||
|
|
||||||
if (error) {
|
if (errorGithubConnected instanceof Error) {
|
||||||
throw 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 (
|
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(
|
const filteredGitHubAppInstallations =
|
||||||
(githubApp) => !!githubApp.accountLogin,
|
githubData?.githubAppInstallations.filter(
|
||||||
);
|
(githubApp) => !!githubApp.accountLogin,
|
||||||
|
);
|
||||||
|
|
||||||
const filteredGitHubRepositories = data?.githubRepositories.filter(
|
const filteredGitHubRepositories = githubData?.githubRepositories.filter(
|
||||||
(repo) => !!repo.githubAppInstallation,
|
(repo) => !!repo.githubAppInstallation,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredGitHubAppInstallationsNullValues =
|
const filteredGitHubAppInstallationsNullValues =
|
||||||
data?.githubAppInstallations.filter((githubApp) => !!githubApp.accountLogin)
|
githubData?.githubAppInstallations.filter(
|
||||||
.length === 0;
|
(githubApp) => !!githubApp.accountLogin,
|
||||||
|
).length === 0;
|
||||||
|
|
||||||
const faultyGitHubInstallation =
|
const faultyGitHubInstallation =
|
||||||
githubAppInstallations?.length === 0 ||
|
githubAppInstallations?.length === 0 ||
|
||||||
filteredGitHubAppInstallationsNullValues;
|
filteredGitHubAppInstallationsNullValues;
|
||||||
|
|
||||||
const noRepositoriesAdded = data?.githubRepositories.length === 0;
|
const noRepositoriesAdded = githubData?.githubRepositories.length === 0;
|
||||||
|
|
||||||
if (faultyGitHubInstallation) {
|
if (faultyGitHubInstallation) {
|
||||||
return (
|
return (
|
||||||
@@ -115,11 +350,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
// Both `target` and `rel` are available when `href` is set. This is
|
|
||||||
// a limitation of MUI.
|
|
||||||
// @ts-ignore
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
||||||
>
|
>
|
||||||
@@ -179,8 +410,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
className="grid grid-flow-col items-center justify-start gap-1"
|
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"
|
className="text-center text-xs font-normal"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
>
|
>
|
||||||
Showing repositories from {data?.githubAppInstallations.length}{' '}
|
Showing repositories from{' '}
|
||||||
GitHub account(s)
|
{githubData?.githubAppInstallations.length} GitHub account(s)
|
||||||
</Text>
|
</Text>
|
||||||
<div className="mb-2 mt-6 flex w-full">
|
<div className="mb-2 mt-6 flex w-full">
|
||||||
<Input
|
<Input
|
||||||
@@ -226,7 +456,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => setSelectedRepoId(repo.id)}
|
onClick={() => setSelectedRepoId(repo.node_id)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
@@ -268,8 +498,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
|
|||||||
Do you miss a repository, or do you need to connect another GitHub
|
Do you miss a repository, or do you need to connect another GitHub
|
||||||
account?{' '}
|
account?{' '}
|
||||||
<Link
|
<Link
|
||||||
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}
|
href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className="text-xs font-medium"
|
className="text-xs font-medium"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||||||
export interface EditRepositorySettingsProps {
|
export interface EditRepositorySettingsProps {
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
openConnectGithubModal?: () => void;
|
openConnectGithubModal?: () => void;
|
||||||
selectedRepoId?: string;
|
selectedRepoId: string;
|
||||||
connectGithubModalState?: ConnectGitHubModalState;
|
connectGithubModalState?: ConnectGitHubModalState;
|
||||||
handleSelectAnotherRepository?: () => void;
|
handleSelectAnotherRepository?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { Text } from '@/components/ui/v2/Text';
|
|||||||
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
|
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
|
||||||
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { useUpdateApplicationMutation } from '@/generated/graphql';
|
import { useConnectGithubRepoMutation } from '@/generated/graphql';
|
||||||
import { analytics } from '@/lib/segment';
|
import { analytics } from '@/lib/segment';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface EditRepositorySettingsModalProps {
|
export interface EditRepositorySettingsModalProps {
|
||||||
selectedRepoId?: string;
|
selectedRepoId: string;
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
handleSelectAnotherRepository?: () => void;
|
handleSelectAnotherRepository?: () => void;
|
||||||
}
|
}
|
||||||
@@ -33,45 +33,29 @@ export default function EditRepositorySettingsModal({
|
|||||||
|
|
||||||
const { project, refetch: refetchProject } = useProject();
|
const { project, refetch: refetchProject } = useProject();
|
||||||
|
|
||||||
const [updateApp, { loading }] = useUpdateApplicationMutation();
|
const [connectGithubRepo, { loading }] = useConnectGithubRepoMutation();
|
||||||
|
|
||||||
const handleEditGitHubIntegration = async (
|
const handleEditGitHubIntegration = async (
|
||||||
data: EditRepositorySettingsFormData,
|
data: EditRepositorySettingsFormData,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!project?.githubRepository || selectedRepoId) {
|
await connectGithubRepo({
|
||||||
await updateApp({
|
variables: {
|
||||||
variables: {
|
appID: project?.id,
|
||||||
appId: project?.id,
|
githubNodeID: selectedRepoId,
|
||||||
app: {
|
productionBranch: data.productionBranch,
|
||||||
githubRepositoryId: selectedRepoId,
|
baseFolder: data.repoBaseFolder,
|
||||||
repositoryProductionBranch: data.productionBranch,
|
},
|
||||||
nhostBaseFolder: data.repoBaseFolder,
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedRepoId) {
|
analytics.track('Project Connected to GitHub', {
|
||||||
analytics.track('Project Connected to GitHub', {
|
projectId: project?.id,
|
||||||
projectId: project?.id,
|
projectName: project?.name,
|
||||||
projectName: project?.name,
|
projectSubdomain: project?.subdomain,
|
||||||
projectSubdomain: project?.subdomain,
|
repositoryId: selectedRepoId,
|
||||||
repositoryId: selectedRepoId,
|
productionBranch: data.productionBranch,
|
||||||
productionBranch: data.productionBranch,
|
baseFolder: data.repoBaseFolder,
|
||||||
baseFolder: data.repoBaseFolder,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
appId: project.id,
|
|
||||||
app: {
|
|
||||||
repositoryProductionBranch: data.productionBranch,
|
|
||||||
nhostBaseFolder: data.repoBaseFolder,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await refetchProject();
|
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';
|
import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal';
|
||||||
|
|
||||||
export default function useGitHubModal() {
|
export default function useGitHubModal() {
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog, closeAlertDialog } = useDialog();
|
||||||
|
|
||||||
function openGitHubModal() {
|
function openGitHubModal() {
|
||||||
openAlertDialog({
|
openAlertDialog({
|
||||||
title: 'Connect GitHub Repository',
|
title: 'Connect GitHub Repository',
|
||||||
payload: <ConnectGitHubModal />,
|
payload: <ConnectGitHubModal close={closeAlertDialog} />,
|
||||||
props: {
|
props: {
|
||||||
hidePrimaryAction: true,
|
hidePrimaryAction: true,
|
||||||
hideSecondaryAction: 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}>
|
<CacheProvider value={emotionCache}>
|
||||||
<NhostProvider nhost={nhost}>
|
<NhostProvider nhost={nhost}>
|
||||||
<AuthProvider>
|
<NhostApolloProvider
|
||||||
<NhostApolloProvider
|
fetchPolicy="cache-and-network"
|
||||||
fetchPolicy="cache-and-network"
|
nhost={nhost}
|
||||||
nhost={nhost}
|
connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'}
|
||||||
connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'}
|
>
|
||||||
>
|
<AuthProvider>
|
||||||
<UIProvider>
|
<UIProvider>
|
||||||
<Toaster position="bottom-center" />
|
<Toaster position="bottom-center" />
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
@@ -106,8 +106,8 @@ function MyApp({
|
|||||||
</RetryableErrorBoundary>
|
</RetryableErrorBoundary>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UIProvider>
|
</UIProvider>
|
||||||
</NhostApolloProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</NhostApolloProvider>
|
||||||
</NhostProvider>
|
</NhostProvider>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</QueryClientProvider>
|
</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 { Container } from '@/components/layout/Container';
|
||||||
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
|
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
|
||||||
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
|
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 { BaseDirectorySettings } from '@/features/orgs/projects/git/settings/components/BaseDirectorySettings';
|
||||||
import { DeploymentBranchSettings } from '@/features/orgs/projects/git/settings/components/DeploymentBranchSettings';
|
import { DeploymentBranchSettings } from '@/features/orgs/projects/git/settings/components/DeploymentBranchSettings';
|
||||||
import { GitConnectionSettings } from '@/features/orgs/projects/git/settings/components/GitConnectionSettings';
|
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() {
|
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 (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
|
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 { useNhostClient } from '@/providers/nhost/';
|
||||||
|
import { useGetAuthUserProvidersLazyQuery } from '@/utils/__generated__/graphql';
|
||||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||||
import { type Session } from '@nhost/nhost-js/auth';
|
import { type Session } from '@nhost/nhost-js/auth';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {
|
import {
|
||||||
type PropsWithChildren,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
|
type PropsWithChildren,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { AuthContext, type AuthContextType } from './AuthContext';
|
import { AuthContext, type AuthContextType } from './AuthContext';
|
||||||
|
|
||||||
function AuthProvider({ children }: PropsWithChildren) {
|
function AuthProvider({ children }: PropsWithChildren) {
|
||||||
const nhost = useNhostClient();
|
const nhost = useNhostClient();
|
||||||
|
const [getAuthUserProviders] = useGetAuthUserProvidersLazyQuery();
|
||||||
const {
|
const {
|
||||||
query,
|
query,
|
||||||
isReady: isRouterReady,
|
isReady: isRouterReady,
|
||||||
@@ -21,7 +29,15 @@ function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
pathname,
|
pathname,
|
||||||
push,
|
push,
|
||||||
} = useRouter();
|
} = 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 [session, setSession] = useState<Session | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
@@ -55,7 +71,6 @@ function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// reset state if we have just signed out
|
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
if (refreshToken && typeof refreshToken === 'string') {
|
if (refreshToken && typeof refreshToken === 'string') {
|
||||||
const sessionResponse = await nhost.auth.refreshToken({
|
const sessionResponse = await nhost.auth.refreshToken({
|
||||||
@@ -63,24 +78,84 @@ function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
});
|
});
|
||||||
setSession(sessionResponse.body);
|
setSession(sessionResponse.body);
|
||||||
removeQueryParamsFromURL();
|
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 {
|
} else {
|
||||||
const currentSession = nhost.getUserSession();
|
const currentSession = nhost.getUserSession();
|
||||||
setSession(currentSession);
|
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 (typeof error === 'string') {
|
||||||
if (error === 'unverified-user') {
|
switch (error) {
|
||||||
removeQueryParamsFromURL();
|
case 'unverified-user': {
|
||||||
await push('/email/verify');
|
removeQueryParamsFromURL();
|
||||||
} else {
|
await push('/email/verify');
|
||||||
const description =
|
break;
|
||||||
typeof errorDescription === 'string'
|
}
|
||||||
? errorDescription
|
|
||||||
: 'An error occurred during the sign-in process. Please try again.';
|
/*
|
||||||
toast.error(description, getToastStyleProps());
|
* If the state isn't handled by Hasura auth, it returns `invalid-state`.
|
||||||
removeQueryParamsFromURL();
|
* However, we check the provider_state search param to see if it has this format:
|
||||||
await push('/signin');
|
* `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({
|
nhost.auth.signOut({
|
||||||
refreshToken: session!.refreshToken,
|
refreshToken: session!.refreshToken,
|
||||||
});
|
});
|
||||||
|
clearGitHubToken();
|
||||||
|
|
||||||
await push('/signin');
|
await push('/signin');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -108,16 +108,16 @@ function Providers({ children }: PropsWithChildren<{}>) {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<CacheProvider value={emotionCache}>
|
<CacheProvider value={emotionCache}>
|
||||||
<NhostProvider nhost={nhost}>
|
<NhostProvider nhost={nhost}>
|
||||||
<AuthProvider>
|
<ApolloProvider client={mockClient}>
|
||||||
<ApolloProvider client={mockClient}>
|
<AuthProvider>
|
||||||
<UIProvider>
|
<UIProvider>
|
||||||
<Toaster position="bottom-center" />
|
<Toaster position="bottom-center" />
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<DialogProvider>{children}</DialogProvider>
|
<DialogProvider>{children}</DialogProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UIProvider>
|
</UIProvider>
|
||||||
</ApolloProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</ApolloProvider>
|
||||||
</NhostProvider>
|
</NhostProvider>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</QueryClientProvider>
|
</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'];
|
database: Scalars['String'];
|
||||||
disk?: Maybe<ConfigSystemConfigPostgresDisk>;
|
disk?: Maybe<ConfigSystemConfigPostgresDisk>;
|
||||||
enabled?: Maybe<Scalars['Boolean']>;
|
enabled?: Maybe<Scalars['Boolean']>;
|
||||||
|
encryptColumnKey?: Maybe<Scalars['String']>;
|
||||||
majorVersion?: Maybe<Scalars['String']>;
|
majorVersion?: Maybe<Scalars['String']>;
|
||||||
|
oldEncryptColumnKey?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigSystemConfigPostgresComparisonExp = {
|
export type ConfigSystemConfigPostgresComparisonExp = {
|
||||||
@@ -3129,7 +3131,9 @@ export type ConfigSystemConfigPostgresComparisonExp = {
|
|||||||
database?: InputMaybe<ConfigStringComparisonExp>;
|
database?: InputMaybe<ConfigStringComparisonExp>;
|
||||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
||||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||||
|
encryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
|
||||||
majorVersion?: InputMaybe<ConfigStringComparisonExp>;
|
majorVersion?: InputMaybe<ConfigStringComparisonExp>;
|
||||||
|
oldEncryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigSystemConfigPostgresConnectionString = {
|
export type ConfigSystemConfigPostgresConnectionString = {
|
||||||
@@ -3193,7 +3197,9 @@ export type ConfigSystemConfigPostgresInsertInput = {
|
|||||||
database: Scalars['String'];
|
database: Scalars['String'];
|
||||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
|
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
|
||||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
encryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||||
majorVersion?: InputMaybe<Scalars['String']>;
|
majorVersion?: InputMaybe<Scalars['String']>;
|
||||||
|
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigSystemConfigPostgresUpdateInput = {
|
export type ConfigSystemConfigPostgresUpdateInput = {
|
||||||
@@ -3201,7 +3207,9 @@ export type ConfigSystemConfigPostgresUpdateInput = {
|
|||||||
database?: InputMaybe<Scalars['String']>;
|
database?: InputMaybe<Scalars['String']>;
|
||||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
|
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
|
||||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
encryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||||
majorVersion?: InputMaybe<Scalars['String']>;
|
majorVersion?: InputMaybe<Scalars['String']>;
|
||||||
|
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigSystemConfigUpdateInput = {
|
export type ConfigSystemConfigUpdateInput = {
|
||||||
@@ -4426,6 +4434,7 @@ export type Apps = {
|
|||||||
billingDedicatedCompute?: Maybe<Billing_Dedicated_Compute>;
|
billingDedicatedCompute?: Maybe<Billing_Dedicated_Compute>;
|
||||||
/** An object relationship */
|
/** An object relationship */
|
||||||
billingSubscriptions?: Maybe<Billing_Subscriptions>;
|
billingSubscriptions?: Maybe<Billing_Subscriptions>;
|
||||||
|
/** main entrypoint to the configuration */
|
||||||
config?: Maybe<ConfigConfig>;
|
config?: Maybe<ConfigConfig>;
|
||||||
createdAt: Scalars['timestamptz'];
|
createdAt: Scalars['timestamptz'];
|
||||||
/** An object relationship */
|
/** An object relationship */
|
||||||
@@ -11094,6 +11103,7 @@ export type Deployments = {
|
|||||||
commitSHA: Scalars['String'];
|
commitSHA: Scalars['String'];
|
||||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
commitUserName?: Maybe<Scalars['String']>;
|
commitUserName?: Maybe<Scalars['String']>;
|
||||||
|
createdAt: Scalars['timestamptz'];
|
||||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||||
/** An array relationship */
|
/** An array relationship */
|
||||||
deploymentLogs: Array<DeploymentLogs>;
|
deploymentLogs: Array<DeploymentLogs>;
|
||||||
@@ -11191,6 +11201,7 @@ export type Deployments_Bool_Exp = {
|
|||||||
commitSHA?: InputMaybe<String_Comparison_Exp>;
|
commitSHA?: InputMaybe<String_Comparison_Exp>;
|
||||||
commitUserAvatarUrl?: InputMaybe<String_Comparison_Exp>;
|
commitUserAvatarUrl?: InputMaybe<String_Comparison_Exp>;
|
||||||
commitUserName?: InputMaybe<String_Comparison_Exp>;
|
commitUserName?: InputMaybe<String_Comparison_Exp>;
|
||||||
|
createdAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||||
deploymentEndedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
deploymentEndedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||||
deploymentLogs?: InputMaybe<DeploymentLogs_Bool_Exp>;
|
deploymentLogs?: InputMaybe<DeploymentLogs_Bool_Exp>;
|
||||||
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Bool_Exp>;
|
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Bool_Exp>;
|
||||||
@@ -11222,6 +11233,7 @@ export type Deployments_Insert_Input = {
|
|||||||
commitSHA?: InputMaybe<Scalars['String']>;
|
commitSHA?: InputMaybe<Scalars['String']>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||||
commitUserName?: InputMaybe<Scalars['String']>;
|
commitUserName?: InputMaybe<Scalars['String']>;
|
||||||
|
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentLogs?: InputMaybe<DeploymentLogs_Arr_Rel_Insert_Input>;
|
deploymentLogs?: InputMaybe<DeploymentLogs_Arr_Rel_Insert_Input>;
|
||||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
@@ -11246,6 +11258,7 @@ export type Deployments_Max_Fields = {
|
|||||||
commitSHA?: Maybe<Scalars['String']>;
|
commitSHA?: Maybe<Scalars['String']>;
|
||||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
commitUserName?: Maybe<Scalars['String']>;
|
commitUserName?: Maybe<Scalars['String']>;
|
||||||
|
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentStatus?: Maybe<Scalars['String']>;
|
deploymentStatus?: Maybe<Scalars['String']>;
|
||||||
@@ -11268,6 +11281,7 @@ export type Deployments_Max_Order_By = {
|
|||||||
commitSHA?: InputMaybe<Order_By>;
|
commitSHA?: InputMaybe<Order_By>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||||
commitUserName?: InputMaybe<Order_By>;
|
commitUserName?: InputMaybe<Order_By>;
|
||||||
|
createdAt?: InputMaybe<Order_By>;
|
||||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||||
deploymentStatus?: InputMaybe<Order_By>;
|
deploymentStatus?: InputMaybe<Order_By>;
|
||||||
@@ -11291,6 +11305,7 @@ export type Deployments_Min_Fields = {
|
|||||||
commitSHA?: Maybe<Scalars['String']>;
|
commitSHA?: Maybe<Scalars['String']>;
|
||||||
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
commitUserAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
commitUserName?: Maybe<Scalars['String']>;
|
commitUserName?: Maybe<Scalars['String']>;
|
||||||
|
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
|
||||||
deploymentStatus?: Maybe<Scalars['String']>;
|
deploymentStatus?: Maybe<Scalars['String']>;
|
||||||
@@ -11313,6 +11328,7 @@ export type Deployments_Min_Order_By = {
|
|||||||
commitSHA?: InputMaybe<Order_By>;
|
commitSHA?: InputMaybe<Order_By>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||||
commitUserName?: InputMaybe<Order_By>;
|
commitUserName?: InputMaybe<Order_By>;
|
||||||
|
createdAt?: InputMaybe<Order_By>;
|
||||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||||
deploymentStatus?: InputMaybe<Order_By>;
|
deploymentStatus?: InputMaybe<Order_By>;
|
||||||
@@ -11359,6 +11375,7 @@ export type Deployments_Order_By = {
|
|||||||
commitSHA?: InputMaybe<Order_By>;
|
commitSHA?: InputMaybe<Order_By>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
commitUserAvatarUrl?: InputMaybe<Order_By>;
|
||||||
commitUserName?: InputMaybe<Order_By>;
|
commitUserName?: InputMaybe<Order_By>;
|
||||||
|
createdAt?: InputMaybe<Order_By>;
|
||||||
deploymentEndedAt?: InputMaybe<Order_By>;
|
deploymentEndedAt?: InputMaybe<Order_By>;
|
||||||
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Order_By>;
|
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Order_By>;
|
||||||
deploymentStartedAt?: InputMaybe<Order_By>;
|
deploymentStartedAt?: InputMaybe<Order_By>;
|
||||||
@@ -11393,6 +11410,8 @@ export enum Deployments_Select_Column {
|
|||||||
/** column name */
|
/** column name */
|
||||||
CommitUserName = 'commitUserName',
|
CommitUserName = 'commitUserName',
|
||||||
/** column name */
|
/** column name */
|
||||||
|
CreatedAt = 'createdAt',
|
||||||
|
/** column name */
|
||||||
DeploymentEndedAt = 'deploymentEndedAt',
|
DeploymentEndedAt = 'deploymentEndedAt',
|
||||||
/** column name */
|
/** column name */
|
||||||
DeploymentStartedAt = 'deploymentStartedAt',
|
DeploymentStartedAt = 'deploymentStartedAt',
|
||||||
@@ -11427,6 +11446,7 @@ export type Deployments_Set_Input = {
|
|||||||
commitSHA?: InputMaybe<Scalars['String']>;
|
commitSHA?: InputMaybe<Scalars['String']>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||||
commitUserName?: InputMaybe<Scalars['String']>;
|
commitUserName?: InputMaybe<Scalars['String']>;
|
||||||
|
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentStatus?: InputMaybe<Scalars['String']>;
|
deploymentStatus?: InputMaybe<Scalars['String']>;
|
||||||
@@ -11457,6 +11477,7 @@ export type Deployments_Stream_Cursor_Value_Input = {
|
|||||||
commitSHA?: InputMaybe<Scalars['String']>;
|
commitSHA?: InputMaybe<Scalars['String']>;
|
||||||
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
|
||||||
commitUserName?: InputMaybe<Scalars['String']>;
|
commitUserName?: InputMaybe<Scalars['String']>;
|
||||||
|
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||||
deploymentStatus?: InputMaybe<Scalars['String']>;
|
deploymentStatus?: InputMaybe<Scalars['String']>;
|
||||||
@@ -11485,6 +11506,8 @@ export enum Deployments_Update_Column {
|
|||||||
/** column name */
|
/** column name */
|
||||||
CommitUserName = 'commitUserName',
|
CommitUserName = 'commitUserName',
|
||||||
/** column name */
|
/** column name */
|
||||||
|
CreatedAt = 'createdAt',
|
||||||
|
/** column name */
|
||||||
DeploymentEndedAt = 'deploymentEndedAt',
|
DeploymentEndedAt = 'deploymentEndedAt',
|
||||||
/** column name */
|
/** column name */
|
||||||
DeploymentStartedAt = 'deploymentStartedAt',
|
DeploymentStartedAt = 'deploymentStartedAt',
|
||||||
@@ -13067,6 +13090,7 @@ export type Mutation_Root = {
|
|||||||
billingUpgradeFreeOrganization: Scalars['String'];
|
billingUpgradeFreeOrganization: Scalars['String'];
|
||||||
billingUploadReports: Scalars['Boolean'];
|
billingUploadReports: Scalars['Boolean'];
|
||||||
changeDatabaseVersion: Scalars['Boolean'];
|
changeDatabaseVersion: Scalars['Boolean'];
|
||||||
|
connectGithubRepo: Scalars['Boolean'];
|
||||||
/** delete single row from the table: "announcements_read" */
|
/** delete single row from the table: "announcements_read" */
|
||||||
deleteAnnouncementRead?: Maybe<Announcements_Read>;
|
deleteAnnouncementRead?: Maybe<Announcements_Read>;
|
||||||
/** delete data from the table: "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 */
|
/** mutation root */
|
||||||
export type Mutation_RootDeleteAnnouncementReadArgs = {
|
export type Mutation_RootDeleteAnnouncementReadArgs = {
|
||||||
id: Scalars['uuid'];
|
id: Scalars['uuid'];
|
||||||
@@ -27517,6 +27550,16 @@ export type ResetDatabasePasswordMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type ResetDatabasePasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean };
|
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<{
|
export type GetHasuraSettingsQueryVariables = Exact<{
|
||||||
appId: Scalars['uuid'];
|
appId: Scalars['uuid'];
|
||||||
}>;
|
}>;
|
||||||
@@ -29446,6 +29489,45 @@ export function useResetDatabasePasswordMutation(baseOptions?: Apollo.MutationHo
|
|||||||
export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>;
|
export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>;
|
||||||
export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>;
|
export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>;
|
||||||
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
|
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`
|
export const GetHasuraSettingsDocument = gql`
|
||||||
query GetHasuraSettings($appId: uuid!) {
|
query GetHasuraSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: false) {
|
config(appID: $appId, resolve: false) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
gofumpt
|
gofumpt
|
||||||
golangci-lint
|
golangci-lint
|
||||||
gqlgenc
|
gqlgenc
|
||||||
|
oapi-codegen
|
||||||
|
|
||||||
# internal packages
|
# internal packages
|
||||||
self.packages.${system}.codegen
|
self.packages.${system}.codegen
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -16,7 +16,6 @@ require (
|
|||||||
github.com/davidbyttow/govips/v2 v2.16.0
|
github.com/davidbyttow/govips/v2 v2.16.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8
|
github.com/gabriel-vasile/mimetype v1.4.8
|
||||||
github.com/getkin/kin-openapi v0.133.0
|
github.com/getkin/kin-openapi v0.133.0
|
||||||
github.com/gin-contrib/cors v1.7.3
|
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-git/go-git/v5 v5.16.2
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
github.com/go-webauthn/webauthn v0.12.2
|
github.com/go-webauthn/webauthn v0.12.2
|
||||||
@@ -30,7 +29,6 @@ require (
|
|||||||
github.com/lmittmann/tint v1.0.7
|
github.com/lmittmann/tint v1.0.7
|
||||||
github.com/mark3labs/mcp-go v0.41.1
|
github.com/mark3labs/mcp-go v0.41.1
|
||||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48
|
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48
|
||||||
github.com/oapi-codegen/gin-middleware v1.0.2
|
|
||||||
github.com/oapi-codegen/runtime v1.1.1
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
github.com/pb33f/libopenapi v0.21.12
|
github.com/pb33f/libopenapi v0.21.12
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -162,8 +162,6 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||||
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
|
|
||||||
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
|
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
@@ -341,8 +339,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
|||||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk=
|
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk=
|
||||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
|
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/oapi-codegen/gin-middleware v1.0.2 h1:/H99UzvHQAUxXK8pzdcGAZgjCVeXdFDAUUWaJT0k0eI=
|
|
||||||
github.com/oapi-codegen/gin-middleware v1.0.2/go.mod h1:2HJDQjH8jzK2/k/VKcWl+/T41H7ai2bKa6dN3AA2GpA=
|
|
||||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||||
|
|||||||
13
internal/lib/oapi/errors.go
Normal file
13
internal/lib/oapi/errors.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package oapi
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type AuthenticatorError struct {
|
||||||
|
Scheme string
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AuthenticatorError) Error() string {
|
||||||
|
return fmt.Sprintf("security error [%s]: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
10
internal/lib/oapi/example/api/api.go
Normal file
10
internal/lib/oapi/example/api/api.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//go:generate oapi-codegen -config server.cfg.yaml openapi.yaml
|
||||||
|
//go:generate oapi-codegen -config types.cfg.yaml openapi.yaml
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed openapi.yaml
|
||||||
|
var OpenAPISchema []byte
|
||||||
200
internal/lib/oapi/example/api/openapi.yaml
Normal file
200
internal/lib/oapi/example/api/openapi.yaml
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
openapi: "3.0.0"
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/signin/email-password:
|
||||||
|
post:
|
||||||
|
summary: Sign in with email and password
|
||||||
|
description: Authenticate a user with their email and password. Returns a session object or MFA challenge if two-factor authentication is enabled.
|
||||||
|
operationId: signInEmailPassword
|
||||||
|
requestBody:
|
||||||
|
description: User credentials for email and password authentication
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SignInEmailPasswordRequest"
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SignInEmailPasswordResponse"
|
||||||
|
description: "Authentication successful. If MFA is enabled, a challenge will be returned instead of a session."
|
||||||
|
default:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
description: "An error occurred while processing the request"
|
||||||
|
|
||||||
|
/user/email/change:
|
||||||
|
post:
|
||||||
|
summary: Change user email
|
||||||
|
description: Request to change the authenticated user's email address. A verification email will be sent to the new address to confirm the change. Requires elevated permissions.
|
||||||
|
operationId: changeUserEmail
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
security:
|
||||||
|
- BearerAuthElevated: []
|
||||||
|
requestBody:
|
||||||
|
description: New email address and optional redirect URL for email change
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UserEmailChangeRequest"
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: >-
|
||||||
|
Email change requested. An email with a verification link has been sent to the new address
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/OKResponse"
|
||||||
|
default:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
description: "An error occurred while processing the request"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: "Bearer authentication with JWT access token. Used to authenticate requests to protected endpoints."
|
||||||
|
BearerAuthElevated:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: "Bearer authentication that requires elevated permissions. Used for sensitive operations that may require additional security measures such as recent authentication. For details see https://docs.nhost.io/products/auth/elevated-permissions"
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
SignInEmailPasswordRequest:
|
||||||
|
type: object
|
||||||
|
description: "Request to authenticate using email and password"
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
description: "User's email address"
|
||||||
|
example: "john.smith@nhost.io"
|
||||||
|
format: email
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
description: "User's password"
|
||||||
|
example: "Str0ngPassw#ord-94|%"
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 50
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
|
||||||
|
SignInEmailPasswordResponse:
|
||||||
|
type: object
|
||||||
|
description: "Response for email-password authentication that may include a session or MFA challenge"
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
session:
|
||||||
|
$ref: "#/components/schemas/Session"
|
||||||
|
|
||||||
|
Session:
|
||||||
|
type: object
|
||||||
|
description: "User authentication session containing tokens and user information"
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
accessToken:
|
||||||
|
type: string
|
||||||
|
description: "JWT token for authenticating API requests"
|
||||||
|
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
accessTokenExpiresIn:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: "Expiration time of the access token in seconds"
|
||||||
|
example: 900
|
||||||
|
refreshTokenId:
|
||||||
|
description: "Identifier for the refresh token"
|
||||||
|
example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24"
|
||||||
|
pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
|
||||||
|
type: string
|
||||||
|
refreshToken:
|
||||||
|
description: "Token used to refresh the access token"
|
||||||
|
example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24"
|
||||||
|
pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- accessToken
|
||||||
|
- accessTokenExpiresIn
|
||||||
|
- refreshToken
|
||||||
|
- refreshTokenId
|
||||||
|
|
||||||
|
UserEmailChangeRequest:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
newEmail:
|
||||||
|
description: A valid email
|
||||||
|
example: john.smith@nhost.io
|
||||||
|
format: email
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- newEmail
|
||||||
|
|
||||||
|
OKResponse:
|
||||||
|
type: string
|
||||||
|
additionalProperties: false
|
||||||
|
enum:
|
||||||
|
- OK
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
description: "Standardized error response"
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
description: "HTTP status error code"
|
||||||
|
type: integer
|
||||||
|
example: 400
|
||||||
|
message:
|
||||||
|
description: "Human-friendly error message"
|
||||||
|
type: string
|
||||||
|
example: "Invalid email format"
|
||||||
|
error:
|
||||||
|
description: "Error code identifying the specific application error"
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- default-role-must-be-in-allowed-roles
|
||||||
|
- disabled-endpoint
|
||||||
|
- disabled-user
|
||||||
|
- email-already-in-use
|
||||||
|
- email-already-verified
|
||||||
|
- forbidden-anonymous
|
||||||
|
- internal-server-error
|
||||||
|
- invalid-email-password
|
||||||
|
- invalid-request
|
||||||
|
- locale-not-allowed
|
||||||
|
- password-too-short
|
||||||
|
- password-in-hibp-database
|
||||||
|
- redirectTo-not-allowed
|
||||||
|
- role-not-allowed
|
||||||
|
- signup-disabled
|
||||||
|
- unverified-user
|
||||||
|
- user-not-anonymous
|
||||||
|
- invalid-pat
|
||||||
|
- invalid-refresh-token
|
||||||
|
- invalid-ticket
|
||||||
|
- disabled-mfa-totp
|
||||||
|
- no-totp-secret
|
||||||
|
- invalid-totp
|
||||||
|
- mfa-type-not-found
|
||||||
|
- totp-already-active
|
||||||
|
- invalid-state
|
||||||
|
- oauth-token-echange-failed
|
||||||
|
- oauth-profile-fetch-failed
|
||||||
|
- oauth-provider-error
|
||||||
|
- invalid-otp
|
||||||
|
- cannot-send-sms
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- message
|
||||||
|
- error
|
||||||
6
internal/lib/oapi/example/api/server.cfg.yaml
Normal file
6
internal/lib/oapi/example/api/server.cfg.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package: api
|
||||||
|
generate:
|
||||||
|
gin-server: true
|
||||||
|
embedded-spec: true
|
||||||
|
strict-server: true
|
||||||
|
output: server.gen.go
|
||||||
351
internal/lib/oapi/example/api/server.gen.go
Normal file
351
internal/lib/oapi/example/api/server.gen.go
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
// Package api provides primitives to interact with the openapi HTTP API.
|
||||||
|
//
|
||||||
|
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerInterface represents all server handlers.
|
||||||
|
type ServerInterface interface {
|
||||||
|
// Sign in with email and password
|
||||||
|
// (POST /signin/email-password)
|
||||||
|
SignInEmailPassword(c *gin.Context)
|
||||||
|
// Change user email
|
||||||
|
// (POST /user/email/change)
|
||||||
|
ChangeUserEmail(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInterfaceWrapper converts contexts to parameters.
|
||||||
|
type ServerInterfaceWrapper struct {
|
||||||
|
Handler ServerInterface
|
||||||
|
HandlerMiddlewares []MiddlewareFunc
|
||||||
|
ErrorHandler func(*gin.Context, error, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiddlewareFunc func(c *gin.Context)
|
||||||
|
|
||||||
|
// SignInEmailPassword operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) SignInEmailPassword(c *gin.Context) {
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
middleware(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siw.Handler.SignInEmailPassword(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserEmail operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) ChangeUserEmail(c *gin.Context) {
|
||||||
|
|
||||||
|
c.Set(BearerAuthElevatedScopes, []string{})
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
middleware(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siw.Handler.ChangeUserEmail(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinServerOptions provides options for the Gin server.
|
||||||
|
type GinServerOptions struct {
|
||||||
|
BaseURL string
|
||||||
|
Middlewares []MiddlewareFunc
|
||||||
|
ErrorHandler func(*gin.Context, error, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
|
||||||
|
func RegisterHandlers(router gin.IRouter, si ServerInterface) {
|
||||||
|
RegisterHandlersWithOptions(router, si, GinServerOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandlersWithOptions creates http.Handler with additional options
|
||||||
|
func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) {
|
||||||
|
errorHandler := options.ErrorHandler
|
||||||
|
if errorHandler == nil {
|
||||||
|
errorHandler = func(c *gin.Context, err error, statusCode int) {
|
||||||
|
c.JSON(statusCode, gin.H{"msg": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := ServerInterfaceWrapper{
|
||||||
|
Handler: si,
|
||||||
|
HandlerMiddlewares: options.Middlewares,
|
||||||
|
ErrorHandler: errorHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
router.POST(options.BaseURL+"/signin/email-password", wrapper.SignInEmailPassword)
|
||||||
|
router.POST(options.BaseURL+"/user/email/change", wrapper.ChangeUserEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInEmailPasswordRequestObject struct {
|
||||||
|
Body *SignInEmailPasswordJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInEmailPasswordResponseObject interface {
|
||||||
|
VisitSignInEmailPasswordResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInEmailPassword200JSONResponse SignInEmailPasswordResponse
|
||||||
|
|
||||||
|
func (response SignInEmailPassword200JSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInEmailPassworddefaultJSONResponse struct {
|
||||||
|
Body ErrorResponse
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response SignInEmailPassworddefaultJSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeUserEmailRequestObject struct {
|
||||||
|
Body *ChangeUserEmailJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeUserEmailResponseObject interface {
|
||||||
|
VisitChangeUserEmailResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeUserEmail200JSONResponse OKResponse
|
||||||
|
|
||||||
|
func (response ChangeUserEmail200JSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeUserEmaildefaultJSONResponse struct {
|
||||||
|
Body ErrorResponse
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response ChangeUserEmaildefaultJSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictServerInterface represents all server handlers.
|
||||||
|
type StrictServerInterface interface {
|
||||||
|
// Sign in with email and password
|
||||||
|
// (POST /signin/email-password)
|
||||||
|
SignInEmailPassword(ctx context.Context, request SignInEmailPasswordRequestObject) (SignInEmailPasswordResponseObject, error)
|
||||||
|
// Change user email
|
||||||
|
// (POST /user/email/change)
|
||||||
|
ChangeUserEmail(ctx context.Context, request ChangeUserEmailRequestObject) (ChangeUserEmailResponseObject, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
|
||||||
|
type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc
|
||||||
|
|
||||||
|
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
|
||||||
|
return &strictHandler{ssi: ssi, middlewares: middlewares}
|
||||||
|
}
|
||||||
|
|
||||||
|
type strictHandler struct {
|
||||||
|
ssi StrictServerInterface
|
||||||
|
middlewares []StrictMiddlewareFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignInEmailPassword operation middleware
|
||||||
|
func (sh *strictHandler) SignInEmailPassword(ctx *gin.Context) {
|
||||||
|
var request SignInEmailPasswordRequestObject
|
||||||
|
|
||||||
|
var body SignInEmailPasswordJSONRequestBody
|
||||||
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
|
ctx.Status(http.StatusBadRequest)
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.SignInEmailPassword(ctx, request.(SignInEmailPasswordRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "SignInEmailPassword")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(ctx, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
} else if validResponse, ok := response.(SignInEmailPasswordResponseObject); ok {
|
||||||
|
if err := validResponse.VisitSignInEmailPasswordResponse(ctx.Writer); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserEmail operation middleware
|
||||||
|
func (sh *strictHandler) ChangeUserEmail(ctx *gin.Context) {
|
||||||
|
var request ChangeUserEmailRequestObject
|
||||||
|
|
||||||
|
var body ChangeUserEmailJSONRequestBody
|
||||||
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
|
ctx.Status(http.StatusBadRequest)
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.ChangeUserEmail(ctx, request.(ChangeUserEmailRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "ChangeUserEmail")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(ctx, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
} else if validResponse, ok := response.(ChangeUserEmailResponseObject); ok {
|
||||||
|
if err := validResponse.VisitChangeUserEmailResponse(ctx.Writer); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
|
"H4sIAAAAAAAC/9RYUXPbuBH+KxhcO30RJMVW0lhP9WV8rXLXS8Z22s7YfoCApYiYBHhY0IrO1X/vLEBK",
|
||||||
|
"pEQ1vs5dp32yTBCL3f2+b3fBZ65cWTkLNiCfP3NUOZQy/rzy3vlrwMpZBHogtTbBOCuLj95V4IMB5PNM",
|
||||||
|
"FggjrgGVNxWt8zm/CdJq6bX5GTQDMsR8a2nEq872Zx6X6UffRDyeKaeBGQ02mGxj7IqFHBhWoExmFJNV",
|
||||||
|
"VRglaUc6hY842Lrk8zuuIZN1EYR3BYiyxiCWIIwVsijcGnR8jnzEtUG5LEALsLpyxobusxoh2iylKYQs",
|
||||||
|
"PEi9ISN1jKP/+Am8yQxoPuKZ80ujNVghrbOb0tV0krEBvJWFQPBP4EXrsbFPsjBaJHOVRFw7rzsLHn6q",
|
||||||
|
"AcmxwilZgLAutHFQOpsdIjgnMHc+dB8aK3KzrISWQS5l9NuDNh5UuHUHlmKu+o/QrGxdiTYjfMRr20ba",
|
||||||
|
"pof+pG29aJPzlQy9UDIPmIvgHsF2ngejHqGX+jKTIrhQ8RG3Lv4SCMpD11qzHl/dVMn1zNWW3Iw7Wmyk",
|
||||||
|
"CuYJOjsxyED/O1mHxhsBKpd2BSKTJkWaFivvMlOAyCCofGDxyegBMJNnSlryCcFqgSXyhxEnR/mcY/DG",
|
||||||
|
"rvh2xEtAlCs4VsBf6lJakXkDVhebRkbt2yMOX2RZFWRrkc5kkUAsc76MOT86iYKuceCg29uPLC02p5Ds",
|
||||||
|
"ukfMptOdPaLxCjzfbolJP9XGgybBNdb3AY0aae+DdsvPoAK58uH7F1eWVtAfvh9M3w0gxjB+UYH6hOAZ",
|
||||||
|
"IUiVpakgmCwx5WyQxsZqQ8RAJq1mRHJmbMouGTmsY1IpQLyNxD5K8fu/3yZjBE/vYLtilx8XrNE49oCF",
|
||||||
|
"zft8+WdlPpj3i08/L179aBa4sNev1bvFm8Vj9Y+/vXt/MR6Ph7DueHP1pTIecDHgVlxK0QdTAnNZLLBp",
|
||||||
|
"c+OwocwoZ3XPtwtiREO1yIk3M35MEWJIFPyJtMTHlFrNgmPNu0cu9HJyps5fL99k50LNlhdi9hbOxcUf",
|
||||||
|
"30qhZ3qavdKzMzibxfoXqNryOb+/X95NxYUU2cPz2+39/VLs/p1tT/7u7np1RtuGstyNbqGP41ukzmXA",
|
||||||
|
"R9wpsF2Q/8uRHUi7S+0T1DpA+ig1Q0Xgxqzswl5R1frYtKvrptX9MjU3u4hDHWkBq5HklcoiabjTWw9m",
|
||||||
|
"EHrlGD6qEn/A1oDWHrAv0M8ut2MsTcj/ZHOHYWwc78gimR3gzc6RU0d2PN2fdhP81K5iqr6h1n4x++fv",
|
||||||
|
"qd7KLz+AXYWcz19PR7w0tv33/GvAtg7ujnsxTP/RWNhui2LojzuHxTjkMrBSbpixqqg1MLmrz86zv353",
|
||||||
|
"yVQuiwLs6niixH1L+J2HjM/5N5P9mDtpZtxJ2zkoKUdBEw4x5HdxKHgpL/uOWFhfDTPrknVa9q9AqQNc",
|
||||||
|
"dwcf40lzAKjam7C5oUQkT78F6cFf1sSaQ1/T2iFAaxNyRm2tW6nH7FNTy3s6bJsbLVTeBVCB7gXNwI3U",
|
||||||
|
"wCIo5OcynraPMA+hIkD2Hl4V8CQD6Jd6GqnUZAcZNLtZBb40kQHYuE2sRLBoaFhkBGQ0gHsyNlbYngOs",
|
||||||
|
"TSYrQWJNJ2CtciaReVBgw4E3Y/ad80xDkKZAhgCMAsT5ZKKdwnEL+aTyTtcq4IS2T1qnRcfpryeNsKaJ",
|
||||||
|
"hc9tXRQj7iqwsjJ8zs/H0/E09ZI84j+hOd/YycEdZP7MK5dof8DfLrwyDUeRESEH4wdK7phdQ6g9DVN7",
|
||||||
|
"IUdKHumZmYyFtROZVMEdoWmQgY1XBKLNDiTqvkOViidhAIZvnd5QIDTegU1S3t8fJ58xVYxUHb5aO063",
|
||||||
|
"rpj2gWlTeYjTgCxwX/96OToIlXc1HXwNUeSpgkbQzqbT3zagpsgPRHR5MDrXsQhkdTFmiyzCucdpxGQH",
|
||||||
|
"3LUpCrakmkB0AM2MxQBS0+y5o8aYxxPjDf5XC7H/QWMoqOYrAnNK1d6DZuvcFEAli6JrPz/4HdAjjnVZ",
|
||||||
|
"Sr9puEezclTBwMhBb09IJ0ljk3TZPK2wzkyTXk1jcUd36U5yOJ+M2SVLF/T2w0hcbdOOVJKCi8YsrNtd",
|
||||||
|
"8RhnM+PLuJSOJNH+u7J5JMDULHe98zcS34nePIDoj7DuZydi4qqmdrcfQtin6x86mmyg+W/Kr3MfHgjj",
|
||||||
|
"quNWSz/QY3a5RzfkTPZxL4x9ZLlEtgSwp3D/v9FZ02X5/O55cBS4e9g+dOWYqJEa025mkiuk6Sh+tnrY",
|
||||||
|
"brfbfwUAAP//3ciGL/8UAAA=",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
// or error if failed to decode
|
||||||
|
func decodeSpec() ([]byte, error) {
|
||||||
|
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
|
||||||
|
}
|
||||||
|
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err = buf.ReadFrom(zr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawSpec = decodeSpecCached()
|
||||||
|
|
||||||
|
// a naive cached of a decoded swagger spec
|
||||||
|
func decodeSpecCached() func() ([]byte, error) {
|
||||||
|
data, err := decodeSpec()
|
||||||
|
return func() ([]byte, error) {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
|
||||||
|
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
|
||||||
|
res := make(map[string]func() ([]byte, error))
|
||||||
|
if len(pathToFile) > 0 {
|
||||||
|
res[pathToFile] = rawSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||||
|
// in this file. The external references of Swagger specification are resolved.
|
||||||
|
// The logic of resolving external references is tightly connected to "import-mapping" feature.
|
||||||
|
// Externally referenced files must be embedded in the corresponding golang packages.
|
||||||
|
// Urls can be supported but this task was out of the scope.
|
||||||
|
func GetSwagger() (swagger *openapi3.T, err error) {
|
||||||
|
resolvePath := PathToRawSpec("")
|
||||||
|
|
||||||
|
loader := openapi3.NewLoader()
|
||||||
|
loader.IsExternalRefsAllowed = true
|
||||||
|
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
|
||||||
|
pathToFile := url.String()
|
||||||
|
pathToFile = path.Clean(pathToFile)
|
||||||
|
getSpec, ok := resolvePath[pathToFile]
|
||||||
|
if !ok {
|
||||||
|
err1 := fmt.Errorf("path not found: %s", pathToFile)
|
||||||
|
return nil, err1
|
||||||
|
}
|
||||||
|
return getSpec()
|
||||||
|
}
|
||||||
|
var specData []byte
|
||||||
|
specData, err = rawSpec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
swagger, err = loader.LoadFromData(specData)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
4
internal/lib/oapi/example/api/types.cfg.yaml
Normal file
4
internal/lib/oapi/example/api/types.cfg.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package: api
|
||||||
|
generate:
|
||||||
|
models: true
|
||||||
|
output: types.gen.go
|
||||||
112
internal/lib/oapi/example/api/types.gen.go
Normal file
112
internal/lib/oapi/example/api/types.gen.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Package api provides primitives to interact with the openapi HTTP API.
|
||||||
|
//
|
||||||
|
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BearerAuthElevatedScopes = "BearerAuthElevated.Scopes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines values for ErrorResponseError.
|
||||||
|
const (
|
||||||
|
CannotSendSms ErrorResponseError = "cannot-send-sms"
|
||||||
|
DefaultRoleMustBeInAllowedRoles ErrorResponseError = "default-role-must-be-in-allowed-roles"
|
||||||
|
DisabledEndpoint ErrorResponseError = "disabled-endpoint"
|
||||||
|
DisabledMfaTotp ErrorResponseError = "disabled-mfa-totp"
|
||||||
|
DisabledUser ErrorResponseError = "disabled-user"
|
||||||
|
EmailAlreadyInUse ErrorResponseError = "email-already-in-use"
|
||||||
|
EmailAlreadyVerified ErrorResponseError = "email-already-verified"
|
||||||
|
ForbiddenAnonymous ErrorResponseError = "forbidden-anonymous"
|
||||||
|
InternalServerError ErrorResponseError = "internal-server-error"
|
||||||
|
InvalidEmailPassword ErrorResponseError = "invalid-email-password"
|
||||||
|
InvalidOtp ErrorResponseError = "invalid-otp"
|
||||||
|
InvalidPat ErrorResponseError = "invalid-pat"
|
||||||
|
InvalidRefreshToken ErrorResponseError = "invalid-refresh-token"
|
||||||
|
InvalidRequest ErrorResponseError = "invalid-request"
|
||||||
|
InvalidState ErrorResponseError = "invalid-state"
|
||||||
|
InvalidTicket ErrorResponseError = "invalid-ticket"
|
||||||
|
InvalidTotp ErrorResponseError = "invalid-totp"
|
||||||
|
LocaleNotAllowed ErrorResponseError = "locale-not-allowed"
|
||||||
|
MfaTypeNotFound ErrorResponseError = "mfa-type-not-found"
|
||||||
|
NoTotpSecret ErrorResponseError = "no-totp-secret"
|
||||||
|
OauthProfileFetchFailed ErrorResponseError = "oauth-profile-fetch-failed"
|
||||||
|
OauthProviderError ErrorResponseError = "oauth-provider-error"
|
||||||
|
OauthTokenEchangeFailed ErrorResponseError = "oauth-token-echange-failed"
|
||||||
|
PasswordInHibpDatabase ErrorResponseError = "password-in-hibp-database"
|
||||||
|
PasswordTooShort ErrorResponseError = "password-too-short"
|
||||||
|
RedirectToNotAllowed ErrorResponseError = "redirectTo-not-allowed"
|
||||||
|
RoleNotAllowed ErrorResponseError = "role-not-allowed"
|
||||||
|
SignupDisabled ErrorResponseError = "signup-disabled"
|
||||||
|
TotpAlreadyActive ErrorResponseError = "totp-already-active"
|
||||||
|
UnverifiedUser ErrorResponseError = "unverified-user"
|
||||||
|
UserNotAnonymous ErrorResponseError = "user-not-anonymous"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines values for OKResponse.
|
||||||
|
const (
|
||||||
|
OK OKResponse = "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorResponse Standardized error response
|
||||||
|
type ErrorResponse struct {
|
||||||
|
// Error Error code identifying the specific application error
|
||||||
|
Error ErrorResponseError `json:"error"`
|
||||||
|
|
||||||
|
// Message Human-friendly error message
|
||||||
|
Message string `json:"message"`
|
||||||
|
|
||||||
|
// Status HTTP status error code
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResponseError Error code identifying the specific application error
|
||||||
|
type ErrorResponseError string
|
||||||
|
|
||||||
|
// OKResponse defines model for OKResponse.
|
||||||
|
type OKResponse string
|
||||||
|
|
||||||
|
// Session User authentication session containing tokens and user information
|
||||||
|
type Session struct {
|
||||||
|
// AccessToken JWT token for authenticating API requests
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
|
||||||
|
// AccessTokenExpiresIn Expiration time of the access token in seconds
|
||||||
|
AccessTokenExpiresIn int64 `json:"accessTokenExpiresIn"`
|
||||||
|
|
||||||
|
// RefreshToken Token used to refresh the access token
|
||||||
|
RefreshToken string `json:"refreshToken"`
|
||||||
|
|
||||||
|
// RefreshTokenId Identifier for the refresh token
|
||||||
|
RefreshTokenId string `json:"refreshTokenId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignInEmailPasswordRequest Request to authenticate using email and password
|
||||||
|
type SignInEmailPasswordRequest struct {
|
||||||
|
// Email User's email address
|
||||||
|
Email openapi_types.Email `json:"email"`
|
||||||
|
|
||||||
|
// Password User's password
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignInEmailPasswordResponse Response for email-password authentication that may include a session or MFA challenge
|
||||||
|
type SignInEmailPasswordResponse struct {
|
||||||
|
// Session User authentication session containing tokens and user information
|
||||||
|
Session *Session `json:"session,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserEmailChangeRequest defines model for UserEmailChangeRequest.
|
||||||
|
type UserEmailChangeRequest struct {
|
||||||
|
// NewEmail A valid email
|
||||||
|
NewEmail openapi_types.Email `json:"newEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignInEmailPasswordJSONRequestBody defines body for SignInEmailPassword for application/json ContentType.
|
||||||
|
type SignInEmailPasswordJSONRequestBody = SignInEmailPasswordRequest
|
||||||
|
|
||||||
|
// ChangeUserEmailJSONRequestBody defines body for ChangeUserEmail for application/json ContentType.
|
||||||
|
type ChangeUserEmailJSONRequestBody = UserEmailChangeRequest
|
||||||
49
internal/lib/oapi/example/controller/controller.go
Normal file
49
internal/lib/oapi/example/controller/controller.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/example/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct{}
|
||||||
|
|
||||||
|
func NewController() *Controller {
|
||||||
|
return &Controller{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) SignInEmailPassword( //nolint:ireturn
|
||||||
|
_ context.Context, req api.SignInEmailPasswordRequestObject,
|
||||||
|
) (api.SignInEmailPasswordResponseObject, error) {
|
||||||
|
switch req.Body.Email {
|
||||||
|
case "bad@email.com":
|
||||||
|
return api.SignInEmailPassworddefaultJSONResponse{
|
||||||
|
Body: api.ErrorResponse{
|
||||||
|
Error: api.DisabledUser,
|
||||||
|
Message: "The user account is disabled.",
|
||||||
|
Status: http.StatusConflict,
|
||||||
|
},
|
||||||
|
StatusCode: http.StatusConflict,
|
||||||
|
}, nil
|
||||||
|
case "crash@email.com":
|
||||||
|
return nil, errors.New("simulated server crash") //nolint:err113
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.SignInEmailPassword200JSONResponse{
|
||||||
|
Session: &api.Session{
|
||||||
|
AccessToken: "access_token_example",
|
||||||
|
AccessTokenExpiresIn: 900, //nolint:mnd
|
||||||
|
RefreshToken: "refresh_token_example",
|
||||||
|
RefreshTokenId: "refresh_token_id_example",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ChangeUserEmail( //nolint:ireturn
|
||||||
|
_ context.Context,
|
||||||
|
_ api.ChangeUserEmailRequestObject,
|
||||||
|
) (api.ChangeUserEmailResponseObject, error) {
|
||||||
|
return api.ChangeUserEmail200JSONResponse(api.OK), nil
|
||||||
|
}
|
||||||
109
internal/lib/oapi/example/main.go
Normal file
109
internal/lib/oapi/example/main.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3filter"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/example/api"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/example/controller"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiPrefix = "/"
|
||||||
|
|
||||||
|
func getLogger() *slog.Logger {
|
||||||
|
handler := tint.NewHandler(os.Stdout, &tint.Options{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
TimeFormat: time.StampMilli,
|
||||||
|
NoColor: false,
|
||||||
|
ReplaceAttr: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
return slog.New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authFn(
|
||||||
|
ctx context.Context,
|
||||||
|
input *openapi3filter.AuthenticationInput,
|
||||||
|
) error {
|
||||||
|
_, ok := ctx.Value(oapi.GinContextKey).(*gin.Context)
|
||||||
|
if !ok {
|
||||||
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "unable to get context",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "your access token is invalid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRouter(logger *slog.Logger) (*gin.Engine, error) {
|
||||||
|
ctrl := controller.NewController()
|
||||||
|
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
|
||||||
|
|
||||||
|
router, mw, err := oapi.NewRouter(
|
||||||
|
api.OpenAPISchema,
|
||||||
|
apiPrefix,
|
||||||
|
authFn,
|
||||||
|
middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create oapi router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api.RegisterHandlersWithOptions(
|
||||||
|
router,
|
||||||
|
handler,
|
||||||
|
api.GinServerOptions{
|
||||||
|
BaseURL: apiPrefix,
|
||||||
|
Middlewares: []api.MiddlewareFunc{mw},
|
||||||
|
ErrorHandler: nil,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context) error {
|
||||||
|
logger := getLogger()
|
||||||
|
|
||||||
|
router, err := setupRouter(logger) //nolint:contextcheck
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{ //nolint:exhaustruct
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: router,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second, //nolint:mnd
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
logger.ErrorContext(ctx, "server failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(context.Background()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
internal/lib/oapi/example/main_test.go
Normal file
177
internal/lib/oapi/example/main_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeRequest(
|
||||||
|
router *gin.Engine,
|
||||||
|
method, path string,
|
||||||
|
headers map[string]string,
|
||||||
|
body io.Reader,
|
||||||
|
) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(method, path, body)
|
||||||
|
|
||||||
|
for key, value := range headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequests(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger := getLogger()
|
||||||
|
|
||||||
|
router, err := setupRouter(logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to set up router: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
headers map[string]string
|
||||||
|
body io.Reader
|
||||||
|
expectedStatus int
|
||||||
|
expectedResponse string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(`{"email": "asd@asd.com", "password": "p4ssw0rd"}`),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedResponse: "{\"session\":{\"accessToken\":\"access_token_example\",\"accessTokenExpiresIn\":900,\"refreshToken\":\"refresh_token_example\",\"refreshTokenId\":\"refresh_token_id_example\"}}\n", //nolint:lll
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "expected error",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(
|
||||||
|
`{"email": "bad@email.com", "password": "p4ssw0rd"}`,
|
||||||
|
),
|
||||||
|
expectedStatus: http.StatusConflict,
|
||||||
|
expectedResponse: "{\"error\":\"disabled-user\",\"message\":\"The user account is disabled.\",\"status\":409}\n", //nolint:lll
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "unexpected error",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(
|
||||||
|
`{"email": "crash@email.com", "password": "p4ssw0rd"}`,
|
||||||
|
),
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
expectedResponse: `{"errors":"internal-server-error","message":"simulated server crash"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "missing body",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: nil,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedResponse: `{"error":"request-validation-error","reason":"value is required but missing"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "wrong param",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(
|
||||||
|
`{"wrong":"asd", "email": "asd@asd.com", "password": "p4ssw0rd"}`,
|
||||||
|
),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedResponse: `{"error":"schema-validation-error","reason":"property \"wrong\" is unsupported"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "missing param",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(`{"email": "asd@asd.com"}`),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedResponse: `{"error":"schema-validation-error","reason":"property \"password\" is missing"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "invalid param",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/signin/email-password",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(`{"email": "asdasd.com", "password": "p4ssw0rd"}`),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedResponse: `{"errors":"bad-request","message":"email: failed to pass regex validation"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "needs security",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/user/email/change",
|
||||||
|
headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: strings.NewReader(`{"newEmail": "new@asd.com"`),
|
||||||
|
expectedStatus: http.StatusUnauthorized,
|
||||||
|
expectedResponse: `{"error":"unauthorized","reason":"your access token is invalid","securityScheme":"BearerAuthElevated"}`, //nolint:lll
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := makeRequest(router, tc.method, tc.path, tc.headers, tc.body)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != tc.expectedStatus {
|
||||||
|
t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(string(body), tc.expectedResponse); diff != "" {
|
||||||
|
t.Errorf("Response body mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
140
internal/lib/oapi/middleware/cors.go
Normal file
140
internal/lib/oapi/middleware/cors.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORSOptions configures the CORS middleware behavior.
|
||||||
|
//
|
||||||
|
// The middleware supports three strategies for handling Access-Control-Allow-Headers:
|
||||||
|
// - nil (default): Reflects the Access-Control-Request-Headers from the client
|
||||||
|
// - empty slice: Denies all headers (no Access-Control-Allow-Headers header is set)
|
||||||
|
// - non-empty slice: Uses the specified headers
|
||||||
|
type CORSOptions struct {
|
||||||
|
// AllowedOrigins is a list of origins permitted to make cross-origin requests.
|
||||||
|
// Use "*" or nil slice to allow all origins.
|
||||||
|
AllowedOrigins []string
|
||||||
|
|
||||||
|
// AllowedMethods is a list of HTTP methods the client is permitted to use.
|
||||||
|
// Common values: GET, POST, PUT, DELETE, PATCH, OPTIONS.
|
||||||
|
AllowedMethods []string
|
||||||
|
|
||||||
|
// AllowedHeaders controls which headers clients can use in requests.
|
||||||
|
// - nil: reflects client's Access-Control-Request-Headers (permissive)
|
||||||
|
// - empty slice: denies all headers
|
||||||
|
// - non-empty: allows only specified headers
|
||||||
|
AllowedHeaders []string
|
||||||
|
|
||||||
|
// ExposedHeaders lists headers that browsers are allowed to access.
|
||||||
|
// By default, browsers only expose simple response headers.
|
||||||
|
ExposedHeaders []string
|
||||||
|
|
||||||
|
// AllowCredentials indicates whether the request can include credentials
|
||||||
|
// (cookies, authorization headers, or TLS client certificates).
|
||||||
|
AllowCredentials bool
|
||||||
|
|
||||||
|
// MaxAge indicates how long (in seconds) the results of a preflight request
|
||||||
|
// can be cached. Empty string means no caching directive is sent.
|
||||||
|
MaxAge string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS returns a Gin middleware handler that implements Cross-Origin Resource Sharing (CORS).
|
||||||
|
//
|
||||||
|
// The middleware handles both preflight (OPTIONS) requests and actual requests, setting
|
||||||
|
// appropriate CORS headers based on the provided configuration. It automatically adds
|
||||||
|
// the "Vary: Origin, Access-Control-Request-Method" header for proper cache behavior.
|
||||||
|
//
|
||||||
|
// For preflight requests (OPTIONS), the middleware responds with 204 No Content and
|
||||||
|
// prevents further request processing. For actual requests, it sets CORS headers and
|
||||||
|
// continues the middleware chain.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// router.Use(middleware.CORS(middleware.CORSOptions{
|
||||||
|
// AllowedOrigins: []string{"https://example.com", "https://app.example.com"},
|
||||||
|
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
|
// AllowedHeaders: nil, // reflects client headers
|
||||||
|
// AllowCredentials: true,
|
||||||
|
// MaxAge: "3600",
|
||||||
|
// }))
|
||||||
|
func CORS(opts CORSOptions) gin.HandlerFunc { //nolint:cyclop,funlen
|
||||||
|
allowedMethods := strings.Join(opts.AllowedMethods, ", ")
|
||||||
|
exposedHeaders := strings.Join(opts.ExposedHeaders, ", ")
|
||||||
|
|
||||||
|
allowCredentials := "false"
|
||||||
|
if opts.AllowCredentials {
|
||||||
|
allowCredentials = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
headerStrategy string // "reflect", "specific", or "deny"
|
||||||
|
allowedHeaders string
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case opts.AllowedHeaders == nil:
|
||||||
|
headerStrategy = "reflect"
|
||||||
|
case len(opts.AllowedHeaders) == 0:
|
||||||
|
headerStrategy = "deny"
|
||||||
|
default:
|
||||||
|
headerStrategy = "specific"
|
||||||
|
allowedHeaders = strings.Join(opts.AllowedHeaders, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
f := func(c *gin.Context, origin string) {
|
||||||
|
if opts.AllowedOrigins != nil &&
|
||||||
|
!slices.Contains(opts.AllowedOrigins, origin) &&
|
||||||
|
!slices.Contains(opts.AllowedOrigins, "*") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Header("Access-Control-Allow-Methods", allowedMethods)
|
||||||
|
|
||||||
|
// Handle allowed headers based on strategy
|
||||||
|
switch headerStrategy {
|
||||||
|
case "specific":
|
||||||
|
c.Header("Access-Control-Allow-Headers", allowedHeaders)
|
||||||
|
case "reflect":
|
||||||
|
headers := c.Request.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if headers != "" {
|
||||||
|
c.Header("Access-Control-Allow-Headers", headers)
|
||||||
|
}
|
||||||
|
case "deny":
|
||||||
|
// Don't set the header at all
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposedHeaders != "" {
|
||||||
|
c.Header("Access-Control-Expose-Headers", exposedHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Access-Control-Allow-Credentials", allowCredentials)
|
||||||
|
|
||||||
|
if opts.MaxAge != "" {
|
||||||
|
c.Header("Access-Control-Max-Age", opts.MaxAge)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
if c.Request.Method == http.MethodOptions {
|
||||||
|
f(c, origin)
|
||||||
|
|
||||||
|
c.Header("Content-Length", "0")
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if origin != "" {
|
||||||
|
f(c, origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
323
internal/lib/oapi/middleware/cors_test.go
Normal file
323
internal/lib/oapi/middleware/cors_test.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
package middleware_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCORS(t *testing.T) { //nolint:maintidx
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
opts middleware.CORSOptions
|
||||||
|
requestMethod string
|
||||||
|
requestOrigin string
|
||||||
|
requestHeaders map[string]string
|
||||||
|
wantStatus int
|
||||||
|
wantHeaders http.Header
|
||||||
|
expectNext bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with allowed origin",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": []string{"https://example.com"},
|
||||||
|
"Access-Control-Allow-Methods": []string{"GET, POST"},
|
||||||
|
"Access-Control-Allow-Headers": []string{"Content-Type, Authorization"},
|
||||||
|
"Access-Control-Allow-Credentials": []string{"false"},
|
||||||
|
"Vary": []string{
|
||||||
|
"Origin, Access-Control-Request-Method",
|
||||||
|
},
|
||||||
|
"Content-Length": []string{"0"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with wildcard origin",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
requestOrigin: "https://any-origin.com",
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": []string{"https://any-origin.com"},
|
||||||
|
"Access-Control-Allow-Methods": []string{"GET, POST, PUT, DELETE"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with disallowed origin",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
requestOrigin: "https://malicious.com",
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with reflected headers (nil)",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"POST"},
|
||||||
|
AllowedHeaders: nil,
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{
|
||||||
|
"Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header",
|
||||||
|
},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Headers": []string{"X-Custom-Header, X-Another-Header"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with denied headers (empty slice)",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"POST"},
|
||||||
|
AllowedHeaders: []string{},
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{
|
||||||
|
"Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header",
|
||||||
|
},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with nil headers and no request headers",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
AllowedHeaders: nil,
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with credentials enabled",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Credentials": []string{"true"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with MaxAge",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
MaxAge: "3600",
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Max-Age": []string{"3600"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS request with exposed headers",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
ExposedHeaders: []string{"X-Custom-Response", "X-Total-Count"},
|
||||||
|
},
|
||||||
|
requestMethod: "OPTIONS",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusNoContent,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Expose-Headers": []string{"X-Custom-Response, X-Total-Count"},
|
||||||
|
},
|
||||||
|
expectNext: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET request with allowed origin",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
AllowedHeaders: []string{"Content-Type"},
|
||||||
|
},
|
||||||
|
requestMethod: "GET",
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": []string{"https://example.com"},
|
||||||
|
"Access-Control-Allow-Methods": []string{"GET, POST"},
|
||||||
|
"Access-Control-Allow-Headers": []string{"Content-Type"},
|
||||||
|
},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST request with disallowed origin",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestOrigin: "https://malicious.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET request without origin header",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{"https://example.com"},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
},
|
||||||
|
requestMethod: "GET",
|
||||||
|
requestOrigin: "",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET request with empty allowed origins (denies all)",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
},
|
||||||
|
requestMethod: "GET",
|
||||||
|
requestOrigin: "https://any-origin.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET request with nil allowed origins (allows all)",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: nil,
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
},
|
||||||
|
requestMethod: "GET",
|
||||||
|
requestOrigin: "https://any-origin.com",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": []string{"https://any-origin.com"},
|
||||||
|
},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET request with multiple allowed origins",
|
||||||
|
opts: middleware.CORSOptions{ //nolint:exhaustruct
|
||||||
|
AllowedOrigins: []string{
|
||||||
|
"https://example.com",
|
||||||
|
"https://another-example.com",
|
||||||
|
"https://third-example.com",
|
||||||
|
},
|
||||||
|
AllowedMethods: []string{"GET"},
|
||||||
|
},
|
||||||
|
requestMethod: "GET",
|
||||||
|
requestHeaders: map[string]string{},
|
||||||
|
requestOrigin: "https://another-example.com",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantHeaders: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": []string{"https://another-example.com"},
|
||||||
|
},
|
||||||
|
expectNext: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Setup router with CORS middleware
|
||||||
|
router := gin.New()
|
||||||
|
nextCalled := false
|
||||||
|
|
||||||
|
router.Use(middleware.CORS(tc.opts))
|
||||||
|
router.Any("/test", func(c *gin.Context) {
|
||||||
|
nextCalled = true
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
req := httptest.NewRequest(tc.requestMethod, "/test", nil)
|
||||||
|
if tc.requestOrigin != "" {
|
||||||
|
req.Header.Set("Origin", tc.requestOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional request headers
|
||||||
|
for key, value := range tc.requestHeaders {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record response
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if w.Code != tc.wantStatus {
|
||||||
|
t.Errorf("expected status %d, got %d", tc.wantStatus, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expected headers using cmp.Diff
|
||||||
|
// Only compare headers that are expected
|
||||||
|
gotHeaders := make(http.Header)
|
||||||
|
for key := range tc.wantHeaders {
|
||||||
|
if values := w.Header().Values(key); len(values) > 0 {
|
||||||
|
gotHeaders[key] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.wantHeaders, gotHeaders); diff != "" {
|
||||||
|
t.Errorf("response headers mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Next() was called
|
||||||
|
if nextCalled != tc.expectNext {
|
||||||
|
t.Errorf("expected Next() called to be %v, got %v", tc.expectNext, nextCalled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ func LoggerFromContext(ctx context.Context) *slog.Logger { //nolint:contextcheck
|
|||||||
return logger
|
return logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logger is a Gin middleware that logs HTTP requests and responses using slog.
|
||||||
func Logger(logger *slog.Logger) gin.HandlerFunc {
|
func Logger(logger *slog.Logger) gin.HandlerFunc {
|
||||||
return func(ctx *gin.Context) {
|
return func(ctx *gin.Context) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
69
internal/lib/oapi/oapi.go
Normal file
69
internal/lib/oapi/oapi.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package oapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/getkin/kin-openapi/openapi3filter"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/example/api"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func surfaceErrorsMiddleWare(c *gin.Context) {
|
||||||
|
// this captures two cases as far as I can see:
|
||||||
|
// 1. request validation errors where the strict generated code fails
|
||||||
|
// to bind the request to the struct (i.e. "invalid param" test)
|
||||||
|
// 2. when a handler returns an error instead of a response
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
if len(c.Errors) > 0 && !c.IsAborted() {
|
||||||
|
var errorCode string
|
||||||
|
switch c.Writer.Status() {
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
errorCode = "bad-request"
|
||||||
|
default:
|
||||||
|
errorCode = "internal-server-error"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(
|
||||||
|
c.Writer.Status(),
|
||||||
|
gin.H{"errors": errorCode, "message": c.Errors[0].Error()},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a Gin router with OpenAPI request validation middleware.
|
||||||
|
func NewRouter(
|
||||||
|
schema []byte,
|
||||||
|
apiPrefix string,
|
||||||
|
authenticationFunc openapi3filter.AuthenticationFunc,
|
||||||
|
corsOptions middleware.CORSOptions,
|
||||||
|
logger *slog.Logger,
|
||||||
|
) (*gin.Engine, func(c *gin.Context), error) {
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
loader := openapi3.NewLoader()
|
||||||
|
|
||||||
|
doc, err := loader.LoadFromData(schema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
|
||||||
|
URL: apiPrefix,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Use(
|
||||||
|
gin.Recovery(),
|
||||||
|
surfaceErrorsMiddleWare,
|
||||||
|
middleware.Logger(logger),
|
||||||
|
middleware.CORS(corsOptions),
|
||||||
|
)
|
||||||
|
|
||||||
|
mw := api.MiddlewareFunc(requestValidatorWithOptions(doc, authenticationFunc))
|
||||||
|
|
||||||
|
return router, mw, nil
|
||||||
|
}
|
||||||
128
internal/lib/oapi/request.go
Normal file
128
internal/lib/oapi/request.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package oapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/getkin/kin-openapi/openapi3filter"
|
||||||
|
"github.com/getkin/kin-openapi/routers"
|
||||||
|
"github.com/getkin/kin-openapi/routers/gorillamux"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GinContextKey ContextKey = "nhost-oapi/gin-context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleError(c *gin.Context, err error) {
|
||||||
|
var (
|
||||||
|
errReq *openapi3filter.RequestError
|
||||||
|
errSchema *openapi3.SchemaError
|
||||||
|
errAuth *AuthenticatorError
|
||||||
|
errSec *openapi3filter.SecurityRequirementsError
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &errSchema):
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "schema-validation-error",
|
||||||
|
"reason": errSchema.Reason,
|
||||||
|
})
|
||||||
|
case errors.As(err, &errReq):
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "request-validation-error",
|
||||||
|
"reason": errReq.Err.Error(),
|
||||||
|
})
|
||||||
|
case errors.As(err, &errAuth):
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"error": errAuth.Code,
|
||||||
|
"reason": errAuth.Message,
|
||||||
|
"securityScheme": errAuth.Scheme,
|
||||||
|
})
|
||||||
|
case errors.As(err, &errSec):
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"error": "unauthorized",
|
||||||
|
"reason": errSec.Error(),
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestValidatorWithOptions(
|
||||||
|
swagger *openapi3.T,
|
||||||
|
authFn openapi3filter.AuthenticationFunc,
|
||||||
|
) gin.HandlerFunc {
|
||||||
|
router, err := gorillamux.NewRouter(swagger)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if err := validateRequestFromContext(c, router, authFn); err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRequestFromContext(
|
||||||
|
c *gin.Context,
|
||||||
|
router routers.Router,
|
||||||
|
authFn openapi3filter.AuthenticationFunc,
|
||||||
|
) error {
|
||||||
|
route, pathParams, err := router.FindRoute(c.Request)
|
||||||
|
if err != nil {
|
||||||
|
var e *routers.RouteError
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &e):
|
||||||
|
return e
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("error validating route: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validationInput := &openapi3filter.RequestValidationInput{ //nolint:exhaustruct
|
||||||
|
Request: c.Request,
|
||||||
|
PathParams: pathParams,
|
||||||
|
Route: route,
|
||||||
|
Options: &openapi3filter.Options{
|
||||||
|
AuthenticationFunc: authFn,
|
||||||
|
ExcludeRequestBody: false,
|
||||||
|
ExcludeRequestQueryParams: false,
|
||||||
|
ExcludeResponseBody: false,
|
||||||
|
ExcludeReadOnlyValidations: false,
|
||||||
|
ExcludeWriteOnlyValidations: false,
|
||||||
|
IncludeResponseStatus: false,
|
||||||
|
MultiError: false,
|
||||||
|
RegexCompiler: nil,
|
||||||
|
SkipSettingDefaults: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
requestContext := context.WithValue(c.Request.Context(), GinContextKey, c)
|
||||||
|
if err := openapi3filter.ValidateRequest(requestContext, validationInput); err != nil {
|
||||||
|
return err //nolint:wrapcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGinContext(c context.Context) *gin.Context {
|
||||||
|
v := c.Value(GinContextKey)
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ginCtx, ok := v.(*gin.Context)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ginCtx
|
||||||
|
}
|
||||||
@@ -56,12 +56,17 @@ in
|
|||||||
{ buildInputs ? [ ]
|
{ buildInputs ? [ ]
|
||||||
, shellHook ? ""
|
, shellHook ? ""
|
||||||
}: pkgs.mkShell {
|
}: pkgs.mkShell {
|
||||||
inherit shellHook;
|
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
gnumake
|
gnumake
|
||||||
nixpkgs-fmt
|
nixpkgs-fmt
|
||||||
] ++ goCheckDeps ++ buildInputs;
|
] ++ goCheckDeps ++ buildInputs;
|
||||||
|
|
||||||
|
shellHook = shellHook + pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
|
||||||
|
export SDKROOT=${pkgs.apple-sdk_12}
|
||||||
|
export SDKROOT_FOR_TARGET=${pkgs.apple-sdk_12}
|
||||||
|
export DEVELOPER_DIR=${pkgs.apple-sdk_12}
|
||||||
|
export DEVELOPER_DIR_FOR_TARGET=${pkgs.apple-sdk_12}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
check =
|
check =
|
||||||
@@ -109,13 +114,13 @@ in
|
|||||||
echo "➜ Running golangci-lint"
|
echo "➜ Running golangci-lint"
|
||||||
golangci-lint run \
|
golangci-lint run \
|
||||||
--timeout 600s \
|
--timeout 600s \
|
||||||
./${submodule}/...
|
./...
|
||||||
|
|
||||||
echo "➜ Running tests"
|
echo "➜ Running tests"
|
||||||
richgo test \
|
richgo test \
|
||||||
-tags="${pkgs.lib.strings.concatStringsSep " " tags}" \
|
-tags="${pkgs.lib.strings.concatStringsSep " " tags}" \
|
||||||
-ldflags="${pkgs.lib.strings.concatStringsSep " " ldflags}" \
|
-ldflags="${pkgs.lib.strings.concatStringsSep " " ldflags}" \
|
||||||
-v ${goTestFlags} ./${submodule}/...
|
-v ${goTestFlags} ./...
|
||||||
|
|
||||||
${extraCheck}
|
${extraCheck}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const updateSessionFromResponseMiddleware = (
|
|||||||
return body.session || null;
|
return body.session || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("accessToken" in body && "refreshToken" in body) {
|
if ("accessToken" in body && "refreshToken" in body && "user" in body) {
|
||||||
// Session
|
// Session
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -115,13 +115,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) YourEndpoint( //nolint:ireturn
|
func (ctrl *Controller) YourEndpoint( //nolint:ireturn
|
||||||
ctx context.Context, request api.YourEndpointRequestObject,
|
ctx context.Context, request api.YourEndpointRequestObject,
|
||||||
) (api.YourEndpointResponseObject, error) {
|
) (api.YourEndpointResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if apiErr := ctrl.wf.ValidateInput(request.Body.Field, logger); apiErr != nil {
|
if apiErr := ctrl.wf.ValidateInput(request.Body.Field, logger); apiErr != nil {
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func cors() gin.HandlerFunc {
|
|
||||||
f := func(c *gin.Context, origin string) {
|
|
||||||
c.Header("Access-Control-Allow-Origin", origin)
|
|
||||||
c.Header("Access-Control-Allow-Methods", "POST, GET")
|
|
||||||
headers := c.Request.Header.Get("Access-Control-Request-Headers")
|
|
||||||
c.Header("Access-Control-Allow-Headers", headers)
|
|
||||||
c.Header("Access-Control-Allow-Credentials", "true")
|
|
||||||
c.Header("Access-Control-Max-Age", "86400")
|
|
||||||
c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method")
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
origin := c.Request.Header.Get("Origin")
|
|
||||||
if c.Request.Method == http.MethodOptions {
|
|
||||||
f(c, origin)
|
|
||||||
|
|
||||||
c.Header("Content-Length", "0")
|
|
||||||
c.AbortWithStatus(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if origin != "" {
|
|
||||||
f(c, origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bradfitz/gomemcache/memcache"
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
|
||||||
"github.com/getkin/kin-openapi/openapi3filter"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/docs"
|
"github.com/nhost/nhost/services/auth/docs"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/controller"
|
"github.com/nhost/nhost/services/auth/go/controller"
|
||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/nhost/nhost/services/auth/go/oidc"
|
"github.com/nhost/nhost/services/auth/go/oidc"
|
||||||
"github.com/nhost/nhost/services/auth/go/providers"
|
"github.com/nhost/nhost/services/auth/go/providers"
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
ginmiddleware "github.com/oapi-codegen/gin-middleware"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1296,87 +1295,42 @@ func getDependencies( //nolint:ireturn
|
|||||||
return emailer, sms, jwtGetter, idTokenValidator, nil
|
return emailer, sms, jwtGetter, idTokenValidator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGoServer( //nolint:funlen
|
func getCORSOptions() oapimw.CORSOptions {
|
||||||
|
return oapimw.CORSOptions{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"POST", "GET"},
|
||||||
|
AllowedHeaders: nil,
|
||||||
|
ExposedHeaders: []string{},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: "86400",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGoServer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cmd *cli.Command,
|
cmd *cli.Command,
|
||||||
db *sql.Queries,
|
db *sql.Queries,
|
||||||
encrypter *crypto.Encrypter,
|
encrypter *crypto.Encrypter,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
) (*http.Server, error) {
|
) (*http.Server, error) {
|
||||||
router := gin.New()
|
ctrl, jwtGetter, err := getController(ctx, cmd, db, encrypter, logger)
|
||||||
|
|
||||||
loader := openapi3.NewLoader()
|
|
||||||
|
|
||||||
doc, err := loader.LoadFromData(docs.OpenAPISchema)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
|
|
||||||
URL: cmd.String(flagAPIPrefix),
|
|
||||||
})
|
|
||||||
|
|
||||||
handlers := []gin.HandlerFunc{
|
|
||||||
// ginmiddleware.OapiRequestValidator(doc),
|
|
||||||
gin.Recovery(),
|
|
||||||
cors(),
|
|
||||||
middleware.Logger(logger), //nolint:contextcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Bool(flagRateLimitEnable) {
|
|
||||||
handlers = append(handlers, getRateLimiter(cmd, logger)) //nolint:contextcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.String(flagTurnstileSecret) != "" {
|
|
||||||
handlers = append(handlers, middleware.Tunrstile( //nolint:contextcheck
|
|
||||||
cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Use(handlers...)
|
|
||||||
|
|
||||||
config, err := getConfig(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("problem creating config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthProviders, err := getOauth2Providers(ctx, cmd, logger)
|
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("problem creating oauth providers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := controller.New(
|
router, mw, err := oapi.NewRouter( //nolint:contextcheck
|
||||||
db,
|
docs.OpenAPISchema,
|
||||||
config,
|
cmd.String(flagAPIPrefix),
|
||||||
jwtGetter,
|
jwtGetter.MiddlewareFunc,
|
||||||
emailer,
|
getCORSOptions(),
|
||||||
smsClient,
|
logger,
|
||||||
hibp.NewClient(),
|
|
||||||
oauthProviders,
|
|
||||||
idTokenValidator,
|
|
||||||
controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now),
|
|
||||||
encrypter,
|
|
||||||
cmd.Root().Version,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create controller: %w", err)
|
return nil, fmt.Errorf("failed to create router: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
|
|
||||||
mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions(
|
|
||||||
doc,
|
|
||||||
&ginmiddleware.Options{ //nolint:exhaustruct
|
|
||||||
Options: openapi3filter.Options{ //nolint:exhaustruct
|
|
||||||
AuthenticationFunc: jwtGetter.MiddlewareFunc,
|
|
||||||
},
|
|
||||||
SilenceServersWarning: true,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
api.RegisterHandlersWithOptions(
|
api.RegisterHandlersWithOptions(
|
||||||
router,
|
router,
|
||||||
handler,
|
handler,
|
||||||
@@ -1387,6 +1341,16 @@ func getGoServer( //nolint:funlen
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if cmd.Bool(flagRateLimitEnable) {
|
||||||
|
router.Use(getRateLimiter(cmd, logger)) //nolint:contextcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.String(flagTurnstileSecret) != "" {
|
||||||
|
router.Use(middleware.Tunrstile( //nolint:contextcheck
|
||||||
|
cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Bool(flagEnableChangeEnv) {
|
if cmd.Bool(flagEnableChangeEnv) {
|
||||||
router.POST(cmd.String(flagAPIPrefix)+"/change-env", ctrl.PostChangeEnv)
|
router.POST(cmd.String(flagAPIPrefix)+"/change-env", ctrl.PostChangeEnv)
|
||||||
}
|
}
|
||||||
@@ -1410,6 +1374,48 @@ func getGoServer( //nolint:funlen
|
|||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getController(
|
||||||
|
ctx context.Context,
|
||||||
|
cmd *cli.Command,
|
||||||
|
db *sql.Queries,
|
||||||
|
encrypter *crypto.Encrypter,
|
||||||
|
logger *slog.Logger,
|
||||||
|
) (*controller.Controller, *controller.JWTGetter, error) {
|
||||||
|
config, err := getConfig(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("problem creating config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthProviders, err := getOauth2Providers(ctx, cmd, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("problem creating oauth providers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := controller.New(
|
||||||
|
db,
|
||||||
|
config,
|
||||||
|
jwtGetter,
|
||||||
|
emailer,
|
||||||
|
smsClient,
|
||||||
|
hibp.NewClient(),
|
||||||
|
oauthProviders,
|
||||||
|
idTokenValidator,
|
||||||
|
controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now),
|
||||||
|
encrypter,
|
||||||
|
cmd.Root().Version,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create controller: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl, jwtGetter, nil
|
||||||
|
}
|
||||||
|
|
||||||
func serve(ctx context.Context, cmd *cli.Command) error {
|
func serve(ctx context.Context, cmd *cli.Command) error {
|
||||||
logger := getLogger(cmd.Bool(flagDebug), cmd.Bool(flagLogFormatTEXT))
|
logger := getLogger(cmd.Bool(flagDebug), cmd.Bool(flagLogFormatTEXT))
|
||||||
logger.InfoContext(ctx, cmd.Root().Name+" v"+cmd.Root().Version)
|
logger.InfoContext(ctx, cmd.Root().Name+" v"+cmd.Root().Version)
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import (
|
|||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) AddSecurityKey( //nolint:ireturn
|
func (ctrl *Controller) AddSecurityKey( //nolint:ireturn
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
_ api.AddSecurityKeyRequestObject,
|
_ api.AddSecurityKeyRequestObject,
|
||||||
) (api.AddSecurityKeyResponseObject, error) {
|
) (api.AddSecurityKeyResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) ChangeUserEmail( //nolint:ireturn
|
func (ctrl *Controller) ChangeUserEmail( //nolint:ireturn
|
||||||
ctx context.Context, request api.ChangeUserEmailRequestObject,
|
ctx context.Context, request api.ChangeUserEmailRequestObject,
|
||||||
) (api.ChangeUserEmailResponseObject, error) {
|
) (api.ChangeUserEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) ChangeUserMfa( //nolint:ireturn
|
func (ctrl *Controller) ChangeUserMfa( //nolint:ireturn
|
||||||
ctx context.Context, _ api.ChangeUserMfaRequestObject,
|
ctx context.Context, _ api.ChangeUserMfaRequestObject,
|
||||||
) (api.ChangeUserMfaResponseObject, error) {
|
) (api.ChangeUserMfaResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.MfaEnabled {
|
if !ctrl.config.MfaEnabled {
|
||||||
logger.WarnContext(ctx, "mfa disabled")
|
logger.WarnContext(ctx, "mfa disabled")
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) postUserPasswordAuthenticated( //nolint:ireturn
|
func (ctrl *Controller) postUserPasswordAuthenticated( //nolint:ireturn
|
||||||
@@ -61,7 +61,7 @@ func (ctrl *Controller) ChangeUserPassword( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.ChangeUserPasswordRequestObject,
|
request api.ChangeUserPasswordRequestObject,
|
||||||
) (api.ChangeUserPasswordResponseObject, error) {
|
) (api.ChangeUserPasswordResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
jwtToken, ok := ctrl.wf.jwtGetter.FromContext(ctx)
|
jwtToken, ok := ctrl.wf.jwtGetter.FromContext(ctx)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) CreatePAT( //nolint:ireturn
|
func (ctrl *Controller) CreatePAT( //nolint:ireturn
|
||||||
ctx context.Context, request api.CreatePATRequestObject,
|
ctx context.Context, request api.CreatePATRequestObject,
|
||||||
) (api.CreatePATResponseObject, error) {
|
) (api.CreatePATResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ func (ctrl *Controller) postUserDeanonymizeValidateRequest( //nolint:cyclop
|
|||||||
func (ctrl *Controller) DeanonymizeUser( //nolint:funlen
|
func (ctrl *Controller) DeanonymizeUser( //nolint:funlen
|
||||||
ctx context.Context, request api.DeanonymizeUserRequestObject,
|
ctx context.Context, request api.DeanonymizeUserRequestObject,
|
||||||
) (api.DeanonymizeUserResponseObject, error) {
|
) (api.DeanonymizeUserResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
userID, password, options, apiError := ctrl.postUserDeanonymizeValidateRequest(
|
userID, password, options, apiError := ctrl.postUserDeanonymizeValidateRequest(
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) ElevateWebauthn( //nolint:ireturn
|
func (ctrl *Controller) ElevateWebauthn( //nolint:ireturn
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
_ api.ElevateWebauthnRequestObject,
|
_ api.ElevateWebauthnRequestObject,
|
||||||
) (api.ElevateWebauthnResponseObject, error) {
|
) (api.ElevateWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ func (e *APIError) Error() string {
|
|||||||
return fmt.Sprintf("API error: %s", e.t)
|
return fmt.Sprintf("API error: %s", e.t)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrElevatedClaimRequired = errors.New("elevated-claim-required")
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrJWTConfiguration = errors.New("jwt-configuration")
|
ErrJWTConfiguration = errors.New("jwt-configuration")
|
||||||
|
|
||||||
@@ -525,7 +523,7 @@ func (ctrl *Controller) sendRedirectError(
|
|||||||
) ErrorRedirectResponse {
|
) ErrorRedirectResponse {
|
||||||
errResponse := ctrl.getError(err)
|
errResponse := ctrl.getError(err)
|
||||||
|
|
||||||
redirectURL = generateRedirectURL(redirectURL, map[string]string{
|
redirectURL = appendURLValues(redirectURL, map[string]string{
|
||||||
"error": string(errResponse.Error),
|
"error": string(errResponse.Error),
|
||||||
"errorDescription": errResponse.Message,
|
"errorDescription": errResponse.Message,
|
||||||
})
|
})
|
||||||
@@ -567,17 +565,3 @@ func sqlIsDuplcateError(err error, fkey string) bool {
|
|||||||
return strings.Contains(err.Error(), "SQLSTATE 23505") &&
|
return strings.Contains(err.Error(), "SQLSTATE 23505") &&
|
||||||
strings.Contains(err.Error(), fkey)
|
strings.Contains(err.Error(), fkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRedirectURL(
|
|
||||||
redirectTo *url.URL,
|
|
||||||
opts map[string]string,
|
|
||||||
) *url.URL {
|
|
||||||
q := redirectTo.Query()
|
|
||||||
for k, v := range opts {
|
|
||||||
q.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectTo.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
return redirectTo
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func (ctrl *Controller) GetProviderTokens( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req api.GetProviderTokensRequestObject,
|
req api.GetProviderTokensRequestObject,
|
||||||
) (api.GetProviderTokensResponseObject, error) {
|
) (api.GetProviderTokensResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
logger = logger.With("provider", req.Provider)
|
logger = logger.With("provider", req.Provider)
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/oapi-codegen/runtime/types"
|
"github.com/oapi-codegen/runtime/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) GetUser( //nolint:ireturn
|
func (ctrl *Controller) GetUser( //nolint:ireturn
|
||||||
ctx context.Context, _ api.GetUserRequestObject,
|
ctx context.Context, _ api.GetUserRequestObject,
|
||||||
) (api.GetUserResponseObject, error) {
|
) (api.GetUserResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
// Get authenticated user from JWT
|
// Get authenticated user from JWT
|
||||||
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -17,8 +16,8 @@ import (
|
|||||||
"github.com/getkin/kin-openapi/openapi3filter"
|
"github.com/getkin/kin-openapi/openapi3filter"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
ginmiddleware "github.com/oapi-codegen/gin-middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const JWTContextKey = "nhost/auth/jwt"
|
const JWTContextKey = "nhost/auth/jwt"
|
||||||
@@ -340,7 +339,7 @@ func (j *JWTGetter) Validate(accessToken string) (*jwt.Token, error) {
|
|||||||
func (j *JWTGetter) FromContext(ctx context.Context) (*jwt.Token, bool) {
|
func (j *JWTGetter) FromContext(ctx context.Context) (*jwt.Token, bool) {
|
||||||
token, ok := ctx.Value(JWTContextKey).(*jwt.Token)
|
token, ok := ctx.Value(JWTContextKey).(*jwt.Token)
|
||||||
if !ok { //nolint:nestif
|
if !ok { //nolint:nestif
|
||||||
c := ginmiddleware.GetGinContext(ctx)
|
c := oapi.GetGinContext(ctx)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
a, ok := c.Get(JWTContextKey)
|
a, ok := c.Get(JWTContextKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -415,16 +414,28 @@ func (j *JWTGetter) MiddlewareFunc(
|
|||||||
|
|
||||||
parts := strings.Split(authHeader, " ")
|
parts := strings.Split(authHeader, " ")
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
return errors.New("invalid authorization header") //nolint:err113
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "missing or malformed authorization header",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtToken, err := j.Validate(parts[1])
|
jwtToken, err := j.Validate(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error validating token: %w", err)
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: fmt.Sprintf("error validating token: %s", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !jwtToken.Valid {
|
if !jwtToken.Valid {
|
||||||
return errors.New("invalid token") //nolint:err113
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "invalid token",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.SecuritySchemeName == "BearerAuthElevated" {
|
if input.SecuritySchemeName == "BearerAuthElevated" {
|
||||||
@@ -435,15 +446,23 @@ func (j *JWTGetter) MiddlewareFunc(
|
|||||||
|
|
||||||
found, err := j.verifyElevatedClaim(ctx, jwtToken, requestPath)
|
found, err := j.verifyElevatedClaim(ctx, jwtToken, requestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error verifying elevated claim: %w", err)
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: fmt.Sprintf("error verifying elevated claim: %s", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return ErrElevatedClaimRequired
|
return &oapi.AuthenticatorError{
|
||||||
|
Scheme: input.SecuritySchemeName,
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "elevated claim required",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c := ginmiddleware.GetGinContext(ctx)
|
c := oapi.GetGinContext(ctx)
|
||||||
c.Set(JWTContextKey, jwtToken)
|
c.Set(JWTContextKey, jwtToken)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package controller_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"errors"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -16,9 +15,9 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi"
|
||||||
"github.com/nhost/nhost/services/auth/go/controller"
|
"github.com/nhost/nhost/services/auth/go/controller"
|
||||||
"github.com/nhost/nhost/services/auth/go/controller/mock"
|
"github.com/nhost/nhost/services/auth/go/controller/mock"
|
||||||
ginmiddleware "github.com/oapi-codegen/gin-middleware"
|
|
||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -535,8 +534,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
|
|||||||
SecurityScheme: nil,
|
SecurityScheme: nil,
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
},
|
},
|
||||||
expected: nil,
|
expected: nil,
|
||||||
expectedErr: controller.ErrElevatedClaimRequired,
|
expectedErr: &oapi.AuthenticatorError{
|
||||||
|
Scheme: "BearerAuthElevated",
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "elevated claim required",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -559,8 +562,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
|
|||||||
SecurityScheme: nil,
|
SecurityScheme: nil,
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
},
|
},
|
||||||
expected: nil,
|
expected: nil,
|
||||||
expectedErr: controller.ErrElevatedClaimRequired,
|
expectedErr: &oapi.AuthenticatorError{
|
||||||
|
Scheme: "BearerAuthElevated",
|
||||||
|
Code: "unauthorized",
|
||||||
|
Message: "elevated claim required",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -763,13 +770,13 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
|
|||||||
//nolint
|
//nolint
|
||||||
ctx := context.WithValue(
|
ctx := context.WithValue(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
ginmiddleware.GinContextKey,
|
oapi.GinContextKey,
|
||||||
&gin.Context{},
|
&gin.Context{},
|
||||||
)
|
)
|
||||||
|
|
||||||
err = jwtGetter.MiddlewareFunc(ctx, tc.request)
|
err = jwtGetter.MiddlewareFunc(ctx, tc.request)
|
||||||
if !errors.Is(err, tc.expectedErr) {
|
if diff := cmp.Diff(err, tc.expectedErr); diff != "" {
|
||||||
t.Errorf("err = %v; want %v", err, tc.expectedErr)
|
t.Errorf("err mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, _ := jwtGetter.FromContext(ctx)
|
got, _ := jwtGetter.FromContext(ctx)
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive
|
func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive
|
||||||
ctx context.Context, req api.LinkIdTokenRequestObject,
|
ctx context.Context, req api.LinkIdTokenRequestObject,
|
||||||
) (api.LinkIdTokenResponseObject, error) {
|
) (api.LinkIdTokenResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
profile, apiErr := ctrl.wf.GetOIDCProfileFromIDToken(
|
profile, apiErr := ctrl.wf.GetOIDCProfileFromIDToken(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) RefreshProviderToken( //nolint:ireturn
|
func (ctrl *Controller) RefreshProviderToken( //nolint:ireturn
|
||||||
ctx context.Context, req api.RefreshProviderTokenRequestObject,
|
ctx context.Context, req api.RefreshProviderTokenRequestObject,
|
||||||
) (api.RefreshProviderTokenResponseObject, error) {
|
) (api.RefreshProviderTokenResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
logger = logger.With("provider", req.Provider)
|
logger = logger.With("provider", req.Provider)
|
||||||
|
|
||||||
provider := ctrl.Providers.Get(string(req.Provider))
|
provider := ctrl.Providers.Get(string(req.Provider))
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) RefreshToken( //nolint:ireturn
|
func (ctrl *Controller) RefreshToken( //nolint:ireturn
|
||||||
ctx context.Context, request api.RefreshTokenRequestObject,
|
ctx context.Context, request api.RefreshTokenRequestObject,
|
||||||
) (api.RefreshTokenResponseObject, error) {
|
) (api.RefreshTokenResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
|
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func (ctrl *Controller) SendPasswordResetEmail( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SendPasswordResetEmailRequestObject,
|
request api.SendPasswordResetEmailRequestObject,
|
||||||
) (api.SendPasswordResetEmailResponseObject, error) {
|
) (api.SendPasswordResetEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
options, err := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
options, err := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func (ctrl *Controller) SendVerificationEmail( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SendVerificationEmailRequestObject,
|
request api.SendVerificationEmailRequestObject,
|
||||||
) (api.SendVerificationEmailResponseObject, error) {
|
) (api.SendVerificationEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) postSigninAnonymousValidateRequest(
|
func (ctrl *Controller) postSigninAnonymousValidateRequest(
|
||||||
@@ -49,7 +49,7 @@ func (ctrl *Controller) postSigninAnonymousValidateRequest(
|
|||||||
func (ctrl *Controller) SignInAnonymous( //nolint:ireturn
|
func (ctrl *Controller) SignInAnonymous( //nolint:ireturn
|
||||||
ctx context.Context, req api.SignInAnonymousRequestObject,
|
ctx context.Context, req api.SignInAnonymousRequestObject,
|
||||||
) (api.SignInAnonymousResponseObject, error) {
|
) (api.SignInAnonymousResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
req, apiErr := ctrl.postSigninAnonymousValidateRequest(ctx, req, logger)
|
req, apiErr := ctrl.postSigninAnonymousValidateRequest(ctx, req, logger)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn
|
func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn
|
||||||
@@ -33,7 +33,7 @@ func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn
|
|||||||
func (ctrl *Controller) SignInEmailPassword( //nolint:ireturn
|
func (ctrl *Controller) SignInEmailPassword( //nolint:ireturn
|
||||||
ctx context.Context, request api.SignInEmailPasswordRequestObject,
|
ctx context.Context, request api.SignInEmailPasswordRequestObject,
|
||||||
) (api.SignInEmailPasswordResponseObject, error) {
|
) (api.SignInEmailPasswordResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserByEmail(ctx, string(request.Body.Email), logger)
|
user, apiErr := ctrl.wf.GetUserByEmail(ctx, string(request.Body.Email), logger)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/oidc"
|
"github.com/nhost/nhost/services/auth/go/oidc"
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
"github.com/oapi-codegen/runtime/types"
|
"github.com/oapi-codegen/runtime/types"
|
||||||
@@ -45,7 +45,7 @@ func (ctrl *Controller) postSigninIdtokenCheckUserExists(
|
|||||||
func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive
|
func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive
|
||||||
ctx context.Context, req api.SignInIdTokenRequestObject,
|
ctx context.Context, req api.SignInIdTokenRequestObject,
|
||||||
) (api.SignInIdTokenResponseObject, error) {
|
) (api.SignInIdTokenResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
profile, apiError := ctrl.wf.GetOIDCProfileFromIDToken(
|
profile, apiError := ctrl.wf.GetOIDCProfileFromIDToken(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ func (ctrl *Controller) SignInOTPEmail( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignInOTPEmailRequestObject,
|
request api.SignInOTPEmailRequestObject,
|
||||||
) (api.SignInOTPEmailResponseObject, error) {
|
) (api.SignInOTPEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
if !ctrl.config.OTPEmailEnabled {
|
if !ctrl.config.OTPEmailEnabled {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/notifications"
|
"github.com/nhost/nhost/services/auth/go/notifications"
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,7 @@ func (ctrl *Controller) SignInPasswordlessEmail( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignInPasswordlessEmailRequestObject,
|
request api.SignInPasswordlessEmailRequestObject,
|
||||||
) (api.SignInPasswordlessEmailResponseObject, error) {
|
) (api.SignInPasswordlessEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
if !ctrl.config.EmailPasswordlessEnabled {
|
if !ctrl.config.EmailPasswordlessEnabled {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ func (ctrl *Controller) SignInPasswordlessSms( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignInPasswordlessSmsRequestObject,
|
request api.SignInPasswordlessSmsRequestObject,
|
||||||
) (api.SignInPasswordlessSmsResponseObject, error) {
|
) (api.SignInPasswordlessSmsResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("phoneNumber", request.Body.PhoneNumber))
|
With(slog.String("phoneNumber", request.Body.PhoneNumber))
|
||||||
|
|
||||||
if !ctrl.config.SMSPasswordlessEnabled {
|
if !ctrl.config.SMSPasswordlessEnabled {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ func (ctrl *Controller) SignInPAT( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignInPATRequestObject,
|
request api.SignInPATRequestObject,
|
||||||
) (api.SignInPATResponseObject, error) {
|
) (api.SignInPATResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
|
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) getSigninProviderValidateRequest(
|
func (ctrl *Controller) getSigninProviderValidateRequest(
|
||||||
@@ -42,7 +42,7 @@ func (ctrl *Controller) SignInProvider( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req api.SignInProviderRequestObject,
|
req api.SignInProviderRequestObject,
|
||||||
) (api.SignInProviderResponseObject, error) {
|
) (api.SignInProviderResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("provider", string(req.Provider)))
|
With(slog.String("provider", string(req.Provider)))
|
||||||
|
|
||||||
redirectTo, apiErr := ctrl.getSigninProviderValidateRequest(ctx, req, logger)
|
redirectTo, apiErr := ctrl.getSigninProviderValidateRequest(ctx, req, logger)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/oidc"
|
"github.com/nhost/nhost/services/auth/go/oidc"
|
||||||
"github.com/nhost/nhost/services/auth/go/providers"
|
"github.com/nhost/nhost/services/auth/go/providers"
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
@@ -47,6 +47,20 @@ func (ctrl *Controller) getStateData(
|
|||||||
return stateData, nil
|
return stateData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendURLValues(u *url.URL, values map[string]string) *url.URL {
|
||||||
|
n := *u
|
||||||
|
u = &n
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
for k, v := range values {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
func (ctrl *Controller) signinProviderProviderCallbackValidate(
|
func (ctrl *Controller) signinProviderProviderCallbackValidate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req providerCallbackData,
|
req providerCallbackData,
|
||||||
@@ -56,6 +70,10 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
|
|||||||
|
|
||||||
stateData, apiErr := ctrl.getStateData(ctx, req.State, logger)
|
stateData, apiErr := ctrl.getStateData(ctx, req.State, logger)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
|
redirectTo = appendURLValues(redirectTo, map[string]string{
|
||||||
|
"provider_state": req.State,
|
||||||
|
})
|
||||||
|
|
||||||
return nil, nil, redirectTo, apiErr
|
return nil, nil, redirectTo, apiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +90,17 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Error != nil && *req.Error != "" {
|
if req.Error != nil && *req.Error != "" {
|
||||||
values := redirectTo.Query()
|
values := map[string]string{
|
||||||
values.Add("provider_error", deptr(req.Error))
|
"provider_error": deptr(req.Error),
|
||||||
values.Add("provider_error_description", deptr(req.ErrorDescription))
|
"provider_error_description": deptr(req.ErrorDescription),
|
||||||
values.Add("provider_error_url", deptr(req.ErrorURI))
|
"provider_error_url": deptr(req.ErrorURI),
|
||||||
|
|
||||||
if stateData.State != nil && *stateData.State != "" {
|
|
||||||
values.Add("state", *stateData.State)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo.RawQuery = values.Encode()
|
if stateData.State != nil && *stateData.State != "" {
|
||||||
|
values["state"] = *stateData.State
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectTo = appendURLValues(redirectTo, values)
|
||||||
|
|
||||||
return nil, nil, redirectTo, ErrOauthProviderError
|
return nil, nil, redirectTo, ErrOauthProviderError
|
||||||
}
|
}
|
||||||
@@ -93,9 +112,9 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stateData.State != nil && *stateData.State != "" {
|
if stateData.State != nil && *stateData.State != "" {
|
||||||
values := optionsRedirectTo.Query()
|
optionsRedirectTo = appendURLValues(optionsRedirectTo, map[string]string{
|
||||||
values.Add("state", *stateData.State)
|
"state": *stateData.State,
|
||||||
optionsRedirectTo.RawQuery = values.Encode()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return stateData.Options, stateData.Connect, optionsRedirectTo, nil
|
return stateData.Options, stateData.Connect, optionsRedirectTo, nil
|
||||||
@@ -201,7 +220,7 @@ func (ctrl *Controller) signinProviderProviderCallback(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req providerCallbackData,
|
req providerCallbackData,
|
||||||
) (*url.URL, *APIError) {
|
) (*url.URL, *APIError) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
options, connnect, redirectTo, apiErr := ctrl.signinProviderProviderCallbackValidate(
|
options, connnect, redirectTo, apiErr := ctrl.signinProviderProviderCallbackValidate(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
|
|||||||
},
|
},
|
||||||
expectedResponse: controller.ErrorRedirectResponse{
|
expectedResponse: controller.ErrorRedirectResponse{
|
||||||
Headers: struct{ Location string }{
|
Headers: struct{ Location string }{
|
||||||
Location: `http://localhost:3000?error=invalid-state&errorDescription=Invalid+state`,
|
Location: `^http://localhost:3000\?error=invalid-state&errorDescription=Invalid\+state&provider_state=wrong-state$`, //nolint:lll
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedJWT: nil,
|
expectedJWT: nil,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ func (ctrl *Controller) SignInWebauthn( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignInWebauthnRequestObject,
|
request api.SignInWebauthnRequestObject,
|
||||||
) (api.SignInWebauthnResponseObject, error) {
|
) (api.SignInWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) SignOut( //nolint:ireturn
|
func (ctrl *Controller) SignOut( //nolint:ireturn
|
||||||
ctx context.Context, request api.SignOutRequestObject,
|
ctx context.Context, request api.SignOutRequestObject,
|
||||||
) (api.SignOutResponseObject, error) {
|
) (api.SignOutResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if deptr(request.Body.All) {
|
if deptr(request.Body.All) {
|
||||||
userID, apiErr := ctrl.wf.GetJWTInContext(ctx, logger)
|
userID, apiErr := ctrl.wf.GetJWTInContext(ctx, logger)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ func (ctrl *Controller) SignUpEmailPassword( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req api.SignUpEmailPasswordRequestObject,
|
req api.SignUpEmailPasswordRequestObject,
|
||||||
) (api.SignUpEmailPasswordResponseObject, error) {
|
) (api.SignUpEmailPasswordResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email)))
|
logger := oapimw.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email)))
|
||||||
|
|
||||||
req, apiError := ctrl.postSignupEmailPasswordValidateRequest(ctx, req, logger)
|
req, apiError := ctrl.postSignupEmailPasswordValidateRequest(ctx, req, logger)
|
||||||
if apiError != nil {
|
if apiError != nil {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) postSignupWebauthnValidateRequest(
|
func (ctrl *Controller) postSignupWebauthnValidateRequest(
|
||||||
@@ -42,7 +42,7 @@ func (ctrl *Controller) SignUpWebauthn( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.SignUpWebauthnRequestObject,
|
request api.SignUpWebauthnRequestObject,
|
||||||
) (api.SignUpWebauthnResponseObject, error) {
|
) (api.SignUpWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
options, apiErr := ctrl.postSignupWebauthnValidateRequest(ctx, request, logger)
|
options, apiErr := ctrl.postSignupWebauthnValidateRequest(ctx, request, logger)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ func (ctrl *Controller) VerifyAddSecurityKey( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifyAddSecurityKeyRequestObject,
|
request api.VerifyAddSecurityKeyRequestObject,
|
||||||
) (api.VerifyAddSecurityKeyResponseObject, error) {
|
) (api.VerifyAddSecurityKeyResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ func (ctrl *Controller) postUserMfaActivate( //nolint:ireturn
|
|||||||
func (ctrl *Controller) VerifyChangeUserMfa( //nolint:ireturn
|
func (ctrl *Controller) VerifyChangeUserMfa( //nolint:ireturn
|
||||||
ctx context.Context, req api.VerifyChangeUserMfaRequestObject,
|
ctx context.Context, req api.VerifyChangeUserMfaRequestObject,
|
||||||
) (api.VerifyChangeUserMfaResponseObject, error) {
|
) (api.VerifyChangeUserMfaResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.MfaEnabled {
|
if !ctrl.config.MfaEnabled {
|
||||||
logger.WarnContext(ctx, "mfa disabled")
|
logger.WarnContext(ctx, "mfa disabled")
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func (ctrl *Controller) VerifyElevateWebauthn( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifyElevateWebauthnRequestObject,
|
request api.VerifyElevateWebauthnRequestObject,
|
||||||
) (api.VerifyElevateWebauthnResponseObject, error) {
|
) (api.VerifyElevateWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) VerifySignInMfaTotp( //nolint:ireturn
|
func (ctrl *Controller) VerifySignInMfaTotp( //nolint:ireturn
|
||||||
ctx context.Context, req api.VerifySignInMfaTotpRequestObject,
|
ctx context.Context, req api.VerifySignInMfaTotpRequestObject,
|
||||||
) (api.VerifySignInMfaTotpResponseObject, error) {
|
) (api.VerifySignInMfaTotpResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.MfaEnabled {
|
if !ctrl.config.MfaEnabled {
|
||||||
logger.WarnContext(ctx, "mfa disabled")
|
logger.WarnContext(ctx, "mfa disabled")
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) VerifySignInOTPEmail( //nolint:ireturn
|
func (ctrl *Controller) VerifySignInOTPEmail( //nolint:ireturn
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifySignInOTPEmailRequestObject,
|
request api.VerifySignInOTPEmailRequestObject,
|
||||||
) (api.VerifySignInOTPEmailResponseObject, error) {
|
) (api.VerifySignInOTPEmailResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("email", string(request.Body.Email)))
|
With(slog.String("email", string(request.Body.Email)))
|
||||||
|
|
||||||
user, apiErr := ctrl.wf.GetUserByEmailAndTicket(
|
user, apiErr := ctrl.wf.GetUserByEmailAndTicket(
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) VerifySignInPasswordlessSms( //nolint:ireturn
|
func (ctrl *Controller) VerifySignInPasswordlessSms( //nolint:ireturn
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifySignInPasswordlessSmsRequestObject,
|
request api.VerifySignInPasswordlessSmsRequestObject,
|
||||||
) (api.VerifySignInPasswordlessSmsResponseObject, error) {
|
) (api.VerifySignInPasswordlessSmsResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx).
|
logger := oapimw.LoggerFromContext(ctx).
|
||||||
With(slog.String("phoneNumber", request.Body.PhoneNumber))
|
With(slog.String("phoneNumber", request.Body.PhoneNumber))
|
||||||
|
|
||||||
if !ctrl.config.SMSPasswordlessEnabled {
|
if !ctrl.config.SMSPasswordlessEnabled {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) VerifySignInWebauthnUserHandle(
|
func (ctrl *Controller) VerifySignInWebauthnUserHandle(
|
||||||
@@ -70,7 +70,7 @@ func (ctrl *Controller) VerifySignInWebauthn( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifySignInWebauthnRequestObject,
|
request api.VerifySignInWebauthnRequestObject,
|
||||||
) (api.VerifySignInWebauthnResponseObject, error) {
|
) (api.VerifySignInWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if !ctrl.config.WebauthnEnabled {
|
if !ctrl.config.WebauthnEnabled {
|
||||||
logger.ErrorContext(ctx, "webauthn is disabled")
|
logger.ErrorContext(ctx, "webauthn is disabled")
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func (ctrl *Controller) VerifySignUpWebauthn( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request api.VerifySignUpWebauthnRequestObject,
|
request api.VerifySignUpWebauthnRequestObject,
|
||||||
) (api.VerifySignUpWebauthnResponseObject, error) {
|
) (api.VerifySignUpWebauthnResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
credData, options, nickname, apiErr := ctrl.postSignupWebauthnVerifyValidateRequest(
|
credData, options, nickname, apiErr := ctrl.postSignupWebauthnVerifyValidateRequest(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
"github.com/nhost/nhost/services/auth/go/sql"
|
"github.com/nhost/nhost/services/auth/go/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ func (ctrl *Controller) getVerifyHandleTicketType(
|
|||||||
func (ctrl *Controller) VerifyTicket( //nolint:ireturn
|
func (ctrl *Controller) VerifyTicket( //nolint:ireturn
|
||||||
ctx context.Context, req api.VerifyTicketRequestObject,
|
ctx context.Context, req api.VerifyTicketRequestObject,
|
||||||
) (api.VerifyTicketResponseObject, error) {
|
) (api.VerifyTicketResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
user, ticketType, redirectTo, apiErr := ctrl.getVerifyValidateRequest(ctx, req, logger)
|
user, ticketType, redirectTo, apiErr := ctrl.getVerifyValidateRequest(ctx, req, logger)
|
||||||
switch {
|
switch {
|
||||||
@@ -80,7 +80,7 @@ func (ctrl *Controller) VerifyTicket( //nolint:ireturn
|
|||||||
return ctrl.sendError(ErrInternalServerError), nil
|
return ctrl.sendError(ErrInternalServerError), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo = generateRedirectURL(redirectTo, map[string]string{
|
redirectTo = appendURLValues(redirectTo, map[string]string{
|
||||||
"refreshToken": session.RefreshToken,
|
"refreshToken": session.RefreshToken,
|
||||||
"type": string(ticketType),
|
"type": string(ticketType),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/auth/go/api"
|
"github.com/nhost/nhost/services/auth/go/api"
|
||||||
"github.com/nhost/nhost/services/auth/go/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) VerifyToken( //nolint:ireturn
|
func (ctrl *Controller) VerifyToken( //nolint:ireturn
|
||||||
ctx context.Context, request api.VerifyTokenRequestObject,
|
ctx context.Context, request api.VerifyTokenRequestObject,
|
||||||
) (api.VerifyTokenResponseObject, error) {
|
) (api.VerifyTokenResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
if request.Body != nil && request.Body.Token != nil {
|
if request.Body != nil && request.Body.Token != nil {
|
||||||
if apiErr := ctrl.wf.VerifyJWTToken(ctx, *request.Body.Token, logger); apiErr != nil {
|
if apiErr := ctrl.wf.VerifyJWTToken(ctx, *request.Body.Token, logger); apiErr != nil {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ let
|
|||||||
./vacuum.yaml
|
./vacuum.yaml
|
||||||
./vacuum-ignore.yaml
|
./vacuum-ignore.yaml
|
||||||
|
|
||||||
|
(inDirectory ../../internal/lib/oapi)
|
||||||
|
|
||||||
./go/api/server.cfg.yaml
|
./go/api/server.cfg.yaml
|
||||||
./go/api/types.cfg.yaml
|
./go/api/types.cfg.yaml
|
||||||
./go/sql/schema.sh
|
./go/sql/schema.sh
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe('personal access token', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/pat')
|
.post('/pat')
|
||||||
.send({ expiresAt: new Date() })
|
.send({ expiresAt: new Date() })
|
||||||
.expect(StatusCodes.BAD_REQUEST);
|
.expect(StatusCodes.UNAUTHORIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to add metadata to a personal access token', async () => {
|
test('should be able to add metadata to a personal access token', async () => {
|
||||||
@@ -65,7 +65,6 @@ describe('personal access token', () => {
|
|||||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||||
metadata: { name: 'Test PAT' },
|
metadata: { name: 'Test PAT' },
|
||||||
})
|
})
|
||||||
.expect(StatusCodes.OK);
|
|
||||||
|
|
||||||
const { rows } = await client.query(
|
const { rows } = await client.query(
|
||||||
'SELECT * FROM auth.refresh_tokens WHERE refresh_token_hash=$1;',
|
'SELECT * FROM auth.refresh_tokens WHERE refresh_token_hash=$1;',
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe('user email', () => {
|
|||||||
.post('/user/email/change')
|
.post('/user/email/change')
|
||||||
// .set('Authorization', `Bearer ${accessToken}`)
|
// .set('Authorization', `Bearer ${accessToken}`)
|
||||||
.send({ newEmail })
|
.send({ newEmail })
|
||||||
.expect(StatusCodes.BAD_REQUEST);
|
.expect(StatusCodes.UNAUTHORIZED);
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.post('/user/email/change')
|
.post('/user/email/change')
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe('user password', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not get user data if not signed in', async () => {
|
it('should not get user data if not signed in', async () => {
|
||||||
await request.get('/user').expect(StatusCodes.BAD_REQUEST);
|
await request.get('/user').expect(StatusCodes.UNAUTHORIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get user data if signed in', async () => {
|
it('should get user data if signed in', async () => {
|
||||||
|
|||||||
@@ -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
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
|
||||||
"github.com/getkin/kin-openapi/openapi3filter"
|
|
||||||
"github.com/gin-contrib/cors"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/nhost/nhost/internal/lib/oapi"
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/storage/api"
|
"github.com/nhost/nhost/services/storage/api"
|
||||||
"github.com/nhost/nhost/services/storage/controller"
|
"github.com/nhost/nhost/services/storage/controller"
|
||||||
"github.com/nhost/nhost/services/storage/image"
|
"github.com/nhost/nhost/services/storage/image"
|
||||||
@@ -23,7 +22,6 @@ import (
|
|||||||
"github.com/nhost/nhost/services/storage/middleware/cdn/fastly"
|
"github.com/nhost/nhost/services/storage/middleware/cdn/fastly"
|
||||||
"github.com/nhost/nhost/services/storage/migrations"
|
"github.com/nhost/nhost/services/storage/migrations"
|
||||||
"github.com/nhost/nhost/services/storage/storage"
|
"github.com/nhost/nhost/services/storage/storage"
|
||||||
ginmiddleware "github.com/oapi-codegen/gin-middleware"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,88 +51,39 @@ const (
|
|||||||
flagHasuraDBName = "hasura-db-name"
|
flagHasuraDBName = "hasura-db-name"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getCorsMiddleware(
|
func getCORSOptions(cmd *cli.Command) oapimw.CORSOptions {
|
||||||
corsAllowOrigins []string,
|
return oapimw.CORSOptions{
|
||||||
corsAllowCredentials bool,
|
AllowedOrigins: cmd.StringSlice(flagCorsAllowOrigins),
|
||||||
) gin.HandlerFunc {
|
AllowedMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"},
|
||||||
return cors.New(cors.Config{ //nolint:exhaustruct
|
AllowedHeaders: []string{
|
||||||
AllowOrigins: corsAllowOrigins,
|
|
||||||
AllowMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"},
|
|
||||||
AllowHeaders: []string{
|
|
||||||
"Authorization", "Origin", "if-match", "if-none-match", "if-modified-since", "if-unmodified-since",
|
"Authorization", "Origin", "if-match", "if-none-match", "if-modified-since", "if-unmodified-since",
|
||||||
"x-hasura-admin-secret", "x-nhost-bucket-id", "x-nhost-file-name", "x-nhost-file-id",
|
"x-hasura-admin-secret", "x-nhost-bucket-id", "x-nhost-file-name", "x-nhost-file-id",
|
||||||
"x-hasura-role",
|
"x-hasura-role",
|
||||||
},
|
},
|
||||||
ExposeHeaders: []string{
|
ExposedHeaders: []string{
|
||||||
"Content-Length", "Content-Type", "Cache-Control", "ETag", "Last-Modified", "X-Error",
|
"Content-Length", "Content-Type", "Cache-Control", "ETag", "Last-Modified", "X-Error",
|
||||||
},
|
},
|
||||||
AllowCredentials: corsAllowCredentials,
|
AllowCredentials: cmd.Bool(flagCorsAllowCredentials),
|
||||||
MaxAge: 12 * time.Hour, //nolint: mnd
|
MaxAge: "86400",
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGin( //nolint:funlen
|
func getServer(
|
||||||
bind string,
|
cmd *cli.Command,
|
||||||
publicURL string,
|
|
||||||
apiRootPrefix string,
|
|
||||||
hasuraAdminSecret string,
|
|
||||||
metadataStorage controller.MetadataStorage,
|
metadataStorage controller.MetadataStorage,
|
||||||
contentStorage controller.ContentStorage,
|
contentStorage controller.ContentStorage,
|
||||||
imageTransformer *image.Transformer,
|
imageTransformer *image.Transformer,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
debug bool,
|
|
||||||
corsAllowOrigins []string,
|
|
||||||
corsAllowCredentials bool,
|
|
||||||
fastlyService string,
|
|
||||||
fastlyKey string,
|
|
||||||
clamavServer string,
|
|
||||||
) (*http.Server, error) {
|
) (*http.Server, error) {
|
||||||
router := gin.New()
|
av, err := getAv(cmd.String(flagClamavServer))
|
||||||
|
|
||||||
router.GET("/healthz", func(c *gin.Context) {
|
|
||||||
c.String(http.StatusOK, "ok")
|
|
||||||
})
|
|
||||||
|
|
||||||
if !debug {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
loader := openapi3.NewLoader()
|
|
||||||
|
|
||||||
doc, err := loader.LoadFromData(controller.OpenAPISchema)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
|
|
||||||
URL: apiRootPrefix,
|
|
||||||
})
|
|
||||||
|
|
||||||
handlers := []gin.HandlerFunc{
|
|
||||||
middleware.Logger(logger),
|
|
||||||
getCorsMiddleware(corsAllowOrigins, corsAllowCredentials),
|
|
||||||
gin.Recovery(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if fastlyService != "" {
|
|
||||||
logger.InfoContext(context.Background(), "enabling fastly middleware")
|
|
||||||
handlers = append(
|
|
||||||
handlers,
|
|
||||||
fastly.New(fastlyService, fastlyKey, logger),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Use(handlers...)
|
|
||||||
|
|
||||||
av, err := getAv(clamavServer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("problem trying to get av: %w", err)
|
return nil, fmt.Errorf("problem trying to get av: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrl := controller.New(
|
ctrl := controller.New(
|
||||||
publicURL,
|
cmd.String(flagPublicURL),
|
||||||
apiRootPrefix,
|
cmd.String(flagAPIRootPrefix),
|
||||||
hasuraAdminSecret,
|
cmd.String(flagHasuraAdminSecret),
|
||||||
metadataStorage,
|
metadataStorage,
|
||||||
contentStorage,
|
contentStorage,
|
||||||
imageTransformer,
|
imageTransformer,
|
||||||
@@ -143,27 +92,41 @@ func getGin( //nolint:funlen
|
|||||||
)
|
)
|
||||||
|
|
||||||
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
|
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
|
||||||
mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions(
|
|
||||||
doc,
|
router, mw, err := oapi.NewRouter(
|
||||||
&ginmiddleware.Options{ //nolint:exhaustruct
|
controller.OpenAPISchema,
|
||||||
Options: openapi3filter.Options{ //nolint:exhaustruct
|
cmd.String(flagAPIRootPrefix),
|
||||||
AuthenticationFunc: middleware.AuthenticationFunc(hasuraAdminSecret),
|
middleware.AuthenticationFunc(cmd.String(flagHasuraAdminSecret)),
|
||||||
},
|
getCORSOptions(cmd),
|
||||||
SilenceServersWarning: true,
|
logger,
|
||||||
},
|
)
|
||||||
))
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.GET("/healthz", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "ok")
|
||||||
|
})
|
||||||
|
|
||||||
|
if cmd.String(flagFastlyService) != "" {
|
||||||
|
logger.InfoContext(context.Background(), "enabling fastly middleware")
|
||||||
|
router.Use(
|
||||||
|
fastly.New(cmd.String(flagFastlyService), cmd.String(flagFastlyKey), logger),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
api.RegisterHandlersWithOptions(
|
api.RegisterHandlersWithOptions(
|
||||||
router,
|
router,
|
||||||
handler,
|
handler,
|
||||||
api.GinServerOptions{
|
api.GinServerOptions{
|
||||||
BaseURL: apiRootPrefix,
|
BaseURL: cmd.String(flagAPIRootPrefix),
|
||||||
Middlewares: []api.MiddlewareFunc{mw},
|
Middlewares: []api.MiddlewareFunc{mw},
|
||||||
ErrorHandler: nil,
|
ErrorHandler: nil,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
server := &http.Server{ //nolint:exhaustruct
|
server := &http.Server{ //nolint:exhaustruct
|
||||||
Addr: bind,
|
Addr: cmd.String(flagBind),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
ReadHeaderTimeout: 5 * time.Second, //nolint:mnd
|
ReadHeaderTimeout: 5 * time.Second, //nolint:mnd
|
||||||
}
|
}
|
||||||
@@ -453,21 +416,12 @@ func serve(ctx context.Context, cmd *cli.Command) error { //nolint:funlen
|
|||||||
cmd.String(flagHasuraEndpoint) + "/graphql",
|
cmd.String(flagHasuraEndpoint) + "/graphql",
|
||||||
)
|
)
|
||||||
|
|
||||||
server, err := getGin( //nolint: contextcheck
|
server, err := getServer( //nolint: contextcheck
|
||||||
cmd.String(flagBind),
|
cmd,
|
||||||
cmd.String(flagPublicURL),
|
|
||||||
cmd.String(flagAPIRootPrefix),
|
|
||||||
cmd.String(flagHasuraAdminSecret),
|
|
||||||
metadataStorage,
|
metadataStorage,
|
||||||
contentStorage,
|
contentStorage,
|
||||||
imageTransformer,
|
imageTransformer,
|
||||||
logger,
|
logger,
|
||||||
cmd.Bool(flagDebug),
|
|
||||||
cmd.StringSlice(flagCorsAllowOrigins),
|
|
||||||
cmd.Bool(flagCorsAllowCredentials),
|
|
||||||
cmd.String(flagFastlyService),
|
|
||||||
cmd.String(flagFastlyKey),
|
|
||||||
cmd.String(flagClamavServer),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
|
||||||
"github.com/nhost/nhost/services/storage/api"
|
"github.com/nhost/nhost/services/storage/api"
|
||||||
"github.com/nhost/nhost/services/storage/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctrl *Controller) deleteBrokenMetadata(
|
func (ctrl *Controller) deleteBrokenMetadata(
|
||||||
@@ -29,7 +29,7 @@ func (ctrl *Controller) DeleteBrokenMetadata( //nolint:ireturn
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
_ api.DeleteBrokenMetadataRequestObject,
|
_ api.DeleteBrokenMetadataRequestObject,
|
||||||
) (api.DeleteBrokenMetadataResponseObject, error) {
|
) (api.DeleteBrokenMetadataResponseObject, error) {
|
||||||
logger := middleware.LoggerFromContext(ctx)
|
logger := oapimw.LoggerFromContext(ctx)
|
||||||
|
|
||||||
files, apiErr := ctrl.deleteBrokenMetadata(ctx)
|
files, apiErr := ctrl.deleteBrokenMetadata(ctx)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user