mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 11:30:17 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES Waiting on #48809 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added custom access-token expiration date limits, allowing dates from today through one year ahead. * Date pickers now enforce configured minimum and maximum date boundaries. * **Updates** * Removed the option to create non-expiring access tokens. * Expiration is now required when creating classic access tokens. * Improved form reset behavior and expiry tracking. * **Tests** * Added validation coverage for required, valid, and out-of-range custom expiration dates. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Wen Bo Xie <wenbox323@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
import dayjs from 'dayjs'
|
|
|
|
import { PERMISSION_LIST, ScopedAccessTokenPermission } from './AccessToken.constants'
|
|
import {
|
|
AccessTokenSort,
|
|
AccessTokenSortColumn,
|
|
AccessTokenSortOrder,
|
|
BaseToken,
|
|
} from './AccessToken.types'
|
|
|
|
/** Custom expiry dates are capped at one year from today, for classic and scoped tokens alike. */
|
|
export const getMaxCustomExpiryDate = (): dayjs.Dayjs => dayjs().add(1, 'year').endOf('day')
|
|
|
|
export const handleSortChange = (
|
|
currentSort: AccessTokenSort,
|
|
column: AccessTokenSortColumn,
|
|
setSort: (sort: AccessTokenSort) => void
|
|
) => {
|
|
const [currentCol, currentOrder] = currentSort.split(':') as [
|
|
AccessTokenSortColumn,
|
|
AccessTokenSortOrder,
|
|
]
|
|
if (currentCol === column) {
|
|
if (currentOrder === 'asc') {
|
|
setSort(`${column}:desc` as AccessTokenSort)
|
|
} else {
|
|
setSort('created_at:desc')
|
|
}
|
|
} else {
|
|
setSort(`${column}:asc` as AccessTokenSort)
|
|
}
|
|
}
|
|
|
|
export const filterAndSortTokens = <T extends BaseToken>(
|
|
tokens: T[] | undefined,
|
|
searchString: string,
|
|
sort: AccessTokenSort
|
|
): T[] | undefined => {
|
|
const filtered = !searchString
|
|
? tokens
|
|
: tokens?.filter((token) => token.name.toLowerCase().includes(searchString.toLowerCase()))
|
|
|
|
if (!filtered) return filtered
|
|
|
|
const [sortCol, sortOrder] = sort.split(':') as [AccessTokenSortColumn, AccessTokenSortOrder]
|
|
const orderMultiplier = sortOrder === 'asc' ? 1 : -1
|
|
|
|
return [...filtered].sort((a, b) => {
|
|
if (sortCol === 'created_at') {
|
|
return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier
|
|
}
|
|
if (sortCol === 'last_used_at') {
|
|
if (!a.last_used_at && !b.last_used_at) return 0
|
|
if (!a.last_used_at) return 1
|
|
if (!b.last_used_at) return -1
|
|
return (
|
|
(new Date(a.last_used_at).getTime() - new Date(b.last_used_at).getTime()) * orderMultiplier
|
|
)
|
|
}
|
|
if (sortCol === 'expires_at') {
|
|
if (!a.expires_at && !b.expires_at) return 0
|
|
if (!a.expires_at) return 1
|
|
if (!b.expires_at) return -1
|
|
return (new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()) * orderMultiplier
|
|
}
|
|
return 0
|
|
})
|
|
}
|
|
|
|
export const mapPermissionToFGA = (
|
|
resourceKey: string,
|
|
action: string
|
|
): ScopedAccessTokenPermission[] => {
|
|
const [scope, resource] = resourceKey.split(':')
|
|
const match = PERMISSION_LIST.find(
|
|
(p) => p.scope === scope && p.resource === resource && p.action === action
|
|
)
|
|
return match ? [match.id as ScopedAccessTokenPermission] : []
|
|
}
|
|
|
|
// [kemal]: Not sure how efficient this will be, but it should get permissions from shared types and transform them whenever @supabase/shared-types updates.
|
|
export const getResourcePermissions = (
|
|
resourceKey: string
|
|
): Record<string, ScopedAccessTokenPermission[]> => {
|
|
const [scope, resource] = resourceKey.split(':')
|
|
const result: Record<string, ScopedAccessTokenPermission[]> = { 'no access': [] }
|
|
|
|
PERMISSION_LIST.filter((p) => p.scope === scope && p.resource === resource).forEach((p) => {
|
|
result[p.action] = [p.id as ScopedAccessTokenPermission]
|
|
})
|
|
|
|
if (result['read'] && result['write']) {
|
|
result['read-write'] = [...result['read'], ...result['write']]
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export const getRealAccess = (resource: string, tokenPermissions: string[]) => {
|
|
const resourcePermissions = getResourcePermissions(resource)
|
|
const actionTypes = ['read', 'write', 'create', 'delete'] as const
|
|
const grantedActions = actionTypes.filter((action) =>
|
|
resourcePermissions[action]?.some((p) => tokenPermissions.includes(p))
|
|
)
|
|
|
|
if (grantedActions.length === 0) {
|
|
return 'no access'
|
|
}
|
|
|
|
if (grantedActions.length === 1) {
|
|
return grantedActions[0]
|
|
}
|
|
|
|
if (
|
|
grantedActions.length === 2 &&
|
|
grantedActions[0] === 'read' &&
|
|
grantedActions[1] === 'write'
|
|
) {
|
|
return 'read-write'
|
|
}
|
|
|
|
return grantedActions.join('-')
|
|
}
|
|
|
|
export const formatAccessText = (action: string): string => {
|
|
switch (action) {
|
|
case 'no access':
|
|
return 'No access'
|
|
default:
|
|
return action
|
|
.split('-')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join('-')
|
|
}
|
|
}
|
|
|
|
export const getExpirationDate = (key: string): string | undefined => {
|
|
switch (key) {
|
|
case 'hour':
|
|
return dayjs().add(1, 'hours').toISOString()
|
|
case 'day':
|
|
return dayjs().add(1, 'day').toISOString()
|
|
case 'week':
|
|
return dayjs().add(7, 'days').toISOString()
|
|
case 'month':
|
|
return dayjs().add(30, 'days').toISOString()
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|