diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cf59fd85..646a2865c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ``` diff --git a/api/v1/server/handlers/users/create.go b/api/v1/server/handlers/users/create.go index 8d6df5330..837519a13 100644 --- a/api/v1/server/handlers/users/create.go +++ b/api/v1/server/handlers/users/create.go @@ -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, } diff --git a/frontend/app/src/lib/api/queries.ts b/frontend/app/src/lib/api/queries.ts index 2a12dc5dd..39ae060e4 100644 --- a/frontend/app/src/lib/api/queries.ts +++ b/frontend/app/src/lib/api/queries.ts @@ -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) => ({ diff --git a/frontend/app/src/lib/atoms.ts b/frontend/app/src/lib/atoms.ts index 613a95061..9b7967423 100644 --- a/frontend/app/src/lib/atoms.ts +++ b/frontend/app/src/lib/atoms.ts @@ -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 = (key: string): T | undefined => { const item = localStorage.getItem(key); @@ -11,14 +14,110 @@ const getInitialValue = (key: string): T | undefined => { return; }; -const currTenantKey = 'currTenant'; +const lastTenantKey = 'lastTenant'; -const currTenantAtomInit = atom(getInitialValue(currTenantKey)); +const lastTenantAtomInit = atom(getInitialValue(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(); + + 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]; +} diff --git a/frontend/app/src/pages/auth/no-auth.tsx b/frontend/app/src/pages/auth/no-auth.tsx new file mode 100644 index 000000000..81ce0996f --- /dev/null +++ b/frontend/app/src/pages/auth/no-auth.tsx @@ -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; +} diff --git a/frontend/app/src/pages/main/auth.tsx b/frontend/app/src/pages/main/auth.tsx index 621268c9f..8137497ed 100644 --- a/frontend/app/src/pages/main/auth.tsx +++ b/frontend/app/src/pages/main/auth.tsx @@ -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 >; const ctx = useContextFromParent({ user, - memberships, + memberships: listMembershipsQuery.data?.rows || memberships, }); if (!user || !memberships) { diff --git a/frontend/app/src/pages/main/events/components/event-columns.tsx b/frontend/app/src/pages/main/events/components/event-columns.tsx index f15c09d73..67dc6085f 100644 --- a/frontend/app/src/pages/main/events/components/event-columns.tsx +++ b/frontend/app/src/pages/main/events/components/event-columns.tsx @@ -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(); invariant(tenant); const [hoverCardOpen, setPopoverOpen] = useState< diff --git a/frontend/app/src/pages/main/events/index.tsx b/frontend/app/src/pages/main/events/index.tsx index 6af5f68eb..3b8dda461 100644 --- a/frontend/app/src/pages/main/events/index.tsx +++ b/frontend/app/src/pages/main/events/index.tsx @@ -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(null); - const [tenant] = useAtom(currTenantAtom); + const { tenant } = useOutletContext(); 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(); invariant(tenant); const listWorkflowRunsQuery = useQuery({ diff --git a/frontend/app/src/pages/main/index.tsx b/frontend/app/src/pages/main/index.tsx index 8c1cdb0b8..55a4bbdd1 100644 --- a/frontend/app/src/pages/main/index.tsx +++ b/frontend/app/src/pages/main/index.tsx @@ -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(); - 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 ; } return (
- +
- +
); @@ -88,9 +81,10 @@ export default Main; interface SidebarProps extends React.HTMLAttributes { memberships: TenantMember[]; + currTenant: Tenant; } -function Sidebar({ className, memberships }: SidebarProps) { +function Sidebar({ className, memberships, currTenant }: SidebarProps) { return (
@@ -139,7 +133,7 @@ function Sidebar({ className, memberships }: SidebarProps) {
- + ); @@ -189,22 +183,6 @@ function MainNav({ user }: MainNavProps) { - {/* - - Profile - ⇧⌘P - - - Billing - ⌘B - - - Settings - ⌘S - - New Team - - */} logoutMutation.mutate()}> Log out ⇧⌘Q @@ -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} diff --git a/frontend/app/src/pages/main/workers/$worker/index.tsx b/frontend/app/src/pages/main/workers/$worker/index.tsx index febf6b848..47e38326f 100644 --- a/frontend/app/src/pages/main/workers/$worker/index.tsx +++ b/frontend/app/src/pages/main/workers/$worker/index.tsx @@ -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(); invariant(tenant); const params = useParams(); diff --git a/frontend/app/src/pages/main/workers/index.tsx b/frontend/app/src/pages/main/workers/index.tsx index d739354e6..37f68ee6c 100644 --- a/frontend/app/src/pages/main/workers/index.tsx +++ b/frontend/app/src/pages/main/workers/index.tsx @@ -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(); invariant(tenant); const listWorkersQuery = useQuery({ diff --git a/frontend/app/src/pages/main/workflow-runs/$run/index.tsx b/frontend/app/src/pages/main/workflow-runs/$run/index.tsx index ef52e25e5..c02b3f42f 100644 --- a/frontend/app/src/pages/main/workflow-runs/$run/index.tsx +++ b/frontend/app/src/pages/main/workflow-runs/$run/index.tsx @@ -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([]); - const [tenant] = useAtom(currTenantAtom); + const { tenant } = useOutletContext(); invariant(tenant); const params = useParams(); diff --git a/frontend/app/src/pages/main/workflow-runs/index.tsx b/frontend/app/src/pages/main/workflow-runs/index.tsx index ffa46da64..2c454c9ae 100644 --- a/frontend/app/src/pages/main/workflow-runs/index.tsx +++ b/frontend/app/src/pages/main/workflow-runs/index.tsx @@ -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(); invariant(tenant); const [sorting, setSorting] = useState([]); diff --git a/frontend/app/src/pages/main/workflows/$workflow/index.tsx b/frontend/app/src/pages/main/workflows/$workflow/index.tsx index 39ccf5b6c..b00404403 100644 --- a/frontend/app/src/pages/main/workflows/$workflow/index.tsx +++ b/frontend/app/src/pages/main/workflows/$workflow/index.tsx @@ -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(); invariant(tenant); const params = useParams(); @@ -126,7 +126,7 @@ function WorkflowDefinition() { } function RecentRunsList() { - const [tenant] = useAtom(currTenantAtom); + const { tenant } = useOutletContext(); invariant(tenant); const params = useParams(); diff --git a/frontend/app/src/pages/main/workflows/index.tsx b/frontend/app/src/pages/main/workflows/index.tsx index 10a048bbe..a7712dc4e 100644 --- a/frontend/app/src/pages/main/workflows/index.tsx +++ b/frontend/app/src/pages/main/workflows/index.tsx @@ -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(); invariant(tenant); const listWorkflowsQuery = useQuery({ diff --git a/frontend/app/src/pages/onboarding/create-tenant/index.tsx b/frontend/app/src/pages/onboarding/create-tenant/index.tsx index 3dacaf3da..27e2b204c 100644 --- a/frontend/app/src/pages/onboarding/create-tenant/index.tsx +++ b/frontend/app/src/pages/onboarding/create-tenant/index.tsx @@ -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, diff --git a/frontend/app/src/router.tsx b/frontend/app/src/router.tsx index b9dbfd054..4f3a9355b 100644 --- a/frontend/app/src/router.tsx +++ b/frontend/app/src/router.tsx @@ -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 () => diff --git a/internal/config/database/config.go b/internal/config/database/config.go index ab74001fc..692ce78ff 100644 --- a/internal/config/database/config.go +++ b/internal/config/database/config.go @@ -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"` diff --git a/internal/config/server/server.go b/internal/config/server/server.go index ce5f22e99..208be1e5e 100644 --- a/internal/config/server/server.go +++ b/internal/config/server/server.go @@ -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") diff --git a/internal/repository/prisma/event.go b/internal/repository/prisma/event.go index a81b784fa..0f9f752bd 100644 --- a/internal/repository/prisma/event.go +++ b/internal/repository/prisma/event.go @@ -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