Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7618e97
feat(sso): consolidate SSO SAML schema into single migration
Jan 7, 2026
ce2d0ce
fix: rename SSO migration to avoid version conflict in CI
jokabuyasina Jan 7, 2026
2882d81
fix: restore test users in seed.sql required by CLI tests
jokabuyasina Jan 7, 2026
1690800
fix: use LENGTH() instead of empty string comparison in SQL
jokabuyasina Jan 7, 2026
c13d075
fix: replace remaining empty string comparison in validation trigger
jokabuyasina Jan 7, 2026
673342e
security: add authorization checks and fix auto-join logic
jokabuyasina Jan 8, 2026
ae73bc7
docs: fix migration filename in SSO_PR_SPLIT_PLAN.md
jokabuyasina Jan 8, 2026
95bd3e7
fix: use correct foreign key for auto_join_enabled check
jokabuyasina Jan 8, 2026
3dd3de7
fix: remove ineffective auth.identities check and restrict trigger gr…
jokabuyasina Jan 8, 2026
57be01f
feat: add PII compliance for SSO audit logs
jokabuyasina Jan 8, 2026
6df041d
chore: remove SSO_PR_SPLIT_PLAN.md planning document
jokabuyasina Jan 8, 2026
ab598c2
fix: resolve TypeScript errors in UsageCard, BundlePreviewFrame, and …
jokabuyasina Jan 8, 2026
287624f
Merge branch 'main' into feature/sso-01-schema
jokabuyasina Jan 8, 2026
33649c0
refactor(sso): move audit cleanup from cron_tasks to process_all_cron…
jokabuyasina Jan 8, 2026
7edede7
fix(sso): add SECURITY DEFINER to validate_sso_configuration trigger
jokabuyasina Jan 8, 2026
d3de7e3
fix(sso): refactor auth checks and improve UsageCard safety
jokabuyasina Jan 9, 2026
b98773b
fix: add SSO table schemas to postgres_schema.ts and fix UsageCard co…
jokabuyasina Jan 9, 2026
068d732
fix: add missing newline at end of postgres_schema.ts
jokabuyasina Jan 9, 2026
1ac6987
fix: add forceDemo prop to UsageCard for payment failed state
jokabuyasina Jan 9, 2026
3c2e3ae
Merge branch 'main' into feature/sso-01-schema
jokabuyasina Jan 9, 2026
9a13d4e
fix: correct dataArray type to (number | undefined)[] for accurate ty…
jokabuyasina Jan 9, 2026
5e60fb3
fix: add function whitelist to process_all_cron_tasks for security
jokabuyasina Jan 9, 2026
bb05d9a
fix: remove unused imports and computed in UsageCard
jokabuyasina Jan 9, 2026
c737de9
fix: resolve TypeScript errors in UsageCard and other components
jokabuyasina Jan 9, 2026
988303e
fix: remove unused @ts-expect-error directives
jokabuyasina Jan 9, 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
65 changes: 34 additions & 31 deletions src/components/dashboard/UsageCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
generateDemoStorageData,
getDemoDayCount,
} from '~/services/demoChartData'
import { useDashboardAppsStore } from '~/stores/dashboardApps'
import ChartCard from './ChartCard.vue'
import LineChartStats from './LineChartStats.vue'

Expand All @@ -33,7 +32,7 @@ const props = defineProps({
},
data: {
type: Array,
default: () => Array.from({ length: getDaysInCurrentMonth() }).fill(undefined) as number[],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to the requested feature do not modify

default: undefined,
},
dataByApp: {
type: Object,
Expand All @@ -54,6 +53,25 @@ const props = defineProps({
},
})

const dataArray = computed(() => {
if (!props.data || props.data.length === 0) {
return Array.from({ length: getDaysInCurrentMonth() }).fill(undefined) as (number | undefined)[]
}
return props.data as (number | undefined)[]
})

// Check if we have real data
const hasRealData = computed(() => {
const arr = dataArray.value ?? []
// Has data if there's at least one defined, non-zero value
const hasDefinedData = arr.some(val => val !== undefined && val !== null && val > 0)
// Or has data by app with at least one defined value
const hasAppData = props.dataByApp && Object.values(props.dataByApp).some((appValues: any) =>
appValues.some((val: any) => val !== undefined && val !== null && val > 0),
)
return hasDefinedData || hasAppData
})

// Get the appropriate data generator based on chart type
function getDataGenerator(title: string) {
const titleLower = title.toLowerCase()
Expand All @@ -72,7 +90,7 @@ function getDataGenerator(title: string) {
// Generate consistent demo data where total is derived from per-app breakdown
// Use existing data length or default based on billing period mode
const consistentDemoData = computed(() => {
const dataLength = (props.data as number[]).length
const dataLength = dataArray.value?.length ?? 0
const days = getDemoDayCount(props.useBillingPeriod, dataLength)
const generator = getDataGenerator(props.title)
return generateConsistentDemoData(days, generator)
Expand All @@ -82,34 +100,24 @@ const consistentDemoData = computed(() => {
const demoData = computed(() => consistentDemoData.value.total)
const demoDataByApp = computed(() => consistentDemoData.value.byApp)

// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const dashboardAppsStore = useDashboardAppsStore()
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})
const effectiveData = computed(() => isDemoMode.value ? demoData.value : props.data as number[])
// Use real data or demo data
const isDemoMode = computed(() => props.forceDemo || (!hasRealData.value && !props.isLoading))
const effectiveData = computed(() => isDemoMode.value ? demoData.value : dataArray.value)
const effectiveDataByApp = computed(() => isDemoMode.value ? demoDataByApp.value : props.dataByApp)
const effectiveAppNames = computed(() => isDemoMode.value ? DEMO_APP_NAMES : props.appNames)

const total = computed(() => {
const dataArray = effectiveData.value
const hasData = dataArray.some(val => val !== undefined)
const sumValues = (values: number[]) => values.reduce((acc, val) => (typeof val === 'number' ? acc + val : acc), 0)
const arr = effectiveData.value
const hasData = arr.some(val => val !== undefined)
const sumValues = (values: (number | undefined)[]): number => values.reduce<number>((acc, val) => (typeof val === 'number' ? acc + val : acc), 0)

if (hasData) {
return sumValues(dataArray)
return sumValues(arr)
}

if (effectiveDataByApp.value && Object.keys(effectiveDataByApp.value).length > 0) {
return Object.values(effectiveDataByApp.value).reduce((totalSum, appValues: any) => {
return totalSum + sumValues(appValues)
return Object.values(effectiveDataByApp.value).reduce((totalSum: number, appValues: any) => {
return totalSum + sumValues(appValues as (number | undefined)[])
}, 0)
}

Expand All @@ -118,10 +126,10 @@ const total = computed(() => {

const lastDayEvolution = computed(() => {
if (isDemoMode.value) {
return calculateDemoEvolution(effectiveData.value)
return calculateDemoEvolution(effectiveData.value.filter((v): v is number => typeof v === 'number'))
}

const arr = props.data as number[]
const arr = dataArray.value ?? []
const arrWithoutUndefined = arr.filter((val: any) => val !== undefined)

if (arrWithoutUndefined.length < 2) {
Expand All @@ -138,14 +146,9 @@ const lastDayEvolution = computed(() => {
return ((lastValue - previousValue) / previousValue) * 100
})

// Check if there's actual chart data (values in the array), not just a total
// This handles cases like Storage where total can be > 0 but no activity in current period
const hasChartData = computed(() => {
if (isDemoMode.value)
return true
const dataArray = effectiveData.value
// Check if any value in the array is defined and > 0
return dataArray.some(val => typeof val === 'number' && val > 0)
const arr = effectiveData.value
return arr.some(val => val !== undefined && val !== null && val > 0)
})
</script>

Expand Down
50 changes: 50 additions & 0 deletions supabase/functions/_backend/utils/postgres_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,53 @@ export const org_users = pgTable('org_users', {
channel_id: bigint('channel_id', { mode: 'number' }),
user_right: userMinRightPgEnum('user_right'),
})
export const org_saml_connections = pgTable('org_saml_connections', {
id: uuid('id').primaryKey().notNull(),
org_id: uuid('org_id').notNull(),
sso_provider_id: uuid('sso_provider_id').notNull().unique(),
provider_name: text('provider_name').notNull(),
metadata_url: text('metadata_url'),
metadata_xml: text('metadata_xml'),
entity_id: text('entity_id').notNull(),
current_certificate: text('current_certificate'),
certificate_expires_at: timestamp('certificate_expires_at', { withTimezone: true }),
certificate_last_checked: timestamp('certificate_last_checked', { withTimezone: true }).defaultNow(),
enabled: boolean('enabled').notNull().default(false),
verified: boolean('verified').notNull().default(false),
auto_join_enabled: boolean('auto_join_enabled').notNull().default(false),
attribute_mapping: text('attribute_mapping').default('{}'),
created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
created_by: uuid('created_by'),
})

export const saml_domain_mappings = pgTable('saml_domain_mappings', {
id: uuid('id').primaryKey().notNull(),
domain: text('domain').notNull(),
org_id: uuid('org_id').notNull(),
sso_connection_id: uuid('sso_connection_id').notNull(),
priority: integer('priority').notNull().default(0),
verified: boolean('verified').notNull().default(true),
verification_code: text('verification_code'),
verified_at: timestamp('verified_at', { withTimezone: true }),
created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})

export const sso_audit_logs = pgTable('sso_audit_logs', {
id: uuid('id').primaryKey().notNull(),
timestamp: timestamp('timestamp', { withTimezone: true }).notNull().defaultNow(),
user_id: uuid('user_id'),
email: text('email'),
event_type: text('event_type').notNull(),
org_id: uuid('org_id'),
sso_provider_id: uuid('sso_provider_id'),
sso_connection_id: uuid('sso_connection_id'),
ip_address: text('ip_address'),
user_agent: text('user_agent'),
country: text('country'),
saml_assertion_id: text('saml_assertion_id'),
saml_session_index: text('saml_session_index'),
error_code: text('error_code'),
error_message: text('error_message'),
metadata: text('metadata').default('{}'),
})
Loading