diff --git a/messages/de.json b/messages/de.json index ec7290fd26..fe5cba283d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Übertragung der App-Besitzrechte", "transfer-app-ownership-requirements": "Um eine App zwischen Organisationen zu übertragen, müssen Sie in beiden Quell- und Zielorganisationen Super-Admin-Berechtigungen haben. Dies gewährleistet eine sichere Übertragung des Eigentums und verhindert unbefugten Zugriff.", "transfer-app-ownership-too-soon": "Sie können Apps nur alle 32 Tage übertragen.", + "trial-banner-cta": "Pläne ansehen", + "trial-banner-message": "Gefällt Ihnen Ihre Capgo-Testversion? Abonnieren Sie einen Plan.", "trial-end-date": "Enddatum der Testphase", "trial-left": "verbleibende Tage", "trial-organizations-list": "Liste der Test-Organisationen", diff --git a/messages/en.json b/messages/en.json index fbb9b2d6d4..69f79bcd04 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1409,6 +1409,8 @@ "transfer-app-ownership": "Transfer app ownership", "transfer-app-ownership-requirements": "To transfer an app between organizations, you must have super_admin privileges in both the source and destination organizations. This ensures secure transfer of ownership and prevents unauthorized access.", "transfer-app-ownership-too-soon": "You can only transfer apps every 32 days", + "trial-banner-cta": "View plans", + "trial-banner-message": "Enjoying your Capgo trial? Subscribe to a plan.", "trial-end-date": "Trial End Date", "trial-left": "days left", "trial-organizations-list": "Trial Organizations List", diff --git a/messages/es.json b/messages/es.json index 818e811a02..d79a31e0d7 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Transferir la propiedad de la aplicación", "transfer-app-ownership-requirements": "Para transferir una aplicación entre organizaciones, debes tener privilegios de super_administrador tanto en la organización de origen como en la de destino. Esto garantiza una transferencia segura de propiedad y evita el acceso no autorizado.", "transfer-app-ownership-too-soon": "Solo puedes transferir aplicaciones cada 32 días.", + "trial-banner-cta": "Ver planes", + "trial-banner-message": "¿Disfrutas de la prueba de Capgo? Suscríbete a un plan.", "trial-end-date": "Fecha de fin de prueba", "trial-left": "días restantes", "trial-organizations-list": "Lista de organizaciones en prueba", diff --git a/messages/fr.json b/messages/fr.json index 32817220b3..fbbbed4dbb 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Transférer la propriété de l'application", "transfer-app-ownership-requirements": "Pour transférer une application entre organisations, vous devez avoir des privilèges de super_admin dans les deux organisations source et destination. Cela garantit un transfert de propriété sécurisé et empêche l'accès non autorisé.", "transfer-app-ownership-too-soon": "Vous ne pouvez transférer des applications que tous les 32 jours", + "trial-banner-cta": "Voir les plans", + "trial-banner-message": "Vous appréciez la période d'essai de Capgo ? Souscrivez à un plan.", "trial-end-date": "Date de fin d'essai", "trial-left": "jours restants", "trial-organizations-list": "Liste des organisations en essai", diff --git a/messages/hi.json b/messages/hi.json index e9d5d93574..04bbd82aff 100644 --- a/messages/hi.json +++ b/messages/hi.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "ऐप मालिकाना हस्तांतरण करें", "transfer-app-ownership-requirements": "एक ऐप को संगठनों के बीच स्थानांतरित करने के लिए, आपको स्रोत और गंतव्य संगठनों में super_admin विशेषाधिकार होना चाहिए। यह मालिकाना हक के सुरक्षित स्थानांतरण की सुनिश्चिति करता है और अनधिकृत पहुंच को रोकता है।", "transfer-app-ownership-too-soon": "आप केवल हर 32 दिनों में ऐप्स स्थानांतरित कर सकते हैं।", + "trial-banner-cta": "योजनाएँ देखें", + "trial-banner-message": "क्या आप अपने Capgo ट्रायल का आनंद ले रहे हैं? एक प्लान सब्सक्राइब करें।", "trial-end-date": "ट्रायल समाप्ति तिथि", "trial-left": "बचे हुए दिन", "trial-organizations-list": "ट्रायल संगठनों की सूची", diff --git a/messages/id.json b/messages/id.json index 153562b9c3..ba96e8c49d 100644 --- a/messages/id.json +++ b/messages/id.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Transfer kepemilikan aplikasi", "transfer-app-ownership-requirements": "Untuk mentransfer aplikasi antara organisasi, Anda harus memiliki hak istimewa super_admin di kedua organisasi sumber dan tujuan. Ini memastikan transfer kepemilikan yang aman dan mencegah akses tidak sah.", "transfer-app-ownership-too-soon": "Anda hanya dapat mentransfer aplikasi setiap 32 hari", + "trial-banner-cta": "Lihat paket", + "trial-banner-message": "Menikmati uji coba Capgo Anda? Berlangganan ke sebuah paket.", "trial-end-date": "Tanggal akhir uji coba", "trial-left": "sisa hari", "trial-organizations-list": "Daftar organisasi uji coba", diff --git a/messages/it.json b/messages/it.json index ba8c732c71..e3e2f9545c 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Trasferisci la proprietà dell'app", "transfer-app-ownership-requirements": "Per trasferire un'app tra organizzazioni, devi avere privilegi di super_admin sia nell'organizzazione di origine che in quella di destinazione. Questo garantisce un trasferimento sicuro della proprietà e previene l'accesso non autorizzato.", "transfer-app-ownership-too-soon": "Puoi trasferire le app solo ogni 32 giorni", + "trial-banner-cta": "Visualizza i piani", + "trial-banner-message": "Vi piace la prova di Capgo? Abbonatevi a un piano.", "trial-end-date": "Data fine prova", "trial-left": "giorni rimanenti", "trial-organizations-list": "Elenco organizzazioni in prova", diff --git a/messages/ja.json b/messages/ja.json index 27842f9dc8..4da85b38a4 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "アプリの所有権を譲渡する", "transfer-app-ownership-requirements": "アプリを組織間で転送するには、ソースと宛先の両方の組織でsuper_admin権限を持っている必要があります。これにより、所有権の安全な転送が保証され、不正なアクセスが防止されます。", "transfer-app-ownership-too-soon": "アプリの転送は32日ごとに1回のみ可能です", + "trial-banner-cta": "プランを見る", + "trial-banner-message": "Capgoのトライアルをお楽しみですか?プランにご加入ください。", "trial-end-date": "トライアル終了日", "trial-left": "残りの日数", "trial-organizations-list": "トライアル組織一覧", diff --git a/messages/ko.json b/messages/ko.json index 110fd33402..86d4df8948 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "앱 소유권 이전", "transfer-app-ownership-requirements": "앱을 조직 간에 전송하려면 소스 및 대상 조직에서 모두 super_admin 권한이 있어야 합니다. 이는 소유권의 안전한 전송을 보장하고 무단 접근을 방지합니다.", "transfer-app-ownership-too-soon": "앱을 32일마다 한 번만 이전할 수 있습니다.", + "trial-banner-cta": "요금제 보기", + "trial-banner-message": "Capgo 체험을 즐기고 계신가요? 요금제를 구독하세요.", "trial-end-date": "체험 종료일", "trial-left": "남은 날들", "trial-organizations-list": "체험 조직 목록", diff --git a/messages/pl.json b/messages/pl.json index 526a94fe51..a9a0e7df23 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Przenieś własność aplikacji", "transfer-app-ownership-requirements": "Aby przenieść aplikację między organizacjami, musisz mieć uprawnienia super_admina zarówno w organizacji źródłowej, jak i docelowej. Zapewnia to bezpieczne przeniesienie własności i zapobiega nieautoryzowanemu dostępowi.", "transfer-app-ownership-too-soon": "Możesz przenosić aplikacje tylko co 32 dni.", + "trial-banner-cta": "Wyświetl plany", + "trial-banner-message": "Korzystasz z okresu próbnego Capgo? Subskrybuj plan.", "trial-end-date": "Data zakończenia okresu próbnego", "trial-left": "pozostałe dni", "trial-organizations-list": "Lista organizacji w okresie próbnym", diff --git a/messages/pt-br.json b/messages/pt-br.json index 93c62594be..805a8fb791 100644 --- a/messages/pt-br.json +++ b/messages/pt-br.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Transferir propriedade do aplicativo", "transfer-app-ownership-requirements": "Para transferir um aplicativo entre organizações, você deve ter privilégios de super_administrador em ambas as organizações de origem e destino. Isso garante a transferência segura da propriedade e evita o acesso não autorizado.", "transfer-app-ownership-too-soon": "Você só pode transferir aplicativos a cada 32 dias.", + "trial-banner-cta": "Exibir planos", + "trial-banner-message": "Está gostando do teste do Capgo? Assine um plano.", "trial-end-date": "Data de término do teste", "trial-left": "dias restantes", "trial-organizations-list": "Lista de organizações em teste", diff --git a/messages/ru.json b/messages/ru.json index 2ada311f9f..530890936d 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Передача владения приложением", "transfer-app-ownership-requirements": "Для передачи приложения между организациями вы должны иметь привилегии супер_администратора как в исходной, так и в целевой организации. Это обеспечивает безопасную передачу прав собственности и предотвращает несанкционированный доступ.", "transfer-app-ownership-too-soon": "Вы можете переносить приложения только каждые 32 дня.", + "trial-banner-cta": "Просмотр планов", + "trial-banner-message": "Нравится пробная версия Capgo? Подпишитесь на тарифный план.", "trial-end-date": "Дата окончания пробного периода", "trial-left": "осталось дней", "trial-organizations-list": "Список организаций на пробном периоде", diff --git a/messages/tr.json b/messages/tr.json index 37c75af590..b76a536b68 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Uygulama sahipliğini aktar", "transfer-app-ownership-requirements": "Bir uygulamayı organizasyonlar arasında aktarmak için, hem kaynak hem de hedef organizasyonlarda super_admin ayrıcalıklarına sahip olmanız gerekir. Bu, mülkiyetin güvenli bir şekilde aktarılmasını sağlar ve yetkisiz erişimi önler.", "transfer-app-ownership-too-soon": "Yalnızca her 32 günde bir uygulamaları aktarabilirsiniz.", + "trial-banner-cta": "Planları görüntüle", + "trial-banner-message": "Capgo deneme sürümünüzü beğendiniz mi? Bir plana abone olun.", "trial-end-date": "Deneme Bitiş Tarihi", "trial-left": "kalan günler", "trial-organizations-list": "Deneme Organizasyonları Listesi", diff --git a/messages/vi.json b/messages/vi.json index df7f3153ef..79074be8ea 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "Chuyển quyền sở hữu ứng dụng", "transfer-app-ownership-requirements": "Để chuyển một ứng dụng giữa các tổ chức, bạn phải có quyền super_admin trong cả tổ chức nguồn và đích. Điều này đảm bảo việc chuyển giao sở hữu an toàn và ngăn chặn truy cập không được phép.", "transfer-app-ownership-too-soon": "Bạn chỉ có thể chuyển ứng dụng mỗi 32 ngày", + "trial-banner-cta": "Xem các gói", + "trial-banner-message": "Bạn đang trải nghiệm bản dùng thử Capgo? Hãy đăng ký gói dịch vụ.", "trial-end-date": "Ngày kết thúc dùng thử", "trial-left": "còn lại các ngày", "trial-organizations-list": "Danh sách tổ chức dùng thử", diff --git a/messages/zh-cn.json b/messages/zh-cn.json index 5f0e117da7..b630647b86 100644 --- a/messages/zh-cn.json +++ b/messages/zh-cn.json @@ -1393,6 +1393,8 @@ "transfer-app-ownership": "转让应用程序所有权", "transfer-app-ownership-requirements": "要在组织间传输应用程序,您必须在源组织和目标组织中都拥有 super_admin 权限。这可确保所有权的安全转移,并防止未经授权的访问。", "transfer-app-ownership-too-soon": "每 32 天只能传输一次应用程序", + "trial-banner-cta": "查看计划", + "trial-banner-message": "享受您的 Capgo 试用版?订阅计划。", "trial-end-date": "试用结束日期", "trial-left": "剩余天数", "trial-organizations-list": "试用组织列表", diff --git a/src/components.d.ts b/src/components.d.ts index 0ed5952053..1672586aab 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -76,6 +76,7 @@ declare module 'vue' { TabSidebar: typeof import('./components/TabSidebar.vue')['default'] Toast: typeof import('./components/Toast.vue')['default'] Toggle: typeof import('./components/Toggle.vue')['default'] + TrialBanner: typeof import('./components/dashboard/TrialBanner.vue')['default'] UpdateStatsCard: typeof import('./components/dashboard/UpdateStatsCard.vue')['default'] UpdateStatsChart: typeof import('./components/dashboard/UpdateStatsChart.vue')['default'] Usage: typeof import('./components/dashboard/Usage.vue')['default'] @@ -152,6 +153,7 @@ declare global { const TabSidebar: typeof import('./components/TabSidebar.vue')['default'] const Toast: typeof import('./components/Toast.vue')['default'] const Toggle: typeof import('./components/Toggle.vue')['default'] + const TrialBanner: typeof import('./components/dashboard/TrialBanner.vue')['default'] const UpdateStatsCard: typeof import('./components/dashboard/UpdateStatsCard.vue')['default'] const UpdateStatsChart: typeof import('./components/dashboard/UpdateStatsChart.vue')['default'] const Usage: typeof import('./components/dashboard/Usage.vue')['default'] diff --git a/src/components/dashboard/TrialBanner.vue b/src/components/dashboard/TrialBanner.vue new file mode 100644 index 0000000000..a0971dd7d8 --- /dev/null +++ b/src/components/dashboard/TrialBanner.vue @@ -0,0 +1,423 @@ + + + + + diff --git a/src/pages/dashboard.vue b/src/pages/dashboard.vue index 31115ece57..df39e5845b 100644 --- a/src/pages/dashboard.vue +++ b/src/pages/dashboard.vue @@ -90,6 +90,9 @@ displayStore.defaultBack = '/apps' + + +
diff --git a/src/types/supabase.types.ts b/src/types/supabase.types.ts index 020bedf1a3..b26b705db2 100644 --- a/src/types/supabase.types.ts +++ b/src/types/supabase.types.ts @@ -3204,6 +3204,7 @@ export type Database = { "2fa_has_access": boolean app_count: number can_use_more: boolean + created_at: string created_by: string credit_available: number credit_next_expiration: string @@ -3238,6 +3239,7 @@ export type Database = { "2fa_has_access": boolean app_count: number can_use_more: boolean + created_at: string created_by: string credit_available: number credit_next_expiration: string diff --git a/supabase/functions/_backend/utils/supabase.types.ts b/supabase/functions/_backend/utils/supabase.types.ts index 020bedf1a3..b26b705db2 100644 --- a/supabase/functions/_backend/utils/supabase.types.ts +++ b/supabase/functions/_backend/utils/supabase.types.ts @@ -3204,6 +3204,7 @@ export type Database = { "2fa_has_access": boolean app_count: number can_use_more: boolean + created_at: string created_by: string credit_available: number credit_next_expiration: string @@ -3238,6 +3239,7 @@ export type Database = { "2fa_has_access": boolean app_count: number can_use_more: boolean + created_at: string created_by: string credit_available: number credit_next_expiration: string diff --git a/supabase/migrations/20260224120000_add_created_at_to_get_orgs_v7.sql b/supabase/migrations/20260224120000_add_created_at_to_get_orgs_v7.sql new file mode 100644 index 0000000000..1a2a0424a5 --- /dev/null +++ b/supabase/migrations/20260224120000_add_created_at_to_get_orgs_v7.sql @@ -0,0 +1,329 @@ +-- Add org created_at to get_orgs_v7 return type +-- The frontend TrialBanner needs the real org creation time to gate display. +-- Previously it used subscription_start which is the billing-cycle anchor (bc.cycle_start), +-- NOT the account creation time, causing the 3-hour check to pass immediately for new trial orgs. + +-- Drop both overloads of get_orgs_v7 (with and without parameters) +DROP FUNCTION IF EXISTS public.get_orgs_v7(); +DROP FUNCTION IF EXISTS public.get_orgs_v7(uuid); + +-- Recreate get_orgs_v7(userid) with created_at added to the return type. +-- Based on prod.sql (the canonical schema) — only change is the new created_at column. +CREATE FUNCTION public.get_orgs_v7(userid uuid) +RETURNS TABLE ( + gid uuid, + created_by uuid, + created_at timestamptz, + logo text, + name text, + role character varying, + paying boolean, + trial_left integer, + can_use_more boolean, + is_canceled boolean, + app_count bigint, + subscription_start timestamptz, + subscription_end timestamptz, + management_email text, + is_yearly boolean, + stats_updated_at timestamp without time zone, + next_stats_update_at timestamptz, + credit_available numeric, + credit_total numeric, + credit_next_expiration timestamptz, + enforcing_2fa boolean, + "2fa_has_access" boolean, + enforce_hashed_api_keys boolean, + password_policy_config jsonb, + password_has_access boolean, + require_apikey_expiration boolean, + max_apikey_expiration_days integer, + enforce_encrypted_bundles boolean, + required_encryption_key character varying, + use_new_rbac boolean +) LANGUAGE plpgsql SECURITY DEFINER +SET search_path = '' AS $$ +BEGIN + RETURN QUERY + WITH app_counts AS ( + SELECT owner_org, COUNT(*) as cnt + FROM public.apps + GROUP BY owner_org + ), + rbac_roles AS ( + SELECT rb.org_id, r.name, r.priority_rank + FROM public.role_bindings rb + JOIN public.roles r ON rb.role_id = r.id + WHERE rb.principal_type = public.rbac_principal_user() + AND rb.principal_id = userid + AND rb.scope_type = public.rbac_scope_org() + AND rb.org_id IS NOT NULL + AND (rb.expires_at IS NULL OR rb.expires_at > now()) + UNION ALL + SELECT rb.org_id, r.name, r.priority_rank + FROM public.role_bindings rb + JOIN public.group_members gm ON gm.group_id = rb.principal_id + JOIN public.roles r ON rb.role_id = r.id + WHERE rb.principal_type = public.rbac_principal_group() + AND gm.user_id = userid + AND rb.scope_type = public.rbac_scope_org() + AND rb.org_id IS NOT NULL + AND (rb.expires_at IS NULL OR rb.expires_at > now()) + ), + rbac_org_roles AS ( + SELECT org_id, (ARRAY_AGG(rbac_roles.name ORDER BY rbac_roles.priority_rank DESC))[1] AS role_name + FROM rbac_roles + GROUP BY org_id + ), + user_orgs AS ( + SELECT ou.org_id + FROM public.org_users ou + WHERE ou.user_id = userid + UNION + SELECT rbac_org_roles.org_id + FROM rbac_org_roles + ), + -- Compute next stats update info for all paying orgs at once + paying_orgs_ordered AS ( + SELECT + o.id, + ROW_NUMBER() OVER (ORDER BY o.id ASC) - 1 as preceding_count + FROM public.orgs o + JOIN public.stripe_info si ON o.customer_id = si.customer_id + WHERE ( + (si.status = 'succeeded' + AND (si.canceled_at IS NULL OR si.canceled_at > NOW()) + AND si.subscription_anchor_end > NOW()) + OR si.trial_at > NOW() + ) + ), + -- Calculate current billing cycle for each org + billing_cycles AS ( + SELECT + o.id AS org_id, + CASE + WHEN COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + > NOW() - date_trunc('MONTH', NOW()) + THEN date_trunc('MONTH', NOW() - INTERVAL '1 MONTH') + + COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + ELSE date_trunc('MONTH', NOW()) + + COALESCE(si.subscription_anchor_start - date_trunc('MONTH', si.subscription_anchor_start), '0 DAYS'::INTERVAL) + END AS cycle_start + FROM public.orgs o + LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id + ), + -- Calculate 2FA access status for user/org combinations + two_fa_access AS ( + SELECT + o.id AS org_id, + o.enforcing_2fa, + CASE + WHEN o.enforcing_2fa = false THEN true + ELSE public.has_2fa_enabled(userid) + END AS "2fa_has_access", + (o.enforcing_2fa = true AND NOT public.has_2fa_enabled(userid)) AS should_redact_2fa + FROM public.orgs o + JOIN user_orgs uo ON uo.org_id = o.id + ), + -- Calculate password policy access status for user/org combinations + password_policy_access AS ( + SELECT + o.id AS org_id, + o.password_policy_config, + public.user_meets_password_policy(userid, o.id) AS password_has_access, + NOT public.user_meets_password_policy(userid, o.id) AS should_redact_password + FROM public.orgs o + JOIN user_orgs uo ON uo.org_id = o.id + ) + SELECT + o.id AS gid, + o.created_by, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::timestamptz + ELSE o.created_at + END AS created_at, + o.logo, + o.name, + CASE + WHEN o.use_new_rbac AND ou.user_right::text LIKE 'invite_%' THEN ou.user_right::varchar + WHEN o.use_new_rbac THEN COALESCE(ror.role_name, ou.rbac_role_name, ou.user_right::varchar) + ELSE COALESCE(ou.user_right::varchar, ror.role_name) + END AS role, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN false + ELSE COALESCE(si.status = 'succeeded', false) + END AS paying, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN 0 + ELSE GREATEST(COALESCE((si.trial_at::date - NOW()::date), 0), 0)::integer + END AS trial_left, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN false + ELSE COALESCE((si.status = 'succeeded' AND si.is_good_plan = true) + OR (si.trial_at::date - NOW()::date > 0) + OR COALESCE(ucb.available_credits, 0) > 0, false) + END AS can_use_more, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN false + ELSE COALESCE(si.status = 'canceled', false) + END AS is_canceled, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN 0::bigint + ELSE COALESCE(ac.cnt, 0) + END AS app_count, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::timestamptz + ELSE bc.cycle_start + END AS subscription_start, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::timestamptz + ELSE (bc.cycle_start + INTERVAL '1 MONTH') + END AS subscription_end, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::text + ELSE o.management_email + END AS management_email, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN false + ELSE COALESCE(si.price_id = p.price_y_id, false) + END AS is_yearly, + o.stats_updated_at, + CASE + WHEN poo.id IS NOT NULL THEN + public.get_next_cron_time('0 3 * * *', NOW()) + make_interval(mins => poo.preceding_count::int * 4) + ELSE NULL + END AS next_stats_update_at, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::numeric + ELSE COALESCE(ucb.available_credits, 0) + END AS credit_available, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::numeric + ELSE COALESCE(ucb.total_credits, 0) + END AS credit_total, + CASE + WHEN tfa.should_redact_2fa OR ppa.should_redact_password THEN NULL::timestamptz + ELSE ucb.next_expiration + END AS credit_next_expiration, + tfa.enforcing_2fa, + tfa."2fa_has_access", + o.enforce_hashed_api_keys, + ppa.password_policy_config, + ppa.password_has_access, + o.require_apikey_expiration, + o.max_apikey_expiration_days, + o.enforce_encrypted_bundles, + o.required_encryption_key, + o.use_new_rbac + FROM public.orgs o + JOIN user_orgs uo ON uo.org_id = o.id + LEFT JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id + LEFT JOIN rbac_org_roles ror ON ror.org_id = o.id + LEFT JOIN two_fa_access tfa ON tfa.org_id = o.id + LEFT JOIN password_policy_access ppa ON ppa.org_id = o.id + LEFT JOIN public.stripe_info si ON o.customer_id = si.customer_id + LEFT JOIN public.plans p ON si.product_id = p.stripe_id + LEFT JOIN app_counts ac ON ac.owner_org = o.id + LEFT JOIN public.usage_credit_balances ucb ON ucb.org_id = o.id + LEFT JOIN paying_orgs_ordered poo ON poo.id = o.id + LEFT JOIN billing_cycles bc ON bc.org_id = o.id; +END; +$$; + +ALTER FUNCTION public.get_orgs_v7(uuid) OWNER TO "postgres"; + +-- Revoke from public roles (security: prevents users from querying other users' orgs) +REVOKE ALL ON FUNCTION public.get_orgs_v7(uuid) FROM public; +REVOKE ALL ON FUNCTION public.get_orgs_v7(uuid) FROM anon; +REVOKE ALL ON FUNCTION public.get_orgs_v7(uuid) FROM authenticated; + +-- Grant only to postgres and service_role (private function) +GRANT EXECUTE ON FUNCTION public.get_orgs_v7(uuid) TO postgres; +GRANT EXECUTE ON FUNCTION public.get_orgs_v7(uuid) TO service_role; + +-- Recreate the get_orgs_v7() wrapper with created_at in the return type +CREATE OR REPLACE FUNCTION public.get_orgs_v7() +RETURNS TABLE ( + gid uuid, + created_by uuid, + created_at timestamptz, + logo text, + name text, + role character varying, + paying boolean, + trial_left integer, + can_use_more boolean, + is_canceled boolean, + app_count bigint, + subscription_start timestamptz, + subscription_end timestamptz, + management_email text, + is_yearly boolean, + stats_updated_at timestamp without time zone, + next_stats_update_at timestamptz, + credit_available numeric, + credit_total numeric, + credit_next_expiration timestamptz, + enforcing_2fa boolean, + "2fa_has_access" boolean, + enforce_hashed_api_keys boolean, + password_policy_config jsonb, + password_has_access boolean, + require_apikey_expiration boolean, + max_apikey_expiration_days integer, + enforce_encrypted_bundles boolean, + required_encryption_key character varying, + use_new_rbac boolean +) LANGUAGE plpgsql +SET search_path = '' SECURITY DEFINER AS $$ +DECLARE + api_key_text text; + api_key record; + user_id uuid; +BEGIN + SELECT public.get_apikey_header() INTO api_key_text; + user_id := NULL; + + IF api_key_text IS NOT NULL THEN + SELECT * FROM public.find_apikey_by_value(api_key_text) INTO api_key; + + IF api_key IS NULL THEN + PERFORM public.pg_log('deny: INVALID_API_KEY', jsonb_build_object('source', 'header')); + RAISE EXCEPTION 'Invalid API key provided'; + END IF; + + -- Check if API key is expired + IF public.is_apikey_expired(api_key.expires_at) THEN + PERFORM public.pg_log('deny: API_KEY_EXPIRED', jsonb_build_object('key_id', api_key.id)); + RAISE EXCEPTION 'API key has expired'; + END IF; + + user_id := api_key.user_id; + + IF COALESCE(array_length(api_key.limited_to_orgs, 1), 0) > 0 THEN + RETURN QUERY + SELECT orgs.* + FROM public.get_orgs_v7(user_id) AS orgs + WHERE orgs.gid = ANY(api_key.limited_to_orgs::uuid[]); + RETURN; + END IF; + END IF; + + IF user_id IS NULL THEN + SELECT public.get_identity() INTO user_id; + + IF user_id IS NULL THEN + PERFORM public.pg_log('deny: UNAUTHENTICATED', '{}'::jsonb); + RAISE EXCEPTION 'No authentication provided - API key or valid session required'; + END IF; + END IF; + + RETURN QUERY SELECT * FROM public.get_orgs_v7(user_id); +END; +$$; + +ALTER FUNCTION public.get_orgs_v7() OWNER TO "postgres"; + +GRANT ALL ON FUNCTION public.get_orgs_v7() TO anon; +GRANT ALL ON FUNCTION public.get_orgs_v7() TO authenticated; +GRANT ALL ON FUNCTION public.get_orgs_v7() TO service_role;