diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 8391644281578..92037e9c9c108 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2409,6 +2409,7 @@ export const security: NavMenuConstant = { { name: 'Platform configuration', url: '/guides/security/platform-security' }, { name: 'Product configuration', url: '/guides/security/product-security' }, { name: 'Security testing', url: '/guides/security/security-testing' }, + { name: 'Platform Audit Logs', url: '/guides/security/platform-audit-logs' }, ], }, { diff --git a/apps/docs/content/guides/security/platform-audit-logs.mdx b/apps/docs/content/guides/security/platform-audit-logs.mdx new file mode 100644 index 0000000000000..af8c66cbd707b --- /dev/null +++ b/apps/docs/content/guides/security/platform-audit-logs.mdx @@ -0,0 +1,45 @@ +--- +title: 'Platform Audit Logs' +description: 'Monitor and track organization member activities via platform API or dashboard.' +--- + +Any [Platform API](/docs/reference/api/introduction) or [dashboard](/dashboard) actions performed by organization members are logged automatically for auditing and security purposes. This includes actions such as creating a new project, inviting members, modifying an edge function or changing project settings. + +Besides Platform Audit Logs, Supabase Auth also provides [Auth Audit Logs](/docs/guides/auth/audit-logs) to monitor authentication-related activities within your projects. + + + +Platform Audit Logs are only available on the [Team and Enterprise plans](/pricing). + + + +## Accessing audit logs + +Platform Audit Logs can be found under your [organization's audit logs](/dashboard/org/_/audit). + +Platform audit logs + +For each audit log, you can see additional details by clicking on the log entry: + +- Timestamp of action +- Actor who performed the action + - IP address + - Email + - Token Type +- Action performed + - Name + - Metadata such as route and response status +- Action Target (Project, organization, Edge Function, ...) + +## Limitations + +- There is currently no way to export the logs via dashboard +- There is currently no way to set up a log drain of platform audit logs +- Retention periods depend on your plan diff --git a/apps/docs/public/img/guides/security/platform-audit-logs--dark.png b/apps/docs/public/img/guides/security/platform-audit-logs--dark.png new file mode 100644 index 0000000000000..aad7f6225fe62 Binary files /dev/null and b/apps/docs/public/img/guides/security/platform-audit-logs--dark.png differ diff --git a/apps/docs/public/img/guides/security/platform-audit-logs--light.png b/apps/docs/public/img/guides/security/platform-audit-logs--light.png new file mode 100644 index 0000000000000..a84587a4bbe9a Binary files /dev/null and b/apps/docs/public/img/guides/security/platform-audit-logs--light.png differ diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx index ba69df9be790a..65d9d40620997 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx @@ -12,10 +12,11 @@ import { toast } from 'sonner' import * as z from 'zod' import { useParams } from 'common' +import Panel from 'components/ui/Panel' +import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { useOAuthServerAppCreateMutation } from 'data/oauth-server-apps/oauth-server-app-create-mutation' import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation' import { useOAuthServerAppUpdateMutation } from 'data/oauth-server-apps/oauth-server-app-update-mutation' -import { useSupabaseClientQuery } from 'hooks/use-supabase-client-query' import { Button, FormControl_Shadcn_, @@ -37,10 +38,9 @@ import { Switch, cn, } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { Input } from 'ui-patterns/DataInputs/Input' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import Panel from 'components/ui/Panel' interface CreateOrUpdateOAuthAppSheetProps { visible: boolean @@ -109,26 +109,21 @@ export const CreateOrUpdateOAuthAppSheet = ({ control: form.control, }) - const { data: supabaseClientData } = useSupabaseClientQuery({ projectRef }) - const { mutateAsync: createOAuthApp, isPending: isCreating } = useOAuthServerAppCreateMutation({ + const { data: endpointData } = useProjectEndpointQuery({ projectRef }) + + const { mutate: createOAuthApp, isPending: isCreating } = useOAuthServerAppCreateMutation({ onSuccess: (data) => { toast.success(`Successfully created OAuth app "${data.client_name}"`) onSuccess(data) }, - onError: (error) => { - toast.error(error.message) - }, }) - const { mutateAsync: updateOAuthApp, isPending: isUpdating } = useOAuthServerAppUpdateMutation({ + const { mutate: updateOAuthApp, isPending: isUpdating } = useOAuthServerAppUpdateMutation({ onSuccess: (data) => { toast.success(`Successfully updated OAuth app "${data.client_name}"`) onSuccess(data) }, - onError: (error) => { - toast.error(error.message) - }, }) - const { mutateAsync: regenerateSecret, isPending: isRegenerating } = + const { mutate: regenerateSecret, isPending: isRegenerating } = useOAuthServerAppRegenerateSecretMutation({ onSuccess: (data) => { if (data) { @@ -137,9 +132,6 @@ export const CreateOrUpdateOAuthAppSheet = ({ setShowRegenerateDialog(false) } }, - onError: (error) => { - toast.error(error.message) - }, }) useEffect(() => { @@ -207,8 +199,8 @@ export const CreateOrUpdateOAuthAppSheet = ({ updateOAuthApp({ projectRef, - supabaseClient: supabaseClientData?.supabaseClient, clientId: appToEdit.client_id, + clientEndpoint: endpointData?.endpoint, ...payload, }) } else { @@ -222,7 +214,7 @@ export const CreateOrUpdateOAuthAppSheet = ({ createOAuthApp({ projectRef, - supabaseClient: supabaseClientData?.supabaseClient, + clientEndpoint: endpointData?.endpoint, ...payload, }) } @@ -238,13 +230,11 @@ export const CreateOrUpdateOAuthAppSheet = ({ } const handleConfirmRegenerate = () => { - if (appToEdit?.client_id) { - regenerateSecret({ - projectRef, - supabaseClient: supabaseClientData?.supabaseClient, - clientId: appToEdit.client_id, - }) - } + regenerateSecret({ + projectRef, + clientId: appToEdit?.client_id, + clientEndpoint: endpointData?.endpoint, + }) } const handleUploadLogo = () => uploadButtonRef.current?.click() diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx index acd888f8aeecf..5b228d8ed8bd8 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx @@ -1,8 +1,8 @@ import type { OAuthClient } from '@supabase/supabase-js' import { useParams } from 'common' -import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' -import { useSupabaseClientQuery } from 'hooks/use-supabase-client-query' +import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' +import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' interface DeleteOAuthAppModalProps { @@ -22,15 +22,12 @@ export const DeleteOAuthAppModal = ({ }: DeleteOAuthAppModalProps) => { const { ref: projectRef } = useParams() - const { data: supabaseClientData } = useSupabaseClientQuery({ projectRef }) - + const { data: endpointData } = useProjectEndpointQuery({ projectRef }) const onConfirmDeleteApp = () => { - if (!selectedApp) return - onDelete({ projectRef, - supabaseClient: supabaseClientData?.supabaseClient, - clientId: selectedApp.client_id, + clientId: selectedApp?.client_id, + clientEndpoint: endpointData?.endpoint, }) } diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index 037fa676d617f..ef134e5485193 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -1,8 +1,8 @@ import type { OAuthClient } from '@supabase/supabase-js' -import { MoreVertical, Edit, Plus, RotateCw, Search, Trash, X } from 'lucide-react' +import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react' import Link from 'next/link' -import { useRef, useState } from 'react' import { parseAsBoolean, useQueryState } from 'nuqs' +import { useRef, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' @@ -11,11 +11,11 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation' import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query' -import { useSupabaseClientQuery } from 'hooks/use-supabase-client-query' -import { useQueryStateWithSelect, handleErrorOnDelete } from 'hooks/misc/useQueryStateWithSelect' +import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { Badge, Button, @@ -55,12 +55,8 @@ export const OAuthAppsList = () => { const [filteredClientTypes, setFilteredClientTypes] = useState([]) const deletingOAuthAppIdRef = useRef(null) - const { data: supabaseClientData } = useSupabaseClientQuery({ projectRef }) const { data, isLoading, isError, error } = useOAuthServerAppsQuery( - { - projectRef, - supabaseClient: supabaseClientData?.supabaseClient, - }, + { projectRef }, { enabled: isOAuthServerEnabled } ) @@ -73,6 +69,8 @@ export const OAuthAppsList = () => { }, }) + const { data: endpointData } = useProjectEndpointQuery({ projectRef }) + const oAuthApps = data?.clients || [] const [showCreateSheet, setShowCreateSheet] = useQueryState( @@ -108,9 +106,6 @@ export const OAuthAppsList = () => { }, }) - const handleEditClick = (app: OAuthClient) => setSelectedAppToEdit(app.client_id) - const handleDeleteClick = (app: OAuthClient) => setSelectedAppToDelete(app.client_id) - const [filterString, setFilterString] = useState('') const filteredOAuthApps = filterOAuthApps({ @@ -259,7 +254,7 @@ export const OAuthAppsList = () => { diff --git a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx index cee69af89c43e..83e1e51c83df4 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -38,7 +38,7 @@ const ProductMenuItem = ({ > {name} {label !== undefined && ( - + {label} )} diff --git a/apps/studio/data/api-keys/temp-api-keys-query.ts b/apps/studio/data/api-keys/temp-api-keys-query.ts index 44e821a62e6c3..0f4de86ef2ff6 100644 --- a/apps/studio/data/api-keys/temp-api-keys-query.ts +++ b/apps/studio/data/api-keys/temp-api-keys-query.ts @@ -1,8 +1,4 @@ -import { useQuery } from '@tanstack/react-query' - import { handleError, post } from 'data/fetchers' -import type { ResponseError, UseCustomQueryOptions } from 'types' -import { apiKeysKeys } from './keys' interface getTemporaryAPIKeyVariables { projectRef?: string @@ -31,23 +27,3 @@ export async function getTemporaryAPIKey( if (error) handleError(error) return data } - -export type TemporaryAPIKeyData = Awaited> - -export const useTemporaryAPIKeyQuery = ( - { projectRef }: { projectRef?: string }, - { - enabled = true, - ...options - }: UseCustomQueryOptions = {} -) => { - // The expiry time is set to 60 seconds in the API. - const expiry = 60 - return useQuery({ - queryKey: apiKeysKeys.temporary(projectRef), - queryFn: ({ signal }) => getTemporaryAPIKey({ projectRef, expiry }, signal), - enabled: enabled && typeof projectRef !== 'undefined', - refetchInterval: expiry * 1000, // convert to ms - ...options, - }) -} diff --git a/apps/studio/data/config/project-endpoint-query.ts b/apps/studio/data/config/project-endpoint-query.ts new file mode 100644 index 0000000000000..3407df5995a39 --- /dev/null +++ b/apps/studio/data/config/project-endpoint-query.ts @@ -0,0 +1,17 @@ +import { IS_PLATFORM } from 'common' +import { ProjectSettingsVariables, useProjectSettingsV2Query } from './project-settings-v2-query' + +export const useProjectEndpointQuery = ({ projectRef }: ProjectSettingsVariables) => { + return useProjectSettingsV2Query( + { projectRef }, + { + select: (data) => { + const protocol = data?.app_config?.protocol ?? 'https' + const endpoint = data?.app_config?.endpoint + const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}` + + return { endpoint: clientEndpoint } + }, + } + ) +} diff --git a/apps/studio/data/config/project-settings-v2-query.ts b/apps/studio/data/config/project-settings-v2-query.ts index 4bced094db3d5..c86950615f71d 100644 --- a/apps/studio/data/config/project-settings-v2-query.ts +++ b/apps/studio/data/config/project-settings-v2-query.ts @@ -53,8 +53,8 @@ export const useProjectSettingsV2Query = ( queryKey: configKeys.settingsV2(projectRef), queryFn: ({ signal }) => getProjectSettings({ projectRef }, signal), enabled: enabled && typeof projectRef !== 'undefined', - refetchInterval(_data) { - const data = _data as ProjectSettingsData | undefined + refetchInterval: (_, query) => { + const data = query.state.data const apiKeys = data?.service_api_keys ?? [] const interval = canReadAPIKeys && data?.status !== 'INACTIVE' && apiKeys.length === 0 ? 2000 : 0 diff --git a/apps/studio/data/oauth-server-apps/keys.ts b/apps/studio/data/oauth-server-apps/keys.ts index 34df6f8cca124..d84544f1d2eed 100644 --- a/apps/studio/data/oauth-server-apps/keys.ts +++ b/apps/studio/data/oauth-server-apps/keys.ts @@ -1,4 +1,5 @@ export const oauthServerAppKeys = { // temporaryApiKey has to be added to reset the query when it changes - list: (projectRef: string | undefined) => ['projects', projectRef, 'oauth-server-apps'] as const, + list: (projectRef: string | undefined, clientEndpoint: string | undefined) => + ['projects', projectRef, 'oauth-server-apps', clientEndpoint] as const, } diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts index 231e588774836..e3b1fc0dffa37 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts @@ -1,24 +1,26 @@ -import { CreateOAuthClientParams, SupabaseClient } from '@supabase/supabase-js' +import { CreateOAuthClientParams } from '@supabase/supabase-js' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { handleError } from 'data/fetchers' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomMutationOptions } from 'types' import { oauthServerAppKeys } from './keys' export type OAuthServerAppCreateVariables = CreateOAuthClientParams & { - projectRef?: string - supabaseClient?: SupabaseClient + projectRef: string | undefined + clientEndpoint: string | undefined } export async function createOAuthServerApp({ projectRef, - supabaseClient, + clientEndpoint, ...params }: OAuthServerAppCreateVariables) { if (!projectRef) throw new Error('Project reference is required') - if (!supabaseClient) throw new Error('Supabase client is required') + if (!clientEndpoint) throw new Error('Client endpoint is required') + const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint) const { data, error } = await supabaseClient.auth.admin.oauth.createClient(params) if (error) return handleError(error) @@ -40,9 +42,9 @@ export const useOAuthServerAppCreateMutation = ({ return useMutation({ mutationFn: (vars) => createOAuthServerApp(vars), onSuccess: async (data, variables, context) => { - const { projectRef } = variables + const { projectRef, clientEndpoint } = variables await queryClient.invalidateQueries({ - queryKey: oauthServerAppKeys.list(projectRef), + queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint), }) await onSuccess?.(data, variables, context) }, diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts index c76af7b7621a6..ec82f0b146c9d 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts @@ -1,28 +1,29 @@ -import { SupabaseClient } from '@supabase/supabase-js' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { handleError } from 'data/fetchers' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomMutationOptions } from 'types' import { oauthServerAppKeys } from './keys' export type OAuthServerAppDeleteVariables = { - clientId?: string - projectRef?: string - supabaseClient?: SupabaseClient + clientId: string | undefined + projectRef: string | undefined + clientEndpoint: string | undefined } export async function deleteOAuthServerApp({ projectRef, - supabaseClient, + clientEndpoint, clientId, }: OAuthServerAppDeleteVariables) { if (!projectRef) throw new Error('Project reference is required') - if (!supabaseClient) throw new Error('Supabase client is required') + if (!clientEndpoint) throw new Error('Client endpoint is required') if (!clientId) throw new Error('Client ID is required') + const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint) const { data, error } = await supabaseClient.auth.admin.oauth.deleteClient(clientId) - console.log(data, error) + if (error) return handleError(error) return null } @@ -42,9 +43,9 @@ export const useOAuthServerAppDeleteMutation = ({ return useMutation({ mutationFn: (vars) => deleteOAuthServerApp(vars), onSuccess: async (data, variables, context) => { - const { projectRef } = variables + const { projectRef, clientEndpoint } = variables await queryClient.invalidateQueries({ - queryKey: oauthServerAppKeys.list(projectRef), + queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint), }) await onSuccess?.(data, variables, context) }, diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts index 3409aad8793ce..62f5a2e2647d7 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts @@ -1,26 +1,27 @@ -import { SupabaseClient } from '@supabase/supabase-js' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { handleError } from 'data/fetchers' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomMutationOptions } from 'types' import { oauthServerAppKeys } from './keys' export type OAuthServerAppRegenerateSecretVariables = { - projectRef?: string - supabaseClient?: SupabaseClient - clientId: string + projectRef: string | undefined + clientId: string | undefined + clientEndpoint: string | undefined } export async function regenerateSecret({ projectRef, - supabaseClient, + clientEndpoint, clientId, }: OAuthServerAppRegenerateSecretVariables) { if (!projectRef) throw new Error('Project reference is required') - if (!supabaseClient) throw new Error('Supabase client is required') + if (!clientEndpoint) throw new Error('Client endpoint is required') if (!clientId) throw new Error('Oauth app client id is required') + const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint) const { data, error } = await supabaseClient.auth.admin.oauth.regenerateClientSecret(clientId) if (error) handleError(error) @@ -50,9 +51,9 @@ export const useOAuthServerAppRegenerateSecretMutation = ({ >({ mutationFn: (vars) => regenerateSecret(vars), onSuccess: async (data, variables, context) => { - const { projectRef } = variables + const { projectRef, clientEndpoint } = variables await queryClient.invalidateQueries({ - queryKey: oauthServerAppKeys.list(projectRef), + queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint), }) await onSuccess?.(data, variables, context) }, diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts index e3d6b7bd41ee5..282205d7eb6d2 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts @@ -1,26 +1,29 @@ -import { UpdateOAuthClientParams, SupabaseClient } from '@supabase/supabase-js' +import { UpdateOAuthClientParams } from '@supabase/supabase-js' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { handleError } from 'data/fetchers' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomMutationOptions } from 'types' import { oauthServerAppKeys } from './keys' export type OAuthServerAppUpdateVariables = UpdateOAuthClientParams & { - clientId: string - projectRef?: string - supabaseClient?: SupabaseClient + clientId: string | undefined + projectRef: string | undefined + clientEndpoint: string | undefined } export async function updateOAuthServerApp({ clientId, projectRef, - supabaseClient, + clientEndpoint, ...params }: OAuthServerAppUpdateVariables) { if (!projectRef) throw new Error('Project reference is required') - if (!supabaseClient) throw new Error('Supabase client is required') + if (!clientEndpoint) throw new Error('Client endpoint is required') + if (!clientId) throw new Error('Client ID is required') + const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint) const { data, error } = await supabaseClient.auth.admin.oauth.updateClient(clientId, params) if (error) return handleError(error) @@ -42,9 +45,9 @@ export const useOAuthServerAppUpdateMutation = ({ return useMutation({ mutationFn: (vars) => updateOAuthServerApp(vars), onSuccess: async (data, variables, context) => { - const { projectRef } = variables + const { projectRef, clientEndpoint } = variables await queryClient.invalidateQueries({ - queryKey: oauthServerAppKeys.list(projectRef), + queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint), }) await onSuccess?.(data, variables, context) }, diff --git a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts index 9e8348da037b1..4fc560c00378e 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts @@ -1,14 +1,14 @@ -import { SupabaseClient } from '@supabase/supabase-js' import { useQuery } from '@tanstack/react-query' import { components } from 'api-types' +import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { handleError } from 'data/fetchers' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomQueryOptions } from 'types' import { oauthServerAppKeys } from './keys' export type OAuthServerAppsVariables = { - projectRef?: string - supabaseClient?: SupabaseClient + projectRef: string | undefined page?: number } @@ -18,11 +18,13 @@ export type OAuthApp = components['schemas']['OAuthAppResponse'] export async function getOAuthServerApps({ projectRef, - supabaseClient, + clientEndpoint, page = 1, -}: OAuthServerAppsVariables) { +}: OAuthServerAppsVariables & { clientEndpoint: string | undefined }) { if (!projectRef) throw new Error('Project reference is required') - if (!supabaseClient) throw new Error('Supabase client is required') + if (!clientEndpoint) throw new Error('Client endpoint is required') + + const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint) const { data, error } = await supabaseClient.auth.admin.oauth.listClients({ page, @@ -37,16 +39,21 @@ export type OAuthServerAppsData = Awaited> export type OAuthServerAppsError = ResponseError export const useOAuthServerAppsQuery = ( - { projectRef, supabaseClient }: OAuthServerAppsVariables, + { projectRef }: OAuthServerAppsVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { + const { data: endpointData } = useProjectEndpointQuery({ + projectRef, + }) + const clientEndpoint = endpointData?.endpoint + return useQuery({ - queryKey: oauthServerAppKeys.list(projectRef), - queryFn: () => getOAuthServerApps({ projectRef, supabaseClient }), - enabled: enabled && typeof projectRef !== 'undefined' && !!supabaseClient, + queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint), + queryFn: () => getOAuthServerApps({ projectRef, clientEndpoint }), + enabled: enabled && typeof projectRef !== 'undefined' && !!clientEndpoint, ...options, }) } diff --git a/apps/studio/hooks/use-supabase-client-query.ts b/apps/studio/hooks/use-supabase-client-query.ts deleted file mode 100644 index 032214560bc24..0000000000000 --- a/apps/studio/hooks/use-supabase-client-query.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createClient } from '@supabase/supabase-js' -import { useQuery } from '@tanstack/react-query' - -import { useTemporaryAPIKeyQuery } from 'data/api-keys/temp-api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' - -const getSupabaseClient = ({ - projectRef, - endpoint, - temporaryApiKey, -}: { - projectRef?: string - endpoint?: string - temporaryApiKey?: string -}) => { - if (!projectRef) { - return undefined - } - if (!endpoint) { - return undefined - } - - if (temporaryApiKey === undefined) { - return undefined - } - - const supabaseClient = createClient(endpoint, temporaryApiKey) - - return { supabaseClient } -} - -/** - * The client uses a temporary API key to authenticate requests. The API key expires after one hour which may cause - * 401 errors for all requests made with the client. It's easily fixable by refreshing the page. - */ -export const useSupabaseClientQuery = ( - { projectRef }: { projectRef?: string }, - { enabled = true, ...options } = {} -) => { - const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const { data: temporaryApiKeyData } = useTemporaryAPIKeyQuery({ projectRef }) - - const endpoint = settings - ? `${settings?.app_config?.protocol ?? 'https'}://${settings?.app_config?.endpoint}` - : undefined - const temporaryApiKey = temporaryApiKeyData?.api_key - - return useQuery({ - queryKey: [projectRef, 'supabase-client', endpoint, temporaryApiKey], - queryFn: () => getSupabaseClient({ projectRef, endpoint, temporaryApiKey }), - enabled: enabled && typeof projectRef !== 'undefined' && !!endpoint && !!temporaryApiKey, - ...options, - }) -} diff --git a/apps/studio/lib/project-supabase-client.ts b/apps/studio/lib/project-supabase-client.ts new file mode 100644 index 0000000000000..7031cb4c6f242 --- /dev/null +++ b/apps/studio/lib/project-supabase-client.ts @@ -0,0 +1,28 @@ +import { createClient } from '@supabase/supabase-js' +import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' + +/** + * Creates a Supabase client bound to a specific project. It uses temporary API key. + */ +export async function createProjectSupabaseClient(projectRef: string, clientEndpoint: string) { + try { + const { apiKey } = await getOrRefreshTemporaryApiKey(projectRef) + + return createClient(clientEndpoint, apiKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + storage: { + getItem: (key) => { + return null + }, + setItem: (key, value) => {}, + removeItem: (key) => {}, + }, + }, + }) + } catch (error) { + throw error + } +} diff --git a/apps/studio/pages/project/[ref]/database/replication/index.tsx b/apps/studio/pages/project/[ref]/database/replication/index.tsx index f35eb3395b32c..ba573879ff32c 100644 --- a/apps/studio/pages/project/[ref]/database/replication/index.tsx +++ b/apps/studio/pages/project/[ref]/database/replication/index.tsx @@ -5,6 +5,7 @@ import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import { AlphaNotice } from 'components/ui/AlphaNotice' import { FormHeader } from 'components/ui/Forms/FormHeader' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -27,10 +28,18 @@ const DatabaseReplicationPage: NextPageWithLayout = () => {
- +
+

Replication

+
+

+ Automatically replicate your database changes to external data warehouses and + analytics platforms in real-time +

+
+ diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index b0374992a6eeb..43e30e45029bb 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -1,3 +1,4 @@ +import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { capitalize, chunk, compact, find, findIndex, has, isObject, uniq, uniqBy } from 'lodash' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import { useLatest } from 'react-use' @@ -5,8 +6,6 @@ import { toast } from 'sonner' import * as tus from 'tus-js-client' import { proxy, useSnapshot } from 'valtio' -import { createClient } from '@supabase/supabase-js' -import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { LOCAL_STORAGE_KEYS } from 'common' import { inverseValidObjectKeyRegex, @@ -37,7 +36,7 @@ import { convertFromBytes } from 'components/interfaces/Storage/StorageSettings/ import { InlineLink } from 'components/ui/InlineLink' import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils' import { configKeys } from 'data/config/keys' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' import { getQueryClient } from 'data/query-client' import { deleteBucketObject } from 'data/storage/bucket-object-delete-mutation' @@ -49,6 +48,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { tryParseJson } from 'lib/helpers' import { lookupMime } from 'lib/mime' +import { createProjectSupabaseClient } from 'lib/project-supabase-client' import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui' type UploadProgress = { @@ -76,29 +76,6 @@ if (typeof window !== 'undefined') { abortController = new AbortController() } -async function createSupabaseClient(projectRef: string, clientEndpoint: string) { - try { - const { apiKey } = await getOrRefreshTemporaryApiKey(projectRef) - - return createClient(clientEndpoint, apiKey, { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false, - storage: { - getItem: (key) => { - return null - }, - setItem: (key, value) => {}, - removeItem: (key) => {}, - }, - }, - }) - } catch (error) { - throw error - } -} - function createStorageExplorerState({ projectRef, connectionString, @@ -345,7 +322,7 @@ function createStorageExplorerState({ const formattedPathToEmptyPlaceholderFile = pathToFolder.length > 0 ? `${pathToFolder}/${emptyPlaceholderFile}` : emptyPlaceholderFile - const client = await createSupabaseClient(state.projectRef, clientEndpoint) + const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint) await client.storage .from(state.selectedBucket.name) .upload( @@ -596,7 +573,7 @@ function createStorageExplorerState({ if (data.length === 0) { const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` - const client = await createSupabaseClient(state.projectRef, clientEndpoint) + const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint) await client.storage .from(state.selectedBucket.name) .upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) @@ -831,11 +808,8 @@ function createStorageExplorerState({ >(async (resolve) => { try { // Get authenticated Supabase client for Storage API access - const client = await createSupabaseClient(state.projectRef, clientEndpoint) + const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint) - if (!client) { - throw new Error('Supabase client not available') - } // Use Storage API directly instead of Management API to avoid throttling const { data, error } = await client.storage .from(state.selectedBucket.id) @@ -1517,10 +1491,7 @@ function createStorageExplorerState({ const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined try { - const client = await createSupabaseClient(state.projectRef, clientEndpoint) - if (!client) { - throw new Error('Supabase client not available') - } + const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint) // Use Storage API directly instead of Management API to avoid throttling const { data, error } = await client.storage @@ -1564,10 +1535,7 @@ function createStorageExplorerState({ if (!file.path) return false try { - const client = await createSupabaseClient(state.projectRef, clientEndpoint) - if (!client) { - throw new Error('Supabase client not available') - } + const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint) const { data, error } = await client.storage .from(state.selectedBucket.id) @@ -1902,15 +1870,10 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG)) const stateRef = useLatest(state) - const { data: settings, isSuccess: isSuccessSettings } = useProjectSettingsV2Query({ + const { data: endpointData, isSuccess: isSuccessSettings } = useProjectEndpointQuery({ projectRef: project?.ref, }) - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - const resumableUploadUrl = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}/storage/v1/upload/resumable` - const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}` - // [Joshen] JFYI opting with the useEffect here as the storage explorer state was being loaded // before the project details were ready, hence the store kept returning project ref as undefined // Can be verified when we're saving the storage explorer preferences into local storage, that ref is undefined @@ -1921,6 +1884,8 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const storeAlreadyLoaded = state.projectRef === project?.ref if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) { + const clientEndpoint = endpointData.endpoint + const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable` setState( createStorageExplorerState({ projectRef: project?.ref ?? '', @@ -1936,9 +1901,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild project?.connectionString, stateRef, isPaused, - resumableUploadUrl, - protocol, - endpoint, + endpointData?.endpoint, isSuccessSettings, ]) diff --git a/apps/www/components/Pricing/PricingTableRow.tsx b/apps/www/components/Pricing/PricingTableRow.tsx index 9e92602131832..3d85ca7adddc4 100644 --- a/apps/www/components/Pricing/PricingTableRow.tsx +++ b/apps/www/components/Pricing/PricingTableRow.tsx @@ -63,6 +63,7 @@ export const pricingTooltips: PricingTooltips = { 'auth.thirdPartyMAUs': { main: 'Users who use the Supabase platform through a third-party authentication provider (Firebase Auth, Auth0 or Cognito).\nBilling is based on the sum of distinct third-party users requesting your API through the billing period. Resets every billing cycle.', }, + 'storage.size': { main: "The sum of all objects' size in your storage buckets.\nBilling is prorated down to the hour and will be displayed as GB-Hrs on your invoice.", }, @@ -108,6 +109,35 @@ export const pricingTooltips: PricingTooltips = { 'security.customDomains': { enterprise: 'Volume discounts available.', }, + + 'auth.auditLogs': { + main: ( + + Auth Audit Logs provide comprehensive tracking of authentication events. Audit logs are + automatically captured for all authentication events and help you monitor user + authentication activities, detect suspicious behavior, and maintain compliance with security + requirements. Read more in our{' '} + + docs + + . + + ), + }, + + 'security.platformAuditLogs': { + main: ( + + Any Platform API/Dashboard actions performed by organization members are logged + automatically for auditing and security purposes. Includes actions such as creating a new + project, inviting members or changing project settings. Read more in our{' '} + + docs + + . + + ), + }, } export const PricingTableRowDesktop = (props: any) => { diff --git a/docker/CHANGELOG.md b/docker/CHANGELOG.md index 15e0e8f41c044..b66438d1506a5 100644 --- a/docker/CHANGELOG.md +++ b/docker/CHANGELOG.md @@ -5,24 +5,43 @@ All notable changes to the Supabase self-hosted Docker configuration. Changes are grouped by service rather than by change type. See [versions.md](./versions.md) for complete image version history and rollback information. -Check updates, changelogs, and release notes for each service to learn more. +Check updates for each service to learn more. **Note:** Configuration updates marked with "requires [...] update" are already included in the latest version of the repository. Pull the latest changes or refer to the linked PR for manual updates. After updating `docker-compose.yml`, pull latest images and recreate containers - use `docker compose down && docker compose pull && docker compose up -d`. +--- + ## Unreleased [...] --- +## [2025-11-26] + +### Studio +- Updated to `2025.11.26-sha-8f096b5` +- Fixed MCP `get_advisors` tool - PR [#40783](https://github.com/supabase/supabase/pull/40783) +- Fixed AI Assistant request schema - PR [#40830](https://github.com/supabase/supabase/pull/40830) +- Fixed log drains page - PR [#40835](https://github.com/supabase/supabase/pull/40835) + +### Realtime +- Updated to `v2.65.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.3) + +### Analytics (Logflare) +- Updated to `v1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13) +- Fixed crashdump when `POSTGRES_BACKEND_URL` is malformed - PR [logflare#2954](https://github.com/Logflare/logflare/pull/2954) + +--- + ## [2025-11-25] ### Studio - Updated to `2025.11.24-sha-d990ae8` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40734) -- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - [PR #40078](https://github.com/supabase/supabase/pull/40078) +- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - PR [#40078](https://github.com/supabase/supabase/pull/40078) +- Fixed parameterized SQL queries in MCP tools - PR [#40499](https://github.com/supabase/supabase/pull/40499) - Fixed Studio showing paid options for log drains - [PR #40510](https://github.com/supabase/supabase/pull/40510) -- Fixed parameterized SQL queries in MCP tools - [PR #40499](https://github.com/supabase/supabase/pull/40499) -- Fixed AI Assistant - [PR #40654](https://github.com/supabase/supabase/pull/40654) +- Fixed AI Assistant authentication - PR [#40654](https://github.com/supabase/supabase/pull/40654) ### Auth - Updated to `v2.183.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.183.0) @@ -37,16 +56,17 @@ Check updates, changelogs, and release notes for each service to learn more. - Updated to `v1.69.25` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.25) ### Analytics (Logflare) -- Updated to `1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12) -- Fixed Auth logs query - [PR #2936](https://github.com/Logflare/logflare/pull/2936) -- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - [PR #2942](https://github.com/Logflare/logflare/pull/2942) +- Updated to `v1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12) +- Fixed Auth logs query - PR [logflare#2936](https://github.com/Logflare/logflare/pull/2936) +- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - PR [logflare#2942](https://github.com/Logflare/logflare/pull/2942) --- ## [2025-11-17] ### Storage -- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - [PR #40500](https://github.com/supabase/supabase/pull/40500) +- No image update +- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - PR [#40500](https://github.com/supabase/supabase/pull/40500) --- @@ -54,8 +74,8 @@ Check updates, changelogs, and release notes for each service to learn more. ### Studio - Updated to `2025.11.10-sha-5291fe3` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083) -- Added log drains - [PR #28297](https://github.com/supabase/supabase/pull/28297) -- Fixed Studio using `postgres` role instead of `supabase_admin` - [PR #39946](https://github.com/supabase/supabase/pull/39946) +- Added log drains - PR [#28297](https://github.com/supabase/supabase/pull/28297) +- Fixed Studio using `postgres` role instead of `supabase_admin` - PR [#39946](https://github.com/supabase/supabase/pull/39946) ### Auth - Updated to `v2.182.1` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md#21821-2025-11-05) | [Release](https://github.com/supabase/auth/releases/tag/v2.182.1) @@ -70,17 +90,19 @@ Check updates, changelogs, and release notes for each service to learn more. - Updated to `v1.69.23` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.23) ### Supavisor -- Updated to `2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4) +- Updated to `v2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4) --- ## [2025-11-05] ### Studio -- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - [PR #40169](https://github.com/supabase/supabase/pull/40169) +- No image update +- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - PR [#40169](https://github.com/supabase/supabase/pull/40169) ### Realtime -- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - [PR #39963](https://github.com/supabase/supabase/pull/39963) +- No image update +- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - PR [#39963](https://github.com/supabase/supabase/pull/39963) --- @@ -88,7 +110,7 @@ Check updates, changelogs, and release notes for each service to learn more. ### Studio - Updated to `2025.10.27-sha-85b84e0` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083) -- Fixed broken authentication when uploading files to Storage - [PR #39829](https://github.com/supabase/supabase/pull/39829) +- Fixed broken authentication when uploading files to Storage - PR [#39829](https://github.com/supabase/supabase/pull/39829) ### Realtime - Updated to `v2.57.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.57.2) @@ -107,8 +129,9 @@ Check updates, changelogs, and release notes for each service to learn more. ## [2025-10-27] ### Studio -- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - [PR #39849](https://github.com/supabase/supabase/pull/39849) -- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - [PR #39952](https://github.com/supabase/supabase/pull/39952) +- No image update +- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - PR [#39849](https://github.com/supabase/supabase/pull/39849) +- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - PR [#39952](https://github.com/supabase/supabase/pull/39952) --- @@ -116,7 +139,7 @@ Check updates, changelogs, and release notes for each service to learn more. ### Studio - Updated to `2025.10.20-sha-5005fc6` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709) -- Fixed issues with Edge Functions and cron logs not being visible in Studio - [PR #39388](https://github.com/supabase/supabase/pull/39388), [PR #39704](https://github.com/supabase/supabase/pull/39704), [PR #39711](https://github.com/supabase/supabase/pull/39711) +- Fixed issues with Edge Functions and cron logs not being visible in Studio - PR [#39388](https://github.com/supabase/supabase/pull/39388), PR [#39704](https://github.com/supabase/supabase/pull/39704), PR [#39711](https://github.com/supabase/supabase/pull/39711) ### Realtime - Updated to `v2.56.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.56.0) @@ -128,17 +151,17 @@ Check updates, changelogs, and release notes for each service to learn more. - Updated to `v0.93.0` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.0) ### Edge Runtime -- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/v1.69.14) +- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.14) ### Supavisor -- Updated to `2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3) +- Updated to `v2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3) --- ## [2025-10-13] ### Analytics (Logflare) -- Updated to `1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6) +- Updated to `v1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6) --- @@ -146,7 +169,7 @@ Check updates, changelogs, and release notes for each service to learn more. ### Studio - Updated to `2025.10.01-sha-8460121` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709) -- Added "local" remote MCP server - [PR #38797](https://github.com/supabase/supabase/pull/38797), [PR #39041](https://github.com/supabase/supabase/pull/39041) +- Added "local" remote MCP server - PR [#38797](https://github.com/supabase/supabase/pull/38797), PR [#39041](https://github.com/supabase/supabase/pull/39041) - ⚠️ Changed Studio connection method to `postgres-meta` - affects non-standard database port configurations ### Auth @@ -165,7 +188,7 @@ Check updates, changelogs, and release notes for each service to learn more. - Updated to `v0.91.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.91.6) ### Analytics (Logflare) -- Updated to `1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4) +- Updated to `v1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4) ### Postgres - Updated to `15.8.1.085` - [Release](https://github.com/supabase/postgres/releases/tag/15.8.1.085) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e3c025d7f4978..034f04e88600b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,7 +11,7 @@ services: studio: container_name: supabase-studio - image: supabase/studio:2025.11.25-sha-8de52c4 + image: supabase/studio:2025.11.26-sha-8f096b5 restart: unless-stopped healthcheck: test: diff --git a/docker/versions.md b/docker/versions.md index 3753fe091164c..0c0b2e03861c6 100644 --- a/docker/versions.md +++ b/docker/versions.md @@ -1,5 +1,10 @@ # Docker Image Versions +## 2025-11-26 +- supabase/studio:2025.11.26-sha-8f096b5 (prev supabase/studio:2025.11.24-sha-d990ae8) +- supabase/realtime:v2.65.3 (prev supabase/realtime:v2.65.2) +- supabase/logflare:1.26.13 (prev supabase/logflare:1.26.12) + ## 2025-11-25 - supabase/studio:2025.11.24-sha-d990ae8 (prev supabase/studio:2025.11.10-sha-5291fe3) - supabase/gotrue:v2.183.0 (prev supabase/gotrue:v2.182.1) diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index c7507541c637f..a4f4054bc6094 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -45,7 +45,7 @@ export type FeatureKey = | 'auth.socialOAuthProviders' | 'auth.customSMTPServer' | 'auth.removeSupabaseBranding' - | 'auth.auditTrails' + | 'auth.auditLogs' | 'auth.basicMFA' | 'auth.advancedMFAPhone' | 'auth.thirdPartyMAUs' @@ -69,7 +69,7 @@ export type FeatureKey = | 'realtime.messagesPerMonth' | 'realtime.maxMessageSize' | 'dashboard.teamMembers' - | 'dashboard.auditTrails' + | 'security.platformAuditLogs' | 'security.byoc' | 'security.logRetention' | 'security.logDrain' @@ -289,8 +289,8 @@ export const pricing: Pricing = { usage_based: false, }, { - key: 'auth.auditTrails', - title: 'Audit trails', + key: 'auth.auditLogs', + title: 'Auth Audit Logs', plans: { free: '1 hour', pro: '7 days', @@ -556,17 +556,6 @@ export const pricing: Pricing = { }, usage_based: false, }, - { - key: 'dashboard.auditTrails', - title: 'Audit trails', - plans: { - free: false, - pro: false, - team: true, - enterprise: true, - }, - usage_based: false, - }, ], }, security: { @@ -606,6 +595,17 @@ export const pricing: Pricing = { }, usage_based: true, }, + { + key: 'security.platformAuditLogs', + title: 'Platform Audit Logs', + plans: { + free: false, + pro: false, + team: true, + enterprise: true, + }, + usage_based: false, + }, { key: 'security.metricsEndpoint', title: 'Metrics endpoint', diff --git a/packages/ui/src/components/radio-group-stacked.tsx b/packages/ui/src/components/radio-group-stacked.tsx index faa6fbce5986f..86447f8bf89f5 100644 --- a/packages/ui/src/components/radio-group-stacked.tsx +++ b/packages/ui/src/components/radio-group-stacked.tsx @@ -38,22 +38,22 @@ const RadioGroupStackedItem = React.forwardRef< ref={ref} {...props} className={cn( - 'flex flex-col gap-2', - 'w-full', - 'bg-overlay/50 disabled:opacity-50', - 'border', + // Base layout and sizing + 'flex flex-col gap-2 w-full', + // Base styles + 'bg-overlay/50 border shadow-sm', 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg', - 'shadow-sm', - 'enabled:hover:bg-surface-300', - 'enabled:hover:border-foreground-muted', - 'enabled:cursor-pointer disabled:cursor-not-allowed', - 'hover:z-[1] focus-visible:z-[1]', - 'data-[state=checked]:z-[1]', + // Disabled state + 'disabled:opacity-50 disabled:cursor-not-allowed', + // Enabled/hover states + 'enabled:cursor-pointer enabled:hover:bg-surface-300 enabled:hover:border-foreground-muted', + // Z-index for interactions + 'hover:z-[1] focus-visible:z-[1] data-[state=checked]:z-[1]', + // Checked state 'data-[state=checked]:ring-1 data-[state=checked]:ring-border', - 'data-[state=checked]:bg-surface-300', - 'data-[state=checked]:border-foreground-muted', - 'transition', - 'group', + 'data-[state=checked]:bg-surface-300 data-[state=checked]:border-foreground-muted', + // Transitions and group + 'transition group', props.className )} > @@ -61,20 +61,19 @@ const RadioGroupStackedItem = React.forwardRef< {showIndicator && (
- +
)} @@ -82,10 +81,11 @@ const RadioGroupStackedItem = React.forwardRef<