fix: frontend bugs relating to redirects/tenants (#25)

* fix: frontend bugs

- Adds a query param `tenant` to application routes
- Allows setting an environment variable `SERVER_AUTH_SET_EMAIL_VERIFIED` to true which automatically sets the email to verified for new signups, since most local installations won't have an email verification mechanism
- When a user is logged in, navigating to `/auth/login` or `/auth/register` will redirect to the application via the no-auth.tsx middleware
- When there are no events found, the backend will no longer respond with a `500`-level error, and will return 0 rows instead
This commit is contained in:
abelanger5
2023-12-26 09:11:53 -05:00
committed by GitHub
parent e857004b79
commit ce61eade4f
20 changed files with 285 additions and 110 deletions

View File

@@ -36,6 +36,7 @@ SERVER_URL=https://app.dev.hatchet-tools.com
SERVER_AUTH_COOKIE_SECRETS="$(randstring 16) $(randstring 16)"
SERVER_AUTH_COOKIE_DOMAIN=app.dev.hatchet-tools.com
SERVER_AUTH_COOKIE_INSECURE=false
SERVER_AUTH_SET_EMAIL_VERIFIED=true
EOF
```

View File

@@ -43,7 +43,7 @@ func (u *UserService) UserCreate(ctx echo.Context, request gen.UserCreateRequest
createOpts := &repository.CreateUserOpts{
Email: string(request.Body.Email),
EmailVerified: repository.BoolPtr(false),
EmailVerified: repository.BoolPtr(u.config.Auth.SetEmailVerified),
Name: repository.StringPtr(request.Body.Name),
Password: *hashedPw,
}

View File

@@ -11,6 +11,10 @@ export const queries = createQueryKeyStore({
queryKey: ['user:get'],
queryFn: async () => (await api.userGetCurrent()).data,
},
listTenantMemberships: {
queryKey: ['tenant-memberships:list'],
queryFn: async () => (await api.tenantMembershipsList()).data,
},
},
workflows: {
list: (tenant: string) => ({

View File

@@ -1,5 +1,8 @@
import { atom } from 'jotai';
import { Tenant } from './api';
import { atom, useAtom } from 'jotai';
import { Tenant, queries } from './api';
import { useSearchParams } from 'react-router-dom';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
const getInitialValue = <T>(key: string): T | undefined => {
const item = localStorage.getItem(key);
@@ -11,14 +14,110 @@ const getInitialValue = <T>(key: string): T | undefined => {
return;
};
const currTenantKey = 'currTenant';
const lastTenantKey = 'lastTenant';
const currTenantAtomInit = atom(getInitialValue<Tenant>(currTenantKey));
const lastTenantAtomInit = atom(getInitialValue<Tenant>(lastTenantKey));
export const currTenantAtom = atom(
(get) => get(currTenantAtomInit),
export const lastTenantAtom = atom(
(get) => get(lastTenantAtomInit),
(_get, set, newVal: Tenant) => {
set(currTenantAtomInit, newVal);
localStorage.setItem(currTenantKey, JSON.stringify(newVal));
set(lastTenantAtomInit, newVal);
localStorage.setItem(lastTenantKey, JSON.stringify(newVal));
},
);
// search param sets the tenant, the last tenant set is used if the search param is empty,
// otherwise the first membership is used
export function useTenantContext(): [
Tenant | undefined,
(tenant: Tenant) => void,
] {
const [lastTenant, setLastTenant] = useAtom(lastTenantAtom);
const [searchParams, setSearchParams] = useSearchParams();
const [currTenant, setCurrTenant] = useState<Tenant>();
const listMembershipsQuery = useQuery({
...queries.user.listTenantMemberships,
});
const memberships = useMemo(() => {
return listMembershipsQuery.data?.rows || [];
}, [listMembershipsQuery]);
const computedCurrTenant = useMemo(() => {
const findTenant = (tenantId: string) => {
return memberships?.find((m) => m.tenant?.metadata.id === tenantId)
?.tenant;
};
const currTenantId = searchParams.get('tenant') || undefined;
if (currTenantId) {
const tenant = findTenant(currTenantId);
if (tenant) {
return tenant;
}
}
const lastTenantId = lastTenant?.metadata.id || undefined;
if (lastTenantId) {
const tenant = findTenant(lastTenantId);
if (tenant) {
return tenant;
}
}
const firstMembershipTenant = memberships?.[0]?.tenant;
return firstMembershipTenant;
}, [memberships, lastTenant?.metadata.id, searchParams]);
// sets the current tenant if the search param changes
useEffect(() => {
if (searchParams.get('tenant') !== currTenant?.metadata.id) {
const newTenant = memberships?.find(
(m) => m.tenant?.metadata.id === searchParams.get('tenant'),
)?.tenant;
if (newTenant) {
setCurrTenant(newTenant);
} else if (computedCurrTenant?.metadata.id) {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('tenant', computedCurrTenant?.metadata.id);
setSearchParams(newSearchParams);
}
}
}, [
searchParams,
currTenant,
setCurrTenant,
memberships,
computedCurrTenant,
setSearchParams,
]);
// sets the current tenant to the initial tenant
useEffect(() => {
if (!currTenant && computedCurrTenant) {
setCurrTenant(computedCurrTenant);
}
}, [computedCurrTenant, currTenant, setCurrTenant]);
// keeps the current tenant in sync with the last tenant
useEffect(() => {
if (currTenant && lastTenant?.metadata.id !== currTenant?.metadata.id) {
setLastTenant(currTenant);
}
}, [lastTenant, currTenant, setLastTenant]);
const setTenant = (tenant: Tenant) => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('tenant', tenant.metadata.id);
setSearchParams(newSearchParams);
};
return [currTenant || computedCurrTenant, setTenant];
}

View File

@@ -0,0 +1,38 @@
import { redirect } from 'react-router-dom';
import api from '@/lib/api';
import queryClient from '@/query-client';
import { AxiosError, isAxiosError } from 'axios';
const noAuthMiddleware = async () => {
try {
const user = await queryClient.fetchQuery({
queryKey: ['user:get:current'],
queryFn: async () => {
const res = await api.userGetCurrent();
return res.data;
},
});
if (user) {
throw redirect('/');
}
} catch (error) {
if (error instanceof Response) {
throw error;
} else if (isAxiosError(error)) {
const axiosErr = error as AxiosError;
if (axiosErr.response?.status === 403) {
return;
} else {
throw error;
}
}
}
};
export async function loader() {
await noAuthMiddleware();
return null;
}

View File

@@ -4,10 +4,11 @@ import {
redirect,
useLoaderData,
} from 'react-router-dom';
import api from '@/lib/api';
import api, { queries } from '@/lib/api';
import queryClient from '@/query-client';
import { useContextFromParent } from '@/lib/outlet';
import { Loading } from '@/components/ui/loading.tsx';
import { useQuery } from '@tanstack/react-query';
const authMiddleware = async (currentUrl: string) => {
try {
@@ -37,12 +38,12 @@ const authMiddleware = async (currentUrl: string) => {
}
};
const membershipsPopulator = async () => {
const membershipsPopulator = async (currentUrl: string) => {
const res = await api.tenantMembershipsList();
const memberships = res.data;
if (memberships.rows?.length === 0) {
if (memberships.rows?.length === 0 && !currentUrl.includes('/onboarding')) {
throw redirect('/onboarding/create-tenant');
}
@@ -51,7 +52,7 @@ const membershipsPopulator = async () => {
export async function loader({ request }: LoaderFunctionArgs) {
const user = await authMiddleware(request.url);
const memberships = await membershipsPopulator();
const memberships = await membershipsPopulator(request.url);
return {
user,
memberships,
@@ -59,13 +60,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
export default function Auth() {
const listMembershipsQuery = useQuery({
...queries.user.listTenantMemberships,
});
const { user, memberships } = useLoaderData() as Awaited<
ReturnType<typeof loader>
>;
const ctx = useContextFromParent({
user,
memberships,
memberships: listMembershipsQuery.data?.rows || memberships,
});
if (!user || !memberships) {

View File

@@ -12,11 +12,11 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { useMemo, useState } from 'react';
import { currTenantAtom } from '@/lib/atoms';
import { useQuery } from '@tanstack/react-query';
import { useAtom } from 'jotai';
import invariant from 'tiny-invariant';
import { DataTable } from '@/components/molecules/data-table/data-table';
import { TenantContextType } from '@/lib/outlet';
import { useOutletContext } from 'react-router-dom';
export const columns = ({
onRowClick,
@@ -100,7 +100,7 @@ export const columns = ({
// eslint-disable-next-line react-refresh/only-export-components
function WorkflowRunSummary({ event }: { event: Event }) {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const [hoverCardOpen, setPopoverOpen] = useState<

View File

@@ -18,8 +18,6 @@ import api, {
queries,
} from '@/lib/api';
import invariant from 'tiny-invariant';
import { useAtom } from 'jotai';
import { currTenantAtom } from '@/lib/atoms';
import { FilterOption } from '@/components/molecules/data-table/data-table-toolbar';
import {
Dialog,
@@ -30,7 +28,7 @@ import {
} from '@/components/ui/dialog';
import { relativeDate } from '@/lib/utils';
import { Code } from '@/components/ui/code';
import { useSearchParams } from 'react-router-dom';
import { useOutletContext, useSearchParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import {
ArrowPathIcon,
@@ -38,6 +36,7 @@ import {
} from '@heroicons/react/24/outline';
import { useApiError } from '@/lib/hooks';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
export default function Events() {
return (
@@ -55,7 +54,7 @@ export default function Events() {
function EventsTable() {
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
const [searchParams, setSearchParams] = useSearchParams();
const [rotate, setRotate] = useState(false);
const { handleApiError } = useApiError({});
@@ -65,16 +64,20 @@ function EventsTable() {
useEffect(() => {
if (
selectedEvent &&
(!searchParams.get('eventId') ||
searchParams.get('eventId') !== selectedEvent.metadata.id)
(!searchParams.get('event') ||
searchParams.get('event') !== selectedEvent.metadata.id)
) {
setSearchParams({ eventId: selectedEvent.metadata.id });
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('event', selectedEvent.metadata.id);
setSearchParams(newSearchParams);
} else if (
!selectedEvent &&
searchParams.get('eventId') &&
searchParams.get('eventId') !== ''
searchParams.get('event') &&
searchParams.get('event') !== ''
) {
setSearchParams({});
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete('event');
setSearchParams(newSearchParams);
}
}, [selectedEvent, searchParams, setSearchParams]);
@@ -296,7 +299,7 @@ function EventDataSection({ event }: { event: Event }) {
}
function EventWorkflowRunsList({ event }: { event: Event }) {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const listWorkflowRunsQuery = useQuery({

View File

@@ -31,7 +31,7 @@ import {
} from '@/components/ui/command';
import { Link, Outlet, useNavigate, useOutletContext } from 'react-router-dom';
import api, { TenantMember, User } from '@/lib/api';
import api, { Tenant, TenantMember, User } from '@/lib/api';
import { useApiError } from '@/lib/hooks';
import { useMutation } from '@tanstack/react-query';
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons';
@@ -40,45 +40,38 @@ import {
Popover,
PopoverContent,
} from '@radix-ui/react-popover';
import React, { useEffect } from 'react';
import React from 'react';
import {
MembershipsContextType,
UserContextType,
useContextFromParent,
} from '@/lib/outlet';
import { useAtom } from 'jotai';
import { currTenantAtom } from '@/lib/atoms';
import { useTenantContext } from '@/lib/atoms';
import { Loading, Spinner } from '@/components/ui/loading.tsx';
function Main() {
const { user, memberships } = useOutletContext<
UserContextType & MembershipsContextType
>();
const [tenant, setTenant] = useAtom(currTenantAtom);
const ctx = useOutletContext<UserContextType & MembershipsContextType>();
useEffect(() => {
if (!tenant && memberships && memberships.length > 0) {
const tenant = memberships[0].tenant;
invariant(tenant);
setTenant(tenant);
}
}, [tenant, memberships, setTenant]);
const { user, memberships } = ctx;
const ctx = useContextFromParent({
const [currTenant] = useTenantContext();
const childCtx = useContextFromParent({
user,
memberships,
tenant: currTenant,
});
if (!user || !memberships) {
if (!user || !memberships || !currTenant) {
return <Loading />;
}
return (
<div className="flex flex-row flex-1 w-full h-full">
<MainNav user={user} />
<Sidebar memberships={memberships} />
<Sidebar memberships={memberships} currTenant={currTenant} />
<div className="pt-12 flex-grow">
<Outlet context={ctx} />
<Outlet context={childCtx} />
</div>
</div>
);
@@ -88,9 +81,10 @@ export default Main;
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
memberships: TenantMember[];
currTenant: Tenant;
}
function Sidebar({ className, memberships }: SidebarProps) {
function Sidebar({ className, memberships, currTenant }: SidebarProps) {
return (
<div className={cn('h-full border-r max-w-xs', className)}>
<div className="flex flex-col justify-between items-start space-y-4 px-4 py-4 h-full">
@@ -139,7 +133,7 @@ function Sidebar({ className, memberships }: SidebarProps) {
</div>
</div>
</div>
<TenantSwitcher memberships={memberships} />
<TenantSwitcher memberships={memberships} currTenant={currTenant} />
</div>
</div>
);
@@ -189,22 +183,6 @@ function MainNav({ user }: MainNavProps) {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* <DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator /> */}
<DropdownMenuItem onClick={() => logoutMutation.mutate()}>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
@@ -220,10 +198,15 @@ function MainNav({ user }: MainNavProps) {
interface TenantSwitcherProps {
className?: string;
memberships: TenantMember[];
currTenant: Tenant;
}
function TenantSwitcher({ className, memberships }: TenantSwitcherProps) {
const [currTenant, setTenant] = useAtom(currTenantAtom);
function TenantSwitcher({
className,
memberships,
currTenant,
}: TenantSwitcherProps) {
const setCurrTenant = useTenantContext()[1];
const [open, setOpen] = React.useState(false);
if (!currTenant) {
@@ -254,7 +237,7 @@ function TenantSwitcher({ className, memberships }: TenantSwitcherProps) {
key={membership.metadata.id}
onSelect={() => {
invariant(membership.tenant);
setTenant(membership.tenant);
setCurrTenant(membership.tenant);
setOpen(false);
}}
value={membership.tenant?.slug}

View File

@@ -1,9 +1,7 @@
import { Separator } from '@/components/ui/separator';
import { queries } from '@/lib/api';
import { currTenantAtom } from '@/lib/atoms';
import { useQuery } from '@tanstack/react-query';
import { useAtom } from 'jotai';
import { useParams } from 'react-router-dom';
import { useOutletContext, useParams } from 'react-router-dom';
import invariant from 'tiny-invariant';
import { relativeDate } from '@/lib/utils';
import { ServerStackIcon } from '@heroicons/react/24/outline';
@@ -11,9 +9,10 @@ import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/molecules/data-table/data-table';
import { columns } from './components/step-runs-columns';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
export default function ExpandedWorkflowRun() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const params = useParams();

View File

@@ -2,15 +2,14 @@ import { Separator } from '@/components/ui/separator';
import { useQuery } from '@tanstack/react-query';
import { queries } from '@/lib/api';
import invariant from 'tiny-invariant';
import { useAtom } from 'jotai';
import { currTenantAtom } from '@/lib/atoms';
import { relativeDate } from '@/lib/utils';
import { Link } from 'react-router-dom';
import { Link, useOutletContext } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
export default function Workers() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const listWorkersQuery = useQuery({

View File

@@ -1,10 +1,8 @@
import { Separator } from '@/components/ui/separator';
import { JobRun, StepRun, StepRunStatus, queries, Event } from '@/lib/api';
import CronPrettifier from 'cronstrue';
import { currTenantAtom } from '@/lib/atoms';
import { useQuery } from '@tanstack/react-query';
import { useAtom } from 'jotai';
import { Link, useParams } from 'react-router-dom';
import { Link, useOutletContext, useParams } from 'react-router-dom';
import invariant from 'tiny-invariant';
import { Badge } from '@/components/ui/badge';
import { relativeDate } from '@/lib/utils';
@@ -22,11 +20,12 @@ import { ColumnDef } from '@tanstack/react-table';
import { useState } from 'react';
import { Code } from '@/components/ui/code';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
export default function ExpandedWorkflowRun() {
const [expandedStepRuns, setExpandedStepRuns] = useState<string[]>([]);
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const params = useParams();

View File

@@ -9,10 +9,10 @@ import {
} from '@tanstack/react-table';
import { useQuery } from '@tanstack/react-query';
import invariant from 'tiny-invariant';
import { useAtom } from 'jotai';
import { currTenantAtom } from '@/lib/atoms';
import { queries } from '@/lib/api';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
import { useOutletContext } from 'react-router-dom';
export default function WorkflowRuns() {
return (
@@ -29,7 +29,7 @@ export default function WorkflowRuns() {
}
function WorkflowRunsTable() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const [sorting, setSorting] = useState<SortingState>([]);

View File

@@ -1,14 +1,13 @@
import { DataTable } from '@/components/molecules/data-table/data-table';
import { Separator } from '@/components/ui/separator';
import api, { Workflow, queries } from '@/lib/api';
import { currTenantAtom } from '@/lib/atoms';
import { useQuery } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { useAtom } from 'jotai';
import {
LoaderFunctionArgs,
redirect,
useLoaderData,
useOutletContext,
useParams,
} from 'react-router-dom';
import invariant from 'tiny-invariant';
@@ -19,6 +18,7 @@ import { Badge } from '@/components/ui/badge';
import { relativeDate } from '@/lib/utils';
import { Square3Stack3DIcon } from '@heroicons/react/24/outline';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
export async function loader({
params,
@@ -97,7 +97,7 @@ export default function ExpandedWorkflow() {
}
function WorkflowDefinition() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const params = useParams();
@@ -126,7 +126,7 @@ function WorkflowDefinition() {
}
function RecentRunsList() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const params = useParams();

View File

@@ -3,12 +3,12 @@ import WorkflowList from './components/workflow-list';
import { useQuery } from '@tanstack/react-query';
import { queries } from '@/lib/api';
import invariant from 'tiny-invariant';
import { useAtom } from 'jotai';
import { currTenantAtom } from '@/lib/atoms';
import { Loading } from '@/components/ui/loading.tsx';
import { useOutletContext } from 'react-router-dom';
import { TenantContextType } from '@/lib/outlet';
export default function Workflows() {
const [tenant] = useAtom(currTenantAtom);
const { tenant } = useOutletContext<TenantContextType>();
invariant(tenant);
const listWorkflowsQuery = useQuery({

View File

@@ -1,6 +1,6 @@
import api, { CreateTenantRequest } from '@/lib/api';
import api, { CreateTenantRequest, queries } from '@/lib/api';
import { useApiError } from '@/lib/hooks';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { TenantCreateForm } from './components/tenant-create-form';
@@ -12,12 +12,17 @@ export default function CreateTenant() {
setFieldErrors: setFieldErrors,
});
const listMembershipsQuery = useQuery({
...queries.user.listTenantMemberships,
});
const createMutation = useMutation({
mutationKey: ['user:update:login'],
mutationFn: async (data: CreateTenantRequest) => {
await api.tenantCreate(data);
},
onSuccess: () => {
onSuccess: async () => {
await listMembershipsQuery.refetch();
navigate('/');
},
onError: handleApiError,

View File

@@ -1,5 +1,9 @@
import { FC } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import {
createBrowserRouter,
redirect,
RouterProvider,
} from 'react-router-dom';
const routes = [
{
@@ -12,22 +16,33 @@ const routes = [
}),
children: [
{
path: '/auth/login',
path: '/auth',
lazy: async () =>
import('./pages/auth/login').then((res) => {
import('./pages/auth/no-auth').then((res) => {
return {
Component: res.default,
};
}),
},
{
path: '/auth/register',
lazy: async () =>
import('./pages/auth/register').then((res) => {
return {
Component: res.default,
loader: res.loader,
};
}),
children: [
{
path: '/auth/login',
lazy: async () =>
import('./pages/auth/login').then((res) => {
return {
Component: res.default,
};
}),
},
{
path: '/auth/register',
lazy: async () =>
import('./pages/auth/register').then((res) => {
return {
Component: res.default,
};
}),
},
],
},
{
path: '/',
@@ -39,6 +54,16 @@ const routes = [
};
}),
children: [
{
path: '/',
lazy: async () => {
return {
loader: function () {
return redirect('/events');
},
};
},
},
{
path: '/onboarding/create-tenant',
lazy: async () =>

View File

@@ -21,7 +21,7 @@ type SeedConfigFile struct {
AdminPassword string `mapstructure:"adminPassword" json:"adminPassword,omitempty" default:"Admin123!!"`
AdminName string `mapstructure:"adminName" json:"adminName,omitempty" default:"Admin"`
DefaultTenantName string `mapstructure:"defaultTenantName" json:"defaultTenantName,omitempty" default:"default"`
DefaultTenantName string `mapstructure:"defaultTenantName" json:"defaultTenantName,omitempty" default:"Default"`
DefaultTenantSlug string `mapstructure:"defaultTenantSlug" json:"defaultTenantSlug,omitempty" default:"default"`
DefaultTenantID string `mapstructure:"defaultTenantId" json:"defaultTenantId,omitempty" default:"707d0855-80ab-4e1f-a156-f1c4546cbf52"`

View File

@@ -42,6 +42,9 @@ type ConfigFileAuth struct {
// Hatchet instance
BasicAuthEnabled bool `mapstructure:"basicAuthEnabled" json:"basicAuthEnabled,omitempty" default:"true"`
// SetEmailVerified controls whether the user's email is automatically set to verified
SetEmailVerified bool `mapstructure:"setEmailVerified" json:"setEmailVerified,omitempty" default:"false"`
// Configuration options for the cookie
Cookie ConfigFileAuthCookie `mapstructure:"cookie" json:"cookie,omitempty"`
}
@@ -111,6 +114,7 @@ func BindAllEnv(v *viper.Viper) {
// auth options
v.BindEnv("auth.restrictedEmailDomains", "SERVER_AUTH_RESTRICTED_EMAIL_DOMAINS")
v.BindEnv("auth.basicAuthEnabled", "SERVER_AUTH_BASIC_AUTH_ENABLED")
v.BindEnv("auth.setEmailVerified", "SERVER_AUTH_SET_EMAIL_VERIFIED")
v.BindEnv("auth.cookie.name", "SERVER_AUTH_COOKIE_NAME")
v.BindEnv("auth.cookie.domain", "SERVER_AUTH_COOKIE_DOMAIN")
v.BindEnv("auth.cookie.secrets", "SERVER_AUTH_COOKIE_SECRETS")

View File

@@ -2,11 +2,14 @@ package prisma
import (
"context"
"errors"
"fmt"
"github.com/hatchet-dev/hatchet/internal/repository"
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db"
"github.com/hatchet-dev/hatchet/internal/repository/prisma/dbsqlc"
"github.com/hatchet-dev/hatchet/internal/validator"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -87,19 +90,27 @@ func (r *eventRepository) ListEvents(tenantId string, opts *repository.ListEvent
events, err := r.queries.ListEvents(context.Background(), tx, queryParams)
if err != nil {
return nil, err
if errors.Is(err, pgx.ErrNoRows) {
events = make([]*dbsqlc.ListEventsRow, 0)
} else {
return nil, fmt.Errorf("could not list events: %w", err)
}
}
count, err := r.queries.CountEvents(context.Background(), tx, countParams)
if err != nil {
return nil, err
if errors.Is(err, pgx.ErrNoRows) {
count = 0
} else {
return nil, fmt.Errorf("could not count events: %w", err)
}
}
err = tx.Commit(context.Background())
if err != nil {
return nil, err
return nil, fmt.Errorf("could not commit transaction: %w", err)
}
res.Rows = events