Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0c8b615
fix: update button height and loading spinner minimum height
riderx May 11, 2026
fd18e6a
feat(apikeys): migrate legacy keys to v2
riderx May 17, 2026
1f192f2
fix(tests): update apikey v2 coverage
riderx May 17, 2026
56ff01f
fix(db): remove stale apikey lint variable
riderx May 17, 2026
cb8d4a2
fix(tests): bind v2 keys for dynamic orgs
riderx May 17, 2026
094c064
fix(tests): bind app validation key to org
riderx May 17, 2026
9d6036d
fix(tests): create hashed apikey fixtures in postgres
riderx May 17, 2026
c368fe3
feat(rbac): make legacy rights compatibility rbac-backed
riderx May 17, 2026
00f8351
fix(rbac): align pg tests with rbac-only rights
riderx May 17, 2026
982e5a2
fix(tests): finish rbac-only pg expectations
riderx May 17, 2026
290f27a
fix(rbac): keep pending invites out of active access
riderx May 17, 2026
65d8bed
fix(rbac): accept legacy invites after pending cleanup
riderx May 17, 2026
e1e8a89
fix(rbac): preserve legacy sql invite acceptance
riderx May 17, 2026
c4f6d79
fix(tests): seed legacy invite acceptance row
riderx May 17, 2026
e5911b3
fix(tests): force legacy invite fixture row
riderx May 17, 2026
46aef8e
fix(tests): use seeded org for legacy invite flow
riderx May 17, 2026
4067e59
fix(tests): align backend tests with rbac-only rights
riderx May 17, 2026
2006a98
fix(tests): expect scoped key org gate
riderx May 17, 2026
8bf3f31
fix(tests): expect hidden org for scoped key
riderx May 17, 2026
d6f78c8
fix(rbac): address apikey v2 review feedback
riderx May 17, 2026
b7aff9e
fix(tests): bind effective api key user permissions
riderx May 17, 2026
e8b6467
fix(tests): assert api key effective user mismatch
riderx May 17, 2026
dbcee6b
fix(tests): respect api key expiration binding policy
riderx May 17, 2026
93e6681
fix(api): remove dead transaction state writes
riderx May 17, 2026
5db42d7
Merge remote-tracking branch 'origin/main' into codex/migrate-apikeys-v2
riderx May 17, 2026
513a4c5
fix(db): move apikey v2 migration after main
riderx May 17, 2026
ca79eea
fix(db): preserve cli warnings on apikey v2
riderx May 17, 2026
89e6636
fix(db): preserve apikey v2 migration scopes
riderx May 18, 2026
9bcc536
fix(frontend): remove obsolete rbac invite flag paths
Dalanir May 22, 2026
106a7b1
fix(db): merge main for apikey v2 review
riderx May 23, 2026
c7d12bb
fix(db): update read replica schema snapshot
riderx May 23, 2026
8a61263
fix(plugin): speed up update payload validation
riderx May 23, 2026
67a6ad2
fix(db): harden apikey v2 migration
riderx May 23, 2026
6bbb493
fix(ci): make migration order check work in pr checkout
riderx May 23, 2026
3c77876
fix(apikey): tighten v2 migration scope
riderx May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion playwright/e2e/apikeys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down
68 changes: 34 additions & 34 deletions read_replicate/schema_replicate.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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: -
--
Expand Down Expand Up @@ -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: -
--
Expand Down
13 changes: 12 additions & 1 deletion scripts/check-supabase-migration-order.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions scripts/restore_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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`)
Expand All @@ -108,18 +114,16 @@ 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,
})

if (insertError) {
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)`)
}
}
}
Expand Down
1 change: 0 additions & 1 deletion src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
49 changes: 8 additions & 41 deletions src/components/dashboard/AppAccess.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -60,7 +59,6 @@ const roleBindings = ref<RoleBinding[]>([])
const availableAppRoles = ref<Role[]>([])
const search = ref('')
const currentPage = ref(1)
const useNewRbac = ref(false)
const canAssignRoles = ref(false)
const ownerOrg = ref<string>('')

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })
Expand All @@ -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 () => {
Expand All @@ -488,12 +462,6 @@ onMounted(async () => {

<template>
<div class="w-full px-3 py-2">
<!-- RBAC not enabled message -->
<div v-if="!useNewRbac" class="mb-4 alert alert-info">
<IconInformation class="size-5" />
<span>{{ t('rbac-not-enabled-for-org') }}</span>
</div>

<!-- Header -->
<div class="flex items-center justify-between mb-4">
<div>
Expand All @@ -506,7 +474,7 @@ onMounted(async () => {
</p>
</div>
<button
v-if="useNewRbac && canAssignRoles"
v-if="canAssignRoles"
class="d-btn d-btn-primary"
@click="openAssignRoleModal"
>
Expand All @@ -516,7 +484,7 @@ onMounted(async () => {
</div>

<!-- Search -->
<div v-if="useNewRbac" class="mb-4">
<div class="mb-4">
<SearchInput
v-model="search"
:placeholder="t('search-role-bindings')"
Expand All @@ -526,7 +494,6 @@ onMounted(async () => {

<!-- Role bindings table -->
<DataTable
v-if="useNewRbac"
:columns="columns"
:element-list="filteredBindings"
:total="filteredBindings.length"
Expand Down
33 changes: 11 additions & 22 deletions src/components/dashboard/AppOnboardingFlow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import IconSmartphone from '~icons/lucide/smartphone'
import IconSparkles from '~icons/lucide/sparkles'
import IconStore from '~icons/lucide/store'
import IconTerminal from '~icons/lucide/terminal'
import { createDefaultApiKey } from '~/services/apikeys'
import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys'
import { createSignedImageUrl, getImmediateImageUrl } from '~/services/storage'
import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase'
import { useDialogV2Store } from '~/stores/dialogv2'
Expand Down Expand Up @@ -257,18 +257,9 @@ async function ensureApiKey() {
if (!userId)
return

const isLiveKey = (expiresAt: string | null) => !expiresAt || new Date(expiresAt).getTime() > Date.now()

const { data, error } = await supabase
.from('apikeys')
.select('key, expires_at')
.eq('user_id', userId)
.eq('mode', 'all')
.order('created_at', { ascending: false })

const validKey = !error ? data?.find(key => !!key.key && isLiveKey(key.expires_at)) : null
if (validKey?.key) {
apiKey.value = validKey.key
const existingKey = await findUsablePlainApiKey(supabase, userId, currentOrg.value?.gid, resumeAppId.value)
if (existingKey) {
apiKey.value = existingKey
return
}

Expand All @@ -277,18 +268,16 @@ async function ensureApiKey() {
if (!claimsUserId)
return

const { error: createError } = await createDefaultApiKey(supabase, 'api-key')
const { data, error: createError } = await createDefaultApiKey(supabase, 'api-key', {
orgId: currentOrg.value?.gid,
appId: resumeAppId.value,
})
if (createError)
throw createError

const { data: refreshedData } = await supabase
.from('apikeys')
.select('key, expires_at')
.eq('user_id', claimsUserId)
.eq('mode', 'all')
.order('created_at', { ascending: false })

apiKey.value = refreshedData?.find(key => !!key.key && isLiveKey(key.expires_at))?.key ?? null
apiKey.value = typeof data?.key === 'string'
? data.key
: await findUsablePlainApiKey(supabase, claimsUserId, currentOrg.value?.gid, resumeAppId.value)
}

async function loadResumeApp() {
Expand Down
Loading
Loading