diff --git a/messages/en.json b/messages/en.json index 0bc7365815..1ded4a20b6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -247,10 +247,8 @@ "alert-add-new-key": "Select new API key type", "alert-cannot-delete-owner-body": "This user is the last super admin of this organization, you cannot delete it. You can either assign the super admin role to another user or delete the organization.", "alert-cannot-delete-owner-title": "Cannot delete the last super admin", - "alert-confirm-appid-limit": "Limit the API key to certain apps", "alert-confirm-delete": "Confirm Delete", "alert-confirm-invite": "Confirm invitation", - "alert-confirm-org-limit": "Limit the API key to certain organizations", "alert-confirm-regenerate": "Confirm regenerating API key", "alert-delete-message": "Are you sure you want to delete this", "alert-delete-message-plural": "Are you sure you want to delete these", diff --git a/playwright/e2e/apikeys.spec.ts b/playwright/e2e/apikeys.spec.ts index e56311c562..e392477d4e 100644 --- a/playwright/e2e/apikeys.spec.ts +++ b/playwright/e2e/apikeys.spec.ts @@ -4,7 +4,9 @@ import { expect, test } from '../support/commands' async function createReadApiKey(page: Page, keyName: string) { await page.click('[data-test="create-key"]') await page.locator('#dialog-v2-content input[type="text"]').fill(keyName) - await page.locator('#dialog-v2-content input[name="key-type"][value="read"]').check() + await page.locator('#dialog-v2-content label').filter({ hasText: 'Read' }).click() + await page.locator('#dialog-v2-content label').filter({ hasText: 'Limit the API key to selected organizations?' }).click() + await page.locator('#dialog-v2-content label').filter({ hasText: 'Demo org' }).click() await page.getByRole('button', { name: 'Create' }).click() await expect(page.locator('[data-test="toast"]')).toContainText('Added new API key successfully') } diff --git a/read_replicate/schema_replicate.sql b/read_replicate/schema_replicate.sql index 05f43f6738..0d74820b35 100644 --- a/read_replicate/schema_replicate.sql +++ b/read_replicate/schema_replicate.sql @@ -87,6 +87,40 @@ $$; -- +-- +-- Name: apps; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.apps ( + created_at timestamp with time zone DEFAULT now(), + app_id character varying NOT NULL, + icon_url character varying NOT NULL, + user_id uuid, + name character varying, + last_version character varying, + updated_at timestamp with time zone, + id uuid DEFAULT gen_random_uuid(), + retention bigint DEFAULT '2592000'::bigint NOT NULL, + owner_org uuid NOT NULL, + default_upload_channel character varying DEFAULT 'production'::character varying NOT NULL, + transfer_history jsonb[] DEFAULT '{}'::jsonb[], + channel_device_count bigint DEFAULT 0 NOT NULL, + manifest_bundle_count bigint DEFAULT 0 NOT NULL, + expose_metadata boolean DEFAULT false NOT NULL, + allow_preview boolean DEFAULT false NOT NULL, + allow_device_custom_id boolean DEFAULT true NOT NULL, + need_onboarding boolean DEFAULT false NOT NULL, + existing_app boolean DEFAULT false NOT NULL, + ios_store_url text, + android_store_url text, + stats_updated_at timestamp without time zone, + stats_refresh_requested_at timestamp without time zone, + build_timeout_seconds bigint DEFAULT 900 NOT NULL, + build_timeout_updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT apps_build_timeout_seconds_check CHECK (((build_timeout_seconds >= 300) AND (build_timeout_seconds <= 21600))) +); + + -- -- Name: app_versions; Type: TABLE; Schema: public; Owner: - -- @@ -132,40 +166,6 @@ ALTER TABLE public.app_versions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDEN ); --- --- Name: apps; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.apps ( - created_at timestamp with time zone DEFAULT now(), - app_id character varying NOT NULL, - icon_url character varying NOT NULL, - user_id uuid, - name character varying, - last_version character varying, - updated_at timestamp with time zone, - id uuid DEFAULT gen_random_uuid(), - retention bigint DEFAULT '2592000'::bigint NOT NULL, - owner_org uuid NOT NULL, - default_upload_channel character varying DEFAULT 'production'::character varying NOT NULL, - transfer_history jsonb[] DEFAULT '{}'::jsonb[], - channel_device_count bigint DEFAULT 0 NOT NULL, - manifest_bundle_count bigint DEFAULT 0 NOT NULL, - expose_metadata boolean DEFAULT false NOT NULL, - allow_preview boolean DEFAULT false NOT NULL, - allow_device_custom_id boolean DEFAULT true NOT NULL, - need_onboarding boolean DEFAULT false NOT NULL, - existing_app boolean DEFAULT false NOT NULL, - ios_store_url text, - android_store_url text, - stats_updated_at timestamp without time zone, - stats_refresh_requested_at timestamp without time zone, - build_timeout_seconds bigint DEFAULT 900 NOT NULL, - build_timeout_updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT apps_build_timeout_seconds_check CHECK (((build_timeout_seconds >= 300) AND (build_timeout_seconds <= 21600))) -); - - -- -- Name: channel_devices; Type: TABLE; Schema: public; Owner: - -- diff --git a/scripts/check-supabase-migration-order.sh b/scripts/check-supabase-migration-order.sh index 305076efe3..28ce064a79 100755 --- a/scripts/check-supabase-migration-order.sh +++ b/scripts/check-supabase-migration-order.sh @@ -55,7 +55,18 @@ added_timestamps_file="${tmp_dir}/added_timestamps.tsv" trap 'rm -rf "${tmp_dir}"' EXIT echo "Checking Supabase migrations against ${base_ref}" -git fetch --no-tags origin "${target_branch}" +if ! git fetch --no-tags origin "${target_branch}"; then + if git rev-parse --verify --quiet "${base_ref}^{commit}" >/dev/null; then + echo "⚠️ Could not fetch ${base_ref}; using existing local ref." + elif git rev-parse --verify --quiet "HEAD^1^{commit}" >/dev/null \ + && git rev-parse --verify --quiet "HEAD^2^{commit}" >/dev/null; then + base_ref='HEAD^1' + echo "⚠️ Could not fetch origin/${target_branch}; using PR merge base parent." + else + echo "❌ Could not fetch ${base_ref} and no local fallback was available." + exit 1 + fi +fi : > "${base_timestamps_file}" while IFS= read -r file; do diff --git a/scripts/restore_account.ts b/scripts/restore_account.ts index 7d7d98dcb8..5de83bf64b 100644 --- a/scripts/restore_account.ts +++ b/scripts/restore_account.ts @@ -22,12 +22,9 @@ interface ApiKey { id: number created_at: string user_id: string - key: string - mode: 'all' | 'upload' | 'read' | 'write' + key: string | null updated_at: string name: string - limited_to_orgs: string[] - limited_to_apps: string[] expires_at: string | null key_hash: string | null } @@ -90,12 +87,21 @@ async function main() { console.log(` Found ${apikeys.length} API key(s) to restore`) for (const apikey of apikeys) { - // Check if this apikey already exists (by key value) - const { data: existingKey } = await supabase + if (!apikey.key && !apikey.key_hash) { + console.log(` Skipping API key "${apikey.name}" - no key value or hash to restore`) + continue + } + + // Check if this apikey already exists by visible key or hash. + let existingKeyQuery = supabase .from('apikeys') .select('id') - .eq('key', apikey.key) - .single() + + existingKeyQuery = apikey.key + ? existingKeyQuery.eq('key', apikey.key) + : existingKeyQuery.eq('key_hash', apikey.key_hash) + + const { data: existingKey } = await existingKeyQuery.single() if (existingKey) { console.log(` Skipping API key "${apikey.name}" - already exists`) @@ -108,10 +114,8 @@ async function main() { .insert({ user_id: apikey.user_id, key: apikey.key, - mode: apikey.mode, name: apikey.name, - limited_to_orgs: apikey.limited_to_orgs || [], - limited_to_apps: apikey.limited_to_apps || [], + key_hash: apikey.key_hash, expires_at: apikey.expires_at, }) @@ -119,7 +123,7 @@ async function main() { console.error(` Error restoring API key "${apikey.name}":`, insertError) } else { - console.log(` Restored API key: "${apikey.name}"`) + console.log(` Restored API key: "${apikey.name}" (RBAC bindings must be reassigned)`) } } } diff --git a/src/components.d.ts b/src/components.d.ts index 9620ecc5a6..689da7ae98 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -19,7 +19,6 @@ declare module 'vue' { AdminOnlyModal: typeof import('./components/AdminOnlyModal.vue')['default'] AdminStatsCard: typeof import('./components/admin/AdminStatsCard.vue')['default'] AdminTrendChart: typeof import('./components/admin/AdminTrendChart.vue')['default'] - ApiKeyRbacManager: typeof import('./components/organization/ApiKeyRbacManager.vue')['default'] AppAccess: typeof import('./components/dashboard/AppAccess.vue')['default'] AppNotFoundModal: typeof import('./components/AppNotFoundModal.vue')['default'] AppOnboardingFlow: typeof import('./components/dashboard/AppOnboardingFlow.vue')['default'] diff --git a/src/components/dashboard/AppAccess.vue b/src/components/dashboard/AppAccess.vue index ced4710829..fd6b45b644 100644 --- a/src/components/dashboard/AppAccess.vue +++ b/src/components/dashboard/AppAccess.vue @@ -4,7 +4,6 @@ import type { TableColumn } from '~/components/comp_def' import { computed, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' -import IconInformation from '~icons/heroicons/information-circle' import IconLock from '~icons/heroicons/lock-closed' import IconPlus from '~icons/heroicons/plus' import IconShield from '~icons/heroicons/shield-check' @@ -60,7 +59,6 @@ const roleBindings = ref([]) const availableAppRoles = ref([]) const search = ref('') const currentPage = ref(1) -const useNewRbac = ref(false) const canAssignRoles = ref(false) const ownerOrg = ref('') @@ -145,27 +143,6 @@ async function fetchAppDetails() { } } -async function checkRbacEnabled() { - if (!ownerOrg.value) - return - - try { - const { data, error } = await supabase - .from('orgs') - .select('use_new_rbac') - .eq('id', ownerOrg.value) - .single() - - if (error) - throw error - - useNewRbac.value = (data as any)?.use_new_rbac || false - } - catch (error: any) { - console.error('Error checking RBAC status:', error) - } -} - async function fetchAppRoleBindings() { if (!props.appId || !ownerOrg.value) return @@ -454,7 +431,6 @@ async function removeRoleBinding(bindingId: string) { async function loadAppAccess() { await fetchAppDetails() - await checkRbacEnabled() if (props.appId) { try { canAssignRoles.value = await checkPermissions('app.update_user_roles', { appId: props.appId }) @@ -467,14 +443,12 @@ async function loadAppAccess() { else { canAssignRoles.value = false } - if (useNewRbac.value) { - await Promise.all([ - fetchAppRoleBindings(), - fetchAvailableAppRoles(), - fetchAvailableMembers(), - fetchAvailableGroups(), - ]) - } + await Promise.all([ + fetchAppRoleBindings(), + fetchAvailableAppRoles(), + fetchAvailableMembers(), + fetchAvailableGroups(), + ]) } watch(() => props.appId, async () => { @@ -488,12 +462,6 @@ onMounted(async () => {