Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,585 changes: 1,478 additions & 107 deletions cli/src/types/supabase.types.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,7 @@
"select-user-perms": "Select user's permissions",
"select-user-perms-expanded": "Select which permission should the invited user have",
"select-user-role": "Select a role",
"select-at-least-one-role": "Select at least one org role or app role",
"select-role-for-each-app": "Select a role for each app",
"select-user-role-expanded": "Choose the RBAC role to assign. Legacy roles remain visible during migration.",
"select_all": "select all",
Expand Down
4 changes: 2 additions & 2 deletions src/components/dashboard/Usage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ function syncLocalOrgRefreshState(state: { stats_refresh_requested_at: string |
localOrgStatsRefreshRequestedAt.value = state.stats_refresh_requested_at ?? null

if (effectiveOrganization.value) {
effectiveOrganization.value.stats_updated_at = state.stats_updated_at
effectiveOrganization.value.stats_refresh_requested_at = state.stats_refresh_requested_at
effectiveOrganization.value.stats_updated_at = state.stats_updated_at ?? effectiveOrganization.value.stats_updated_at
effectiveOrganization.value.stats_refresh_requested_at = state.stats_refresh_requested_at ?? effectiveOrganization.value.stats_refresh_requested_at
Comment thread
Dalanir marked this conversation as resolved.
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/ApiKeys.vue
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ columns.value = [
label: t('type'),
sortable: true,
displayFunction: (row: Database['public']['Tables']['apikeys']['Row']) => {
return row.mode.toUpperCase()
return row.mode ? row.mode.toUpperCase() : 'RBAC'
},
},
{
Expand Down
106 changes: 40 additions & 66 deletions src/pages/settings/organization/ApiKeys.[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface OrgApiKey {
id: number
rbac_id: string
name: string
mode: string
mode: string | null
limited_to_orgs: string[] | null
limited_to_apps: string[] | null
user_id: string
Expand Down Expand Up @@ -404,38 +404,6 @@ async function copyCreatedKey() {
}
}

async function showPartialFailureKeyModal(plainKey: string, isHashed: boolean) {
createdKeyDialogMode.value = isHashed ? 'partial-failure-hashed' : 'partial-failure-plain'
createdPlainKey.value = plainKey
dialogStore.openDialog({
id: 'org-apikey-created',
title: t('api-key-create-partial-failure-title'),
size: 'lg',
preventAccidentalClose: true,
buttons: [
{
text: t('ok'),
role: 'primary',
},
],
})

await dialogStore.onDialogDismiss()
createdPlainKey.value = ''
createdKeyDialogMode.value = 'success'
}

async function rollbackCreatedApiKey(apikeyId: number | string | null) {
if (!apikeyId)
return null

const { error } = await supabase.functions.invoke(`apikey/${apikeyId}`, {
method: 'DELETE',
})

return error ?? null
}

function validateApiKeyForm() {
if (!editName.value.trim()) {
toast.error(t('please-enter-api-key-name'))
Expand All @@ -447,6 +415,16 @@ function validateApiKeyForm() {
return false
}

// In create mode, at least one binding (org role or app binding) is required
if (isCreateMode.value) {
const hasOrgRole = !!selectedOrgRole.value
const hasAppBindings = configuredAppIds.value.length > 0
if (!hasOrgRole && !hasAppBindings) {
toast.error(t('select-at-least-one-role'))
return false
}
}

return true
}

Expand Down Expand Up @@ -521,15 +499,42 @@ async function createAppRoleBinding(principalId: string, orgId: string, appId: s
}

async function createApiKeyRecord(orgId: string) {
// Build bindings array for the atomic API call
const bindings: Array<{
role_name: string
scope_type: 'org' | 'app'
org_id: string
app_id?: string
}> = []

if (selectedOrgRole.value) {
bindings.push({
role_name: selectedOrgRole.value,
scope_type: 'org',
org_id: orgId,
})
}

for (const [appId, roleName] of Object.entries(pendingAppBindings.value)) {
if (!roleName)
continue
bindings.push({
role_name: roleName,
scope_type: 'app',
org_id: orgId,
app_id: appId,
})
}

const { data, error } = await supabase.functions.invoke('apikey', {
method: 'POST',
body: {
mode: 'all',
name: editName.value.trim(),
limited_to_orgs: [orgId],
limited_to_apps: configuredLimitedAppIds.value,
expires_at: getApiKeyExpirationValue(),
hashed: createAsHashed.value,
bindings,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

Expand All @@ -543,30 +548,6 @@ async function createApiKeyRecord(orgId: string) {
return createdApiKey
}

async function assignBindingsForNewApiKey(orgId: string, principalId: string) {
if (selectedOrgRole.value)
await createOrgRoleBinding(principalId, orgId, selectedOrgRole.value)

for (const [appId, roleName] of Object.entries(pendingAppBindings.value)) {
if (!roleName)
continue
await createAppRoleBinding(principalId, orgId, appId, roleName)
}
}

async function rollbackCreatedApiKeyAfterBindingFailure(
bindingError: unknown,
createdApiKey: CreatedApiKeyResult,
) {
const rollbackError = await rollbackCreatedApiKey(createdApiKey.id)
if (rollbackError) {
console.error('Failed to rollback API key after binding error:', rollbackError)
if (createdApiKey.key)
await showPartialFailureKeyModal(createdApiKey.key, createAsHashed.value)
}
throw bindingError
}

async function finalizeCreatedApiKey(createdPlainKey: string | null) {
if (createdPlainKey)
await showOneTimeKeyModal(createdPlainKey)
Expand Down Expand Up @@ -641,15 +622,8 @@ async function createKey() {

isSubmitting.value = true
try {
// Single atomic call: creates key + bindings in one request
const createdApiKey = await createApiKeyRecord(orgId)

try {
await assignBindingsForNewApiKey(orgId, createdApiKey.rbacId)
}
catch (bindingError) {
await rollbackCreatedApiKeyAfterBindingFailure(bindingError, createdApiKey)
}

await finalizeCreatedApiKey(createdApiKey.key)
}
catch (err) {
Expand Down
6 changes: 3 additions & 3 deletions src/services/apikeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function createDefaultApiKey(

interface ApiKeyListRow {
name?: string | null
mode: string
mode: string | null
created_at: string | null
}

Expand Down Expand Up @@ -80,8 +80,8 @@ export function sortApiKeyRows<T extends ApiKeyListRow>(
bValue = b.name?.toLowerCase() || ''
break
case 'mode':
aValue = a.mode.toLowerCase()
bValue = b.mode.toLowerCase()
aValue = (a.mode ?? '').toLowerCase()
bValue = (b.mode ?? '').toLowerCase()
Comment thread
Dalanir marked this conversation as resolved.
break
case 'created_at':
aValue = a.created_at ? new Date(a.created_at).getTime() : 0
Expand Down
Loading
Loading