Skip to content

Better account delete#1191

Merged
riderx merged 16 commits into
mainfrom
better_account_delete
Sep 9, 2025
Merged

Better account delete#1191
riderx merged 16 commits into
mainfrom
better_account_delete

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented Sep 8, 2025

Summary by CodeRabbit

  • New Features

    • Account Disabled page with live countdown and sign-out; two-stage (30-day) account deletion, organization-aware warnings, paid-subscription notices, and stronger confirmation flows.
  • Localization

    • Added account-deletion/cancellation/warning translations across EN, DE, ES, FR, ID, IT, JA, KO, PL, PT-BR, RU, TR, VI, ZH‑CN.
  • UX Improvements

    • Safer table cell rendering (component-based renderers) and icon rendering as components (removes HTML-string rendering).
  • Documentation

    • Several error/help messages updated to include clickable support links.
  • Tests

    • Added tests for scheduled account deletion cleanup.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 8, 2025

Walkthrough

Adds a 30-day staged account-deletion workflow: DB migration, RPCs and types, cron cleanup and tests; client-side guard, account-disabled page with live countdown, org-aware deletion UX and Supabase helpers; many i18n key additions; table rendering refactor (VNode renderFunction); ESLint and dependency i18n swap; two global components declared.

Changes

Cohort / File(s) Summary
Localization additions
messages/*.json
messages/en.json, messages/de.json, messages/es.json, messages/fr.json, messages/id.json, messages/it.json, messages/ja.json, messages/ko.json, messages/pl.json, messages/pt-br.json, messages/ru.json, messages/tr.json, messages/vi.json, messages/zh-cn.json
Add ~17 account-deletion and confirmation keys per locale (e.g., account-deletion-final-warning, account-deletion-requested, account-deletion-restore, account-deletion-timer*, account-deletion-very-soon, i-am-sure, understand-and-continue, org/subscription warnings, cancel-subscriptions-and-continue); en.json includes anchor/HTML variants.
DB migration & scheduled cleanup
supabase/migrations/20250906094709_better_account_delete.sql
New public.to_delete_accounts table, RLS policy, indexes/grants; add delete_user(), is_account_disabled(user_id), get_account_removal_date(user_id), delete_accounts_marked_for_deletion() functions; schedule cron job for periodic cleanup; store removed_data.
DB tests
supabase/tests/29_test_delete_accounts_marked_for_deletion.sql
New TAP-style test suite for delete_accounts_marked_for_deletion(), with helper seeding, permission checks, future/past removal_date behavior, multi-delete/mixed scenarios, and error cases.
Supabase types / GraphQL surface
src/types/supabase.types.ts
Add graphql_public surface; typed public.to_delete_accounts; add get_account_removal_date and is_account_disabled function types; numerous table/function signature updates (e.g., plans.version, devices id removal, deleted_account field tightening, many RPC arg/return changes).
Client: auth guard, account-disabled page & route types
src/modules/auth.ts, src/pages/accountDisabled.vue, src/typed-router.d.ts
Auth guard calls is_account_disabled(user_id) and redirects to /accountDisabled; new /accountDisabled page fetches removal date via get_account_removal_date, shows live countdown/restore link and sign-out; route typing for /accountDisabled added.
Account deletion UX (org-aware) & Supabase helpers
src/pages/settings/Account.vue, src/services/supabase.ts
Account.vue computes impacted orgs (sole super_admin), checks isPayingOrg / getCurrentPlanNameOrg, surfaces org/paid-subscription warnings, and invokes delete_user()/performAccountDeletion; supabase.ts exposes isPayingOrg and getCurrentPlanNameOrg.
Table rendering refactor / API
src/components/comp_def.ts, src/components/Table.vue, src/components/TableLog.vue, src/components/tables/AppTable.vue, src/pages/settings/organization/Members.vue
Add optional renderFunction to TableColumn; replace HTML-string and v-html cell/icon rendering with VNode-based renderFunction or <component :is="...">; add RenderCell helper and vnode-based avatar rendering.
Global component declarations
src/components.d.ts
Add global ambient declarations for LineChartStatsStorage and StoragePage.
i18n library swap & many import updates
package.json, src/modules/i18n.ts, vite.config.mts, many src/** files
Replace petite-vue-i18n with vue-i18n; update imports across numerous components/pages/services and the Vite plugin config.
ESLint config
eslint.config.js
Enable Vue linting (vue: true); add vue/no-v-html rule as error for **/*.vue with exceptions for src/components/Table.vue and src/components/TableLog.vue.
Trigger comment
supabase/functions/_backend/triggers/on_user_delete.ts
Added explanatory comments about on_user_delete running 30 days before deletion and cancelling org subscriptions; no functional change.
Table component fixes
src/components/Table.vue
Introduced RenderCell and select-all computed; observed template typo (pisSelectAllEnabled) that may require fix.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant Guard as Router Guard
  participant RPC as Supabase RPC
  participant Page as /accountDisabled

  U->>Guard: Navigate (authenticated)
  Guard->>RPC: is_account_disabled(user_id)
  RPC-->>Guard: true / false
  alt account disabled
    Guard-->>U: redirect /accountDisabled
    U->>Page: mount
    Page->>RPC: get_account_removal_date(user_id)
    RPC-->>Page: removal_date
    Page-->>U: render countdown & restore link
  else active
    Guard-->>U: continue navigation
  end
Loading
sequenceDiagram
  autonumber
  participant User as Settings User
  participant UI as Account.vue
  participant RPC as Supabase RPC
  participant Cron as Scheduler

  User->>UI: Click "Delete account"
  UI->>RPC: get_org_members(gid) / isPayingOrg(gid) / getCurrentPlanNameOrg(gid)
  RPC-->>UI: members, paying flag, plan name
  UI-->>User: show org & paid-subscription warnings
  User->>UI: Confirm ("i-am-sure")
  UI->>RPC: delete_user()  (inserts to_delete_accounts with removal_date = now + 30d)
  Note right of UI: account marked for deletion (30 days pending)
  Cron->>RPC: delete_accounts_marked_for_deletion() (periodic)
  RPC-->>Cron: permanently deletes ready accounts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Pre-merge checks (1 warning, 2 inconclusive)

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title “Better account delete” is short and does reference the account deletion improvements in this changeset, but its phrasing is vague and non-idiomatic and does not clearly convey the primary scope—such as the introduction of a two-stage deletion workflow, database migration, and new UI flows. Consider renaming the title to be more descriptive and grammatically clear, for example “Implement two-stage account deletion workflow” or “Improve account deletion process” to better reflect the main changes.
Description Check ❓ Inconclusive The pull request’s actual description content and its alignment with the repository’s required template are not available here, so it cannot be evaluated for completeness or structure against the expected sections. Please provide the current PR description and ensure it follows the repository’s description template by including all required sections—such as Summary, Motivation, Changes, Testing, and Related Issues—with the appropriate details filled in.

Poem

I nibble timestamps, tuck them neat, ⏳
Thirty days until the final beat.
I count your orgs and list each plan,
Softly wait, then clear the span.
Hop, confirm — "I am sure" — then off I fleet. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch better_account_delete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 26

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
supabase/functions/_backend/triggers/on_user_delete.ts (2)

63-68: Parallelize and harden cancelSubscription calls.

If one Stripe call throws, the whole job aborts; also, calls run sequentially. Make them concurrent and resilient.

Apply:

-    for (const org of orgs) {
-      // Cancel org subscriptions if they exist
-      if (org.customer_id) {
-        await cancelSubscription(c, org.customer_id)
-      }
-    }
+    await Promise.all(
+      orgs.map(async (org) => {
+        if (!org.customer_id) return
+        try {
+          await cancelSubscription(c, org.customer_id)
+        }
+        catch (err) {
+          cloudlog({
+            requestId: c.get('requestId'),
+            level: 'error',
+            message: 'cancelSubscription failed',
+            org_id: org.id,
+            customer_id: org.customer_id,
+            error: String(err),
+          })
+        }
+      }),
+    )

19-24: Handle Supabase errors explicitly (avoid silent success paths).

Current code ignores error and proceeds. Fail fast with logs to avoid partial work and confusing “success.”

Apply:

-  const { data: userSuperAdminOrgs } = await supabaseAdmin(c)
+  const { data: userSuperAdminOrgs, error: userOrgsError } = await supabaseAdmin(c)
     .from('org_users')
     .select('org_id')
     .eq('user_id', record.id)
     .eq('user_right', 'super_admin')

-  if (!userSuperAdminOrgs?.length) {
+  if (userOrgsError) {
+    cloudlog({ requestId: c.get('requestId'), level: 'error', message: 'fetch user super admin orgs failed', error: userOrgsError.message })
+    return c.json(BRES)
+  }
+  if (!userSuperAdminOrgs?.length) {
     return c.json(BRES)
   }

   // For each org where user is super admin, check if they are the only one
   const orgIds = userSuperAdminOrgs.map(org => org.org_id)
-  const { data: superAdminCounts } = await supabaseAdmin(c)
+  const { data: superAdminCounts, error: countsError } = await supabaseAdmin(c)
     .from('org_users')
     .select('org_id')
     .in('org_id', orgIds)
     .eq('user_right', 'super_admin')

-  if (!superAdminCounts) {
+  if (countsError) {
+    cloudlog({ requestId: c.get('requestId'), level: 'error', message: 'fetch super admin counts failed', error: countsError.message })
+    return c.json(BRES)
+  }
   // Count super admins per org

-  const { data: orgs } = await supabaseAdmin(c)
+  const { data: orgs, error: orgsError } = await supabaseAdmin(c)
     .from('orgs')
     .select('id, customer_id')
     .in('id', singleSuperAdminOrgs)

-  if (orgs && orgs.length > 0) {
+  if (orgsError) {
+    cloudlog({ requestId: c.get('requestId'), level: 'error', message: 'fetch orgs failed', error: orgsError.message })
+    return c.json(BRES)
+  }
+  if (orgs && orgs.length > 0) {

Also applies to: 31-37, 55-60

messages/ko.json (1)

2-2: $schema must be a URL only

The $schema value includes explanatory Korean text, which will break tooling (inlang schema expects a plain URL).

Apply this diff:

-"$schema": "https://inlang.com/schema/inlang-message-format\n\n위의 웹사이트 주소는 언어에 관계없이 동일하게 유지됩니다.",
+"$schema": "https://inlang.com/schema/inlang-message-format",
🧹 Nitpick comments (40)
src/components.d.ts (1)

39-39: LGTM: new StoragePage declaration

Consistent naming and path. If this is a route view rather than a reusable component, consider housing it under a pages directory for organization; types here are unaffected either way.

messages/it.json (2)

22-23: Wording: differentiate “timer” vs “timer-in”.

Add “tra” for the “-in” variant to read naturally in Italian.

-  "account-deletion-timer-in": "Il tuo account cesserà di esistere",
+  "account-deletion-timer-in": "Il tuo account cesserà di esistere tra",

520-520: Minor phrasing improvement.

Make it explicit they’re paid subscriptions.

-  "paid-subscriptions-to-cancel": "Abbonamenti da cancellare",
+  "paid-subscriptions-to-cancel": "Abbonamenti a pagamento da annullare",
messages/de.json (5)

21-23: Timer strings likely insufficient for “in …” usage.

account-deletion-timer vs account-deletion-timer-in differ only by “in”. If UI injects a relative time, switch to ICU with a placeholder, e.g., Ihr Konto wird in {time} gelöscht. Please verify the consuming code.


155-155: German phrasing nit.

“Abonnements abbrechen und fahren Sie fort” → more idiomatic: “Abonnements kündigen und fortfahren”.

-"cancel-subscriptions-and-continue": "Abonnements abbrechen und fahren Sie fort",
+"cancel-subscriptions-and-continue": "Abonnements kündigen und fortfahren",

370-370: Style consistency nit.

Capitalize start: “Ich bin mir sicher”.

-"i-am-sure": "ich bin mir sicher",
+"i-am-sure": "Ich bin mir sicher",

516-521: Wording nit.

“Abonnements zu stornieren” → “Zu kündigende Abonnements”.

-"paid-subscriptions-to-cancel": "Abonnements zu stornieren",
+"paid-subscriptions-to-cancel": "Zu kündigende Abonnements",

730-734: Add rel on external links (policy).

If any warning body renders anchor tags, ensure rel="noopener noreferrer" on target="_blank" links globally.

messages/tr.json (3)

21-22: Timer copy needs placeholder.

Both keys read the same. Use ICU with {time} (e.g., “Hesabınız {time} sonra silinecek”). Verify UI.


155-155: Minor Turkish phrasing nit.

“... devam et” → “... devam edin” (formal) to match tone elsewhere.

-"cancel-subscriptions-and-continue": "Abonelikleri iptal edin ve devam et",
+"cancel-subscriptions-and-continue": "Abonelikleri iptal edin ve devam edin",

370-370: Capitalization nit.

“Eminim” should be capitalized.

-"i-am-sure": "eminim",
+"i-am-sure": "Eminim",
messages/ru.json (2)

21-22: Timer keys: add placeholder.

Use {time} (e.g., “Ваша учетная запись перестанет существовать через {time}”). Verify consumer.


370-370: Capitalization nit.

Start with uppercase: “Я уверен”.

-"i-am-sure": "я уверен",
+"i-am-sure": "Я уверен",
messages/fr.json (1)

21-22: Timer copy: add {time} placeholder.

Currently identical strings; prefer “Votre compte cessera d’exister dans {time}”. Verify usage.

messages/ja.json (1)

21-22: Timer keys: needs placeholder.

Use {time}: “あなたのアカウントは {time} 後に存在しなくなります”. Verify consumer.

messages/pt-br.json (2)

370-370: Capitalization nit.

“Estou certo” capitalized at start.

-"i-am-sure": "estou certo",
+"i-am-sure": "Estou certo",

21-22: Timer copy: add {time} placeholder.

Prefer “Sua conta deixará de existir em {time}”. Verify usage.

messages/id.json (1)

21-22: Timer copy: add {time} placeholder.

Prefer “Akun Anda akan berhenti ada dalam {time}”. Verify consumer.

messages/vi.json (3)

516-516: Clarify meaning (“to be deleted”).

-"organizations-to-be-deleted": "Các tổ chức bị xóa",
+"organizations-to-be-deleted": "Các tổ chức sẽ bị xóa",

520-520: Improve phrasing for list header.

-"paid-subscriptions-to-cancel": "Đăng ký bị hủy",
+"paid-subscriptions-to-cancel": "Các đăng ký cần hủy",

730-734: Polish subscription warning wording.

-"warning-paid-subscriptions": "Cảnh báo: Đăng ký hoạt động",
-"warning-paid-subscriptions-message": "Các tổ chức sau đây có đăng ký trả tiền tích cực. \nCác đăng ký này sẽ bị hủy ngay lập tức:",
+"warning-paid-subscriptions": "Cảnh báo: Đăng ký đang hoạt động",
+"warning-paid-subscriptions-message": "Các tổ chức sau đây có đăng ký trả phí đang hoạt động.\nCác đăng ký này sẽ bị hủy ngay lập tức:",
messages/zh-cn.json (3)

370-370: Use idiomatic confirmation.

-"i-am-sure": "我相信",
+"i-am-sure": "我确定",

731-734: Refine “active subscriptions” wording.

-"warning-paid-subscriptions": "警告:主动订阅",
-"warning-paid-subscriptions-message": "以下组织具有积极的付费订阅。\n这些订阅将立即取消:",
+"warning-paid-subscriptions": "警告:正在生效的订阅",
+"warning-paid-subscriptions-message": "以下组织有正在生效的付费订阅。\n这些订阅将立即取消:",

516-516: Smoother header phrasing.

-"organizations-to-be-deleted": "要删除的组织",
+"organizations-to-be-deleted": "待删除的组织",
src/types/supabase.types.ts (1)

1233-1264: New to_delete_accounts table: add ops safeguards.

Recommend a unique constraint on account_id and an index on removal_date to support scheduled cleanup. Confirm RLS policies restrict visibility to the owning user/admin jobs.

supabase/functions/_backend/triggers/on_user_delete.ts (1)

15-17: Implement the “timeout protection” mentioned in the comment.

Add a small timeout wrapper for external calls (e.g., Stripe) so the function doesn’t hang.

Example helper:

function withTimeout<T>(p: Promise<T>, ms = 15000): Promise<T> {
  return Promise.race([
    p,
    new Promise<never>((_, r) => setTimeout(() => r(new Error(`timeout ${ms}ms`)), ms)),
  ])
}

Then wrap: await withTimeout(cancelSubscription(c, org.customer_id)).

src/modules/auth.ts (2)

71-87: DRY the “is_account_disabled” RPC check; consider a short TTL cache.

Extract a helper (returns boolean and handles logging/redirect) and reuse in both branches; optionally cache the result for ~30–60s to avoid an RPC on every route change while still catching mid-session disables.

Example:

async function redirectIfDisabled(supabase: SupabaseClient, userId: string, next: NavigationGuardNext): Promise<boolean> {
  try {
    const { data: isDisabled, error } = await supabase.rpc('is_account_disabled', { user_id: userId })
    if (error) console.error('is_account_disabled failed', error)
    if (isDisabled) { hideLoader(); next('/accountDisabled'); return true }
  } catch (e) { console.error('is_account_disabled error', e) }
  return false
}

Also applies to: 120-142


116-119: Nit: path comparison should include the leading slash.

Current check likely never matches.

Apply:

-  else if (from.path !== 'login' && !auth.user) {
+  else if (from.path !== '/login' && !auth.user) {
src/pages/accountDisabled.vue (2)

51-55: Start the interval only when countdown is available.

Avoid work when there’s an error or no date.

Apply:

-  // Update current time every second
-  intervalId = setInterval(() => {
-    currentTime.value = new Date()
-  }, 1000)
+  // Update current time every second (only when we have a target date)
+  if (!error.value && deletionDate.value) {
+    intervalId = setInterval(() => {
+      currentTime.value = new Date()
+    }, 1000)
+  }

85-96: Localize the countdown units.

Hardcoded English strings bypass i18n and pluralization.

Example formatter:

function fmt(n: number, key: string) { return t(key, { count: n }) } // use i18n plural rules
// e.g., keys: day(s), hour(s), minute(s), second(s)

Then build the string from localized segments.

messages/pl.json (1)

520-520: Clarify that subscriptions are paid

Add “Płatne” to avoid ambiguity.

Apply this diff:

-"paid-subscriptions-to-cancel": "Subskrypcje do anulowania",
+"paid-subscriptions-to-cancel": "Płatne subskrypcje do anulowania",
messages/ko.json (6)

155-155: CTA phrasing: make it natural

Minor polish for a button label.

Apply this diff:

-"cancel-subscriptions-and-continue": "구독을 취소하고 계속하십시오",
+"cancel-subscriptions-and-continue": "구독을 취소하고 계속하기",

370-370: Make confirmation CTA natural Korean

Current text is stilted.

Apply this diff:

-"i-am-sure": "나는 확실하다",
+"i-am-sure": "확실합니다",

516-516: Grammar fix

Spacing.

Apply this diff:

-"organizations-to-be-deleted": "삭제 될 조직",
+"organizations-to-be-deleted": "삭제될 조직",

520-520: Clarify “paid subscriptions” and naturalize

“가입” is off here; use “유료 구독”.

Apply this diff:

-"paid-subscriptions-to-cancel": "취소 할 가입",
+"paid-subscriptions-to-cancel": "취소해야 할 유료 구독",

686-686: Naturalize proceed CTA

Apply this diff:

-"understand-and-continue": "나는 이해하고 계속하고 싶다",
+"understand-and-continue": "이해했으며 계속합니다",

730-734: Polish titles and punctuation

Korean style omits spaces before “:”.

Apply this diff:

-"warning-organizations-will-be-deleted": "경고 : 조직이 삭제됩니다",
-"warning-organizations-will-be-deleted-message": "귀하는 다음 조직에서 유일한 슈퍼 관리자입니다. \n계정이 제거되면 이러한 조직은 영구적으로 삭제됩니다.",
-"warning-paid-subscriptions": "경고 : 활성 구독",
-"warning-paid-subscriptions-message": "다음 조직에는 활발한 유료 구독이 있습니다. \n이 구독은 즉시 취소됩니다.",
+"warning-organizations-will-be-deleted": "경고: 조직이 삭제됩니다",
+"warning-organizations-will-be-deleted-message": "다음 조직의 유일한 슈퍼 관리자는 회원님입니다.\n계정을 삭제하면 이 조직들이 영구적으로 삭제됩니다.",
+"warning-paid-subscriptions": "경고: 활성 유료 구독",
+"warning-paid-subscriptions-message": "다음 조직에는 활성 유료 구독이 있습니다.\n이 구독들은 즉시 취소됩니다.",
src/pages/settings/Account.vue (1)

144-178: Subscription cancellation not implemented

There’s a TODO for canceling paid subscriptions. Suggest wiring a backend endpoint or Stripe webhook flow and blocking deletion until cancellations succeed.

I can draft a minimal cancellation flow (Supabase RPC + Stripe call) and optimistic UI with retries. Want me to open a follow-up issue and provide a PoC?

supabase/migrations/20250906094709_better_account_delete.sql (2)

106-157: Improve cron deletion for concurrency and large batches

  • Use FOR UPDATE SKIP LOCKED to avoid double-processing when overlapping runs occur.
  • Optionally limit batch size to keep each minute’s job bounded.
-  FOR account_record IN 
-    SELECT account_id, removal_date, removed_data
-    FROM public.to_delete_accounts 
-    WHERE removal_date < NOW()
+  FOR account_record IN 
+    SELECT account_id, removal_date, removed_data
+    FROM public.to_delete_accounts 
+    WHERE removal_date < NOW()
+    FOR UPDATE SKIP LOCKED
   LOOP

Optional batch cap:

-- BEFORE the loop:
-- PERFORM 1;  -- placeholder
-- Add: WITH cte AS (SELECT account_id FROM ... ORDER BY removal_date LIMIT 500 FOR UPDATE SKIP LOCKED) SELECT ... FROM to_delete_accounts JOIN cte USING(account_id)

166-172: Make cron.schedule idempotent and verify pg_cron availability

If this migration re-runs, the job name will collide. Wrap in a DO block to ignore existing-job errors and ensure the extension exists.

-SELECT cron.schedule(
-  'delete-expired-accounts',           -- job name
-  '* * * * *',                        -- cron expression (every minute)
-  'SELECT public.delete_accounts_marked_for_deletion();'  -- SQL command
-);
+DO $$
+BEGIN
+  PERFORM extversion FROM pg_extension WHERE extname = 'pg_cron';
+  IF NOT FOUND THEN
+    RAISE EXCEPTION 'pg_cron extension is not installed';
+  END IF;
+  BEGIN
+    PERFORM cron.schedule(
+      'delete-expired-accounts',
+      '* * * * *',
+      'SELECT public.delete_accounts_marked_for_deletion();'
+    );
+  EXCEPTION WHEN unique_violation THEN
+    -- job already exists; optionally adjust schedule/command via cron.alter_job
+    NULL;
+  END;
+END$$;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01eb2ea and cc291ec.

📒 Files selected for processing (22)
  • messages/de.json (6 hunks)
  • messages/en.json (4 hunks)
  • messages/es.json (6 hunks)
  • messages/fr.json (6 hunks)
  • messages/id.json (6 hunks)
  • messages/it.json (6 hunks)
  • messages/ja.json (6 hunks)
  • messages/ko.json (6 hunks)
  • messages/pl.json (6 hunks)
  • messages/pt-br.json (6 hunks)
  • messages/ru.json (6 hunks)
  • messages/tr.json (6 hunks)
  • messages/vi.json (6 hunks)
  • messages/zh-cn.json (6 hunks)
  • src/components.d.ts (2 hunks)
  • src/modules/auth.ts (2 hunks)
  • src/pages/accountDisabled.vue (1 hunks)
  • src/pages/settings/Account.vue (3 hunks)
  • src/types/supabase.types.ts (33 hunks)
  • supabase/functions/_backend/triggers/on_user_delete.ts (1 hunks)
  • supabase/migrations/20250530233128_base.sql (1 hunks)
  • supabase/migrations/20250906094709_better_account_delete.sql (1 hunks)
🔇 Additional comments (35)
supabase/migrations/20250530233128_base.sql (1)

184-185: Verified enum value casing—no mismatches All references use “InvalidIp” exactly; no callers use a different casing.

messages/vi.json (3)

155-155: LGTM: cancel-subscriptions-and-continue.


370-370: LGTM: i-am-sure.


686-686: LGTM: understand-and-continue.

messages/zh-cn.json (2)

155-155: LGTM: cancel-subscriptions-and-continue.


686-686: LGTM: understand-and-continue.

messages/en.json (1)

23-29: LGTM: new account deletion strings.

src/types/supabase.types.ts (7)

10-34: LGTM: graphql_public surface added.


1475-1478: RPCs wired into auth guards and disabled page
Verified get_account_removal_date in src/pages/accountDisabled.vue and is_account_disabled in src/modules/auth.ts.


1939-1945: read_ RPC callers use correct named args – no action needed.*

All calls to read_device_usage, read_bandwidth_usage, read_storage_usage, and read_version_usage already pass { p_app_id, p_period_start, p_period_end } per the updated types.


1011-1011: plans.version added: verify downstream usage
Check for any code assuming plan rows without version (e.g. object destructuring, serializers) and update accordingly. Applies to src/types/supabase.types.ts lines 1011, 1034, 1057. To locate potential issues, run:

#!/bin/bash
# Search for direct usage of plans.version
rg -nP "plans?\.version" -n -C2 -g "*.{ts,vue}"
# Search for plan object destructuring that might omit version
rg -nP "(?:const|let|var)\s+\{[^}]*\}\s*=\s*plan" -n -C2 -g "*.{ts,vue}"

1601-1610: get_org_members now returns uid and is_tmp

Update call sites to use the new fields:

  • src/pages/settings/Account.vue (line 61): switch from member.email to member.uid for unique keys/identifiers and handle member.is_tmp (e.g., show temporary-invite state).
  • src/stores/organization.ts (line 150): use member.uid instead of member.email and account for member.is_tmp.

1460-1463: Remove audit for exist_app_versions/get_app_versions – no TS call sites to update.

Likely an incorrect or invalid review comment.


581-583: No TS insert/upsert into deleted_account; SQL migrations/seeds include email—no action needed.

supabase/functions/_backend/triggers/on_user_delete.ts (1)

12-13: Confirm trigger timing vs event type (avoid double-cancellation). triggerValidator('users','DELETE') picks up the DB’s AFTER DELETE trigger on public.users (in addition to the PGMQ send at mark-for-deletion), so your cancel-subscriptions logic will run on final delete as well. Verify you’re only firing this endpoint once per user (e.g. use a distinct event type or drop the default trigger).

src/pages/accountDisabled.vue (1)

113-113: v-html needs trusted content only.

Ensure account-deletion-restore is maintained by us and contains no user input to avoid XSS.

messages/pl.json (6)

18-19: Account-deletion warnings: translation looks good

The Polish phrasing is clear and correctly conveys irreversibility.


155-155: CTA looks fine

Wording is clear and consistent with the flow.


370-370: Confirmation CTA is appropriate

Short and natural for a destructive confirm.


516-516: Label OK

Natural listing heading.


686-686: Proceed CTA looks good

Natural Polish phrasing.


730-734: Warning titles and message are clear

Good tone and structure for a destructive action.

messages/ko.json (1)

18-18: Final-warning text OK

Conveys irreversibility clearly.

src/pages/settings/Account.vue (9)

29-29: Store injection LGTM

Pattern consistent with other stores.


36-38: State for warnings LGTM

Appropriate shapes for teleport rendering.


110-114: Guard now works after refactor

Once canProceed reflects errors, this early-exit is useful.


115-143: Organization deletion warning dialog: good UX

Use of teleport to list orgs is clear.


695-709: Teleport org warning block LGTM

Good visual hierarchy and accessibility.


710-729: Teleport paid subscriptions warning block LGTM

Clear list with plan badges.


16-16: Supabase helpers verified and correctly exported
Both getCurrentPlanNameOrg and isPayingOrg are implemented and exported with proper typings in src/services/supabase.ts (lines 377–379 and 421–423).


20-20: Organization store API verified

useOrganizationStore (src/stores/organization.ts) exposes awaitInitialLoad() and returns organizations — Organization objects include gid, role and name.


200-249: Tighten special-case email check; confirmed isNativePlatform() method

  • Change to strict equality:
    if (user.email === 'review@capgo.app' && Capacitor.isNativePlatform())
  • Verified Capacitor.isNativePlatform() is a function in @capacitor/core v7.4.2 (capacitorjs.com, jsdocs.io)
supabase/migrations/20250906094709_better_account_delete.sql (3)

11-19: RLS deny-all is good; keep it as the default posture

Deny-all policy + SECURITY DEFINER functions is the right pattern here.


21-23: Confirm service_role bypasses RLS in your environment

Granting ALL to service_role assumes BYPASSRLS (Supabase default). If not, the SECURITY DEFINER function owner must be a role with access.


159-165: Good: lock down the scheduled deletion function to privileged roles

Revoke/Grant here is correct.

Comment thread messages/de.json
Comment thread messages/en.json Outdated
"account-password-heading": "Change my password",
"account-updated-succ": "Account updated successfully",
"account-deletion-requested": "You have requested your account to be deleted",
"account-deletion-restore": "Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> if you would like to restore it",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden external links opened in new tabs.

Add rel="noopener noreferrer" to avoid reverse‑tabnabbing; keep HTML in i18n minimal/safe.

-"account-deletion-restore": "Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> if you would like to restore it",
+"account-deletion-restore": "Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> if you would like to restore it",

-"cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>capgo support</a> and check browser logs",
+"cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>capgo support</a> and check browser logs",

-"cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+"cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

-"cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+"cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

-"cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",
+"cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",

-"cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",
+"cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",

-"error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+"error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

Also applies to: 190-191, 194-194, 197-197, 201-201, 349-349

🤖 Prompt for AI Agents
In messages/en.json around line 24 (and also apply to lines 190-191, 194, 197,
201, 349), the i18n strings include anchor tags that open links in a new tab but
lack rel="noopener noreferrer"; update each external <a ... target="_blank">
anchor to include rel="noopener noreferrer" to prevent reverse-tabnabbing and
ensure HTML remains minimal/safe (do this for the listed lines and any other
external-target="_blank" anchors in the file).

Comment thread messages/es.json Outdated
Comment thread messages/fr.json
Comment thread messages/id.json
Comment on lines +179 to 199
// Show final confirmation
dialogStore.openDialog({
title: t('are-u-sure'),
description: '', // We'll use Teleport for custom content
buttons: [
{
text: t('button-cancel'),
role: 'cancel',
handler: () => {
console.log('Cancel clicked')
},
},
{
text: t('button-remove'),
text: t('i-am-sure'),
role: 'danger',
handler: async () => {
if (!main.auth || main.auth?.email == null)
return
const supabaseClient = useSupabase()

const authUser = await supabase.auth.getUser()
if (authUser.error)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})

try {
const { data: user } = await supabaseClient
.from('users')
.select()
.eq('id', authUser.data.user.id)
.single()
if (!user)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})

if (user.email.endsWith('review@capgo.app') && Capacitor.isNativePlatform()) {
const { error: banErr } = await supabase
.from('users')
.update({ ban_time: dayjs().add(5, 'minutes').toDate().toISOString() })
.eq('id', user.id)

if (banErr) {
console.error('Cannot set ban duration', banErr)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}

await main.logout()
router.replace('/login')
return
}

// Delete user using RPC function
const { error: deleteError } = await supabase.rpc('delete_user')

if (deleteError) {
console.error('Delete error:', deleteError)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}

// Delete auth user
const { error: authError } = await supabase.auth.admin.deleteUser(authUser.data.user.id)
if (authError) {
console.error('Auth delete error:', authError)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}

await main.logout()
router.replace('/login')
}
catch (error) {
console.error(error)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}
await performAccountDeletion()
},
},
],
})
return dialogStore.onDialogDismiss()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Localize the final confirmation content and reuse the new i18n keys

Hardcoded English text in the final warning breaks localization and duplicates new keys.

Apply this diff:

   dialogStore.openDialog({
     title: t('are-u-sure'),
-    description: '', // We'll use Teleport for custom content
+    description: '', // Teleport content below uses i18n keys
@@
-        text: t('i-am-sure'),
+        text: t('i-am-sure'),
         role: 'danger',
         handler: async () => {
           await performAccountDeletion()
         },

And update the Teleport content:

-    <Teleport v-if="dialogStore.showDialog && dialogStore.dialogOptions?.title === t('are-u-sure')" to="#dialog-v2-content" defer>
-      <div class="text-base text-gray-500 dark:text-gray-400">
-        <p class="mb-4">
-          This action cannot be undone. Your account and all associated data will be permanently deleted.
-        </p>
-        <p class="font-medium text-gray-700 dark:text-gray-300">
-          Your account will be deleted after 30 days
-        </p>
-      </div>
-    </Teleport>
+    <Teleport v-if="dialogStore.showDialog && dialogStore.dialogOptions?.title === t('are-u-sure')" to="#dialog-v2-content" defer>
+      <div class="text-base text-gray-500 dark:text-gray-400">
+        <p class="mb-4" v-html="t('account-deletion-final-warning')" />
+        <p class="font-medium text-gray-700 dark:text-gray-300">
+          {{ t('account-deletion-timer') }} {{ t('account-deletion-timer-in') }} 30 days
+        </p>
+      </div>
+    </Teleport>

Note: if the backend exposes get_account_removal_date, consider rendering the exact date instead of “30 days”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Show final confirmation
dialogStore.openDialog({
title: t('are-u-sure'),
description: '', // We'll use Teleport for custom content
buttons: [
{
text: t('button-cancel'),
role: 'cancel',
handler: () => {
console.log('Cancel clicked')
},
},
{
text: t('button-remove'),
text: t('i-am-sure'),
role: 'danger',
handler: async () => {
if (!main.auth || main.auth?.email == null)
return
const supabaseClient = useSupabase()
const authUser = await supabase.auth.getUser()
if (authUser.error)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
try {
const { data: user } = await supabaseClient
.from('users')
.select()
.eq('id', authUser.data.user.id)
.single()
if (!user)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
if (user.email.endsWith('review@capgo.app') && Capacitor.isNativePlatform()) {
const { error: banErr } = await supabase
.from('users')
.update({ ban_time: dayjs().add(5, 'minutes').toDate().toISOString() })
.eq('id', user.id)
if (banErr) {
console.error('Cannot set ban duration', banErr)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}
await main.logout()
router.replace('/login')
return
}
// Delete user using RPC function
const { error: deleteError } = await supabase.rpc('delete_user')
if (deleteError) {
console.error('Delete error:', deleteError)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}
// Delete auth user
const { error: authError } = await supabase.auth.admin.deleteUser(authUser.data.user.id)
if (authError) {
console.error('Auth delete error:', authError)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}
await main.logout()
router.replace('/login')
}
catch (error) {
console.error(error)
return setErrors('update-account', [t('something-went-wrong-try-again-later')], {})
}
await performAccountDeletion()
},
},
],
})
return dialogStore.onDialogDismiss()
}
// Show final confirmation
dialogStore.openDialog({
title: t('are-u-sure'),
description: '', // Teleport content below uses i18n keys
buttons: [
{
text: t('button-cancel'),
role: 'cancel',
},
{
text: t('i-am-sure'),
role: 'danger',
handler: async () => {
await performAccountDeletion()
},
},
],
})
return dialogStore.onDialogDismiss()
}
<template>
<Teleport
v-if="dialogStore.showDialog && dialogStore.dialogOptions?.title === t('are-u-sure')"
to="#dialog-v2-content"
defer
>
<div class="text-base text-gray-500 dark:text-gray-400">
<p class="mb-4" v-html="t('account-deletion-final-warning')" />
<p class="font-medium text-gray-700 dark:text-gray-300">
{{ t('account-deletion-timer') }} {{ t('account-deletion-timer-in') }} 30 days
</p>
</div>
</Teleport>
</template>
🤖 Prompt for AI Agents
In src/pages/settings/Account.vue around lines 179 to 199, the final
confirmation dialog uses hardcoded English text and duplicates new i18n keys;
replace the empty description/Teleport fallback and any literal strings with the
existing i18n keys (e.g. use t('account-removal-title'),
t('account-removal-description') or the provided new keys) for title,
description and button labels, and remove duplicated literals; update the
Teleport-provided custom content to render the localized description (and use
the backend get_account_removal_date if available to show the exact date instead
of a hardcoded "30 days"); ensure the dialogStore.openDialog call passes the
localized description (or a localized placeholder) and the Teleport content
reads those same i18n keys so all text is localized and no English literals
remain.

Comment thread supabase/migrations/20250906094709_better_account_delete.sql
Comment on lines +25 to +40
CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Check if the user_id exists in the to_delete_accounts table
RETURN EXISTS (
SELECT 1
FROM public.to_delete_accounts
WHERE account_id = user_id
);
END;
$$;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent cross-user probing; restrict is_account_disabled to the caller (or service_role)

As written, any caller can check any user_id, leaking state. Gate by auth.uid() (except for service_role) and include auth in search_path.

-CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
-RETURNS BOOLEAN
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
+CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
+RETURNS BOOLEAN
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
 AS $$
 BEGIN
-    -- Check if the user_id exists in the to_delete_accounts table
+    -- Allow service_role to query any user; others only themselves
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+        RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     RETURN EXISTS (
         SELECT 1 
         FROM public.to_delete_accounts 
         WHERE account_id = user_id
     );
 END;
 $$;

Also lock down privileges:

REVOKE ALL ON FUNCTION public.is_account_disabled(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.is_account_disabled(uuid) TO authenticated, service_role;
🤖 Prompt for AI Agents
In supabase/migrations/20250906094709_better_account_delete.sql around lines
25-40, the is_account_disabled function currently allows any caller to probe
arbitrary user IDs; change it so only the authenticated user (auth.uid()) or the
service_role can check a given account and ensure auth is resolvable by adding
auth to the function's search_path. Concretely: update the function body to
validate that either the current caller is the service_role OR the provided
user_id equals auth.uid() (deny otherwise), and set SET search_path = auth,
public; then revoke public EXECUTE on the function and GRANT EXECUTE only to
authenticated and service_role roles as shown in the review comment.

Comment on lines +42 to +64
CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
RETURNS TIMESTAMPTZ
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
removal_date TIMESTAMPTZ;
BEGIN
-- Get the removal_date for the user_id
SELECT to_delete_accounts.removal_date INTO removal_date
FROM public.to_delete_accounts
WHERE account_id = user_id;

-- Throw exception if account is not in the table
IF removal_date IS NULL THEN
RAISE EXCEPTION 'Account with ID % is not marked for deletion', user_id;
END IF;

RETURN removal_date;
END;
$$;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same privacy gate for get_account_removal_date

Anyone can read any user’s removal_date. Restrict to self or service_role; add clearer error for non-owners.

-CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
-RETURNS TIMESTAMPTZ
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
+CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
+RETURNS TIMESTAMPTZ
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
 AS $$
 DECLARE
     removal_date TIMESTAMPTZ;
 BEGIN
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+        RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     SELECT to_delete_accounts.removal_date INTO removal_date
     FROM public.to_delete_accounts 
     WHERE account_id = user_id;
     
     IF removal_date IS NULL THEN
         RAISE EXCEPTION 'Account with ID % is not marked for deletion', user_id;
     END IF;
     
     RETURN removal_date;
 END;
 $$;

And restrict execution:

REVOKE ALL ON FUNCTION public.get_account_removal_date(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.get_account_removal_date(uuid) TO authenticated, service_role;
🤖 Prompt for AI Agents
In supabase/migrations/20250906094709_better_account_delete.sql around lines
42–64, update the get_account_removal_date function to enforce a privacy gate:
inside the function check the caller identity and allow access only if the
caller is the same user (compare the JWT sub/user id from request context) or if
the caller is the service_role; if neither, RAISE a clear permission error
(e.g., 'Permission denied: cannot view removal_date for another user'). Keep
SECURITY DEFINER but read caller identity using the request JWT claim retrieval
used in your codebase (e.g., request.jwt.claims or current_setting pattern) and
perform the ownership check before selecting/removal_date. After the function
definition add SQL to revoke public execute and grant execute only to
authenticated and service_role (REVOKE ALL ON FUNCTION ... FROM PUBLIC; GRANT
EXECUTE ON FUNCTION ... TO authenticated, service_role).

Comment on lines +66 to +104
CREATE OR REPLACE FUNCTION "public"."delete_user" () RETURNS "void" LANGUAGE "plpgsql"
SET
search_path = '' SECURITY DEFINER AS $$
DECLARE
user_id_fn uuid;
user_email text;
hashed_email text;
BEGIN
-- Get the current user ID and email
SELECT auth.uid() INTO user_id_fn;
SELECT email INTO user_email FROM auth.users WHERE id = user_id_fn;

-- Trigger the queue-based deletion process
-- This cancels the subscriptions of the user's organizations
PERFORM pgmq.send(
'on_user_delete'::text,
jsonb_build_object(
'user_id', user_id_fn,
'email', user_email
)
);

-- Mark the user for deletion
INSERT INTO public.to_delete_accounts (
account_id,
removal_date,
removed_data
) VALUES
(
user_id_fn,
NOW() + INTERVAL '30 days',
jsonb_build_object('email', user_email, 'apikeys', (SELECT jsonb_agg(to_jsonb(a.*)) FROM public.apikeys a WHERE a.user_id = user_id_fn))
);

-- Delete the API keys
DELETE FROM public.apikeys WHERE public.apikeys.user_id = user_id_fn;
END;
$$;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make delete_user authenticated-only, idempotent, and remove unused var; avoid storing sensitive key data

  • Add guard for unauthenticated calls.
  • Use ON CONFLICT with the (proposed) unique index to prevent duplicates and optionally extend removal_date.
  • Remove unused hashed_email.
  • Consider not embedding API key rows into removed_data; store minimal metadata only.
 CREATE OR REPLACE FUNCTION "public"."delete_user" () RETURNS "void" LANGUAGE "plpgsql"
 SET
   search_path = '' SECURITY DEFINER AS $$
 DECLARE
   user_id_fn uuid;
   user_email text;
-  hashed_email text;
 BEGIN
   -- Get the current user ID and email
   SELECT auth.uid() INTO user_id_fn;
+  IF user_id_fn IS NULL THEN
+    RAISE EXCEPTION 'authentication required' USING ERRCODE = '28000';
+  END IF;
   SELECT email INTO user_email FROM auth.users WHERE id = user_id_fn;
   
   -- Trigger the queue-based deletion process
   -- This cancels the subscriptions of the user's organizations
   PERFORM pgmq.send(
     'on_user_delete'::text,
     jsonb_build_object(
       'user_id', user_id_fn,
       'email', user_email
     )
   );
   
   -- Mark the user for deletion
   INSERT INTO public.to_delete_accounts (
     account_id, 
     removal_date, 
     removed_data
   ) VALUES 
   (
     user_id_fn, 
     NOW() + INTERVAL '30 days', 
-    jsonb_build_object('email', user_email, 'apikeys', (SELECT jsonb_agg(to_jsonb(a.*)) FROM public.apikeys a WHERE a.user_id = user_id_fn))
+    jsonb_build_object(
+      'email', user_email
+      -- Consider only lightweight metadata if needed: 'apikey_count', (SELECT count(*) FROM public.apikeys a WHERE a.user_id = user_id_fn)
+    )
-  );
+  )
+  ON CONFLICT (account_id) DO UPDATE
+    SET removal_date = GREATEST(EXCLUDED.removal_date, public.to_delete_accounts.removal_date);
 
   -- Delete the API keys
   DELETE FROM public.apikeys WHERE public.apikeys.user_id = user_id_fn;
 END;
 $$;

Lock execution to authenticated users:

REVOKE ALL ON FUNCTION public.delete_user() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.delete_user() TO authenticated, service_role;

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
messages/es.json (7)

18-23: Fill missing Spanish translations for account-deletion UI keys.

Empty values will render blank UI. Add clear translations and keep placeholders/HTML intact.

-  "account-deletion-final-warning": "",
-  "account-deletion-requested": "",
+  "account-deletion-final-warning": "Esta acción no se puede deshacer.\nTu cuenta y todos los datos asociados se eliminarán permanentemente.",
+  "account-deletion-requested": "Has solicitado la eliminación de tu cuenta",
   "account-deletion-restore": "Por favor contacta <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Soporte de Capgo</a> si deseas restaurarla",
-  "account-deletion-timer": "",
-  "account-deletion-timer-in": "",
-  "account-deletion-very-soon": "",
+  "account-deletion-timer": "Tu cuenta dejará de existir",
+  "account-deletion-timer-in": "Tu cuenta dejará de existir en",
+  "account-deletion-very-soon": "Muy pronto",

155-155: Fill translation: cancel-subscriptions-and-continue.

-  "cancel-subscriptions-and-continue": "",
+  "cancel-subscriptions-and-continue": "Cancelar suscripciones y continuar",

370-370: Fill translation: i-am-sure.

-  "i-am-sure": "",
+  "i-am-sure": "Estoy seguro",

516-516: Fill translation: organizations-to-be-deleted.

-  "organizations-to-be-deleted": "",
+  "organizations-to-be-deleted": "Organizaciones que se eliminarán",

520-520: Fill translation: paid-subscriptions-to-cancel.

-  "paid-subscriptions-to-cancel": "",
+  "paid-subscriptions-to-cancel": "Suscripciones de pago a cancelar",

686-686: Fill translation: understand-and-continue.

-  "understand-and-continue": "",
+  "understand-and-continue": "Entiendo y deseo continuar",

730-733: Fill translations for warning keys (organizations and paid subscriptions).

Keep tone consistent with other warnings.

-  "warning-organizations-will-be-deleted": "",
-  "warning-organizations-will-be-deleted-message": "",
-  "warning-paid-subscriptions": "",
-  "warning-paid-subscriptions-message": "",
+  "warning-organizations-will-be-deleted": "ADVERTENCIA: se eliminarán organizaciones",
+  "warning-organizations-will-be-deleted-message": "Eres el único super administrador en las siguientes organizaciones.\nEstas organizaciones se eliminarán permanentemente cuando se elimine tu cuenta:",
+  "warning-paid-subscriptions": "ADVERTENCIA: suscripciones activas",
+  "warning-paid-subscriptions-message": "Las siguientes organizaciones tienen suscripciones de pago activas.\nEstas suscripciones se cancelarán inmediatamente:",
🧹 Nitpick comments (1)
messages/es.json (1)

20-20: Polish grammar/casing in account-deletion-restore.

Minor Spanish fix; match file style (lowercase “soporte”) and add article.

-  "account-deletion-restore": "Por favor contacta <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Soporte de Capgo</a> si deseas restaurarla",
+  "account-deletion-restore": "Contacta al <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>soporte de Capgo</a> si deseas restaurarla",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc291ec and 80732fa.

📒 Files selected for processing (3)
  • messages/es.json (7 hunks)
  • messages/fr.json (7 hunks)
  • supabase/migrations/20250906094709_better_account_delete.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • messages/fr.json
  • supabase/migrations/20250906094709_better_account_delete.sql

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
supabase/migrations/20250906094709_better_account_delete.sql (4)

10-14: LGTM: uniqueness + scheduling index added

Prevents duplicate rows per account and makes sweeps efficient. Nicely done.


30-46: Restrict is_account_disabled to self/service_role; prevent cross‑user probing

Anyone can probe any user’s status. Gate by auth.uid() (except service_role), include auth on search_path, and lock down EXECUTE.

Apply:

 CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
 RETURNS BOOLEAN
 LANGUAGE plpgsql
 SECURITY DEFINER
-SET search_path = public
+SET search_path = public, auth
 AS $$
 BEGIN
-    -- Check if the user_id exists in the to_delete_accounts table
+    -- Allow service_role to query any user; others only themselves
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+      RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     RETURN EXISTS (
         SELECT 1 
         FROM public.to_delete_accounts 
         WHERE account_id = user_id
     );
 END;
 $$;
+
+-- Restrict who can call it
+REVOKE ALL ON FUNCTION public.is_account_disabled(uuid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.is_account_disabled(uuid) TO authenticated, service_role;

47-70: Apply same privacy gate to get_account_removal_date and restrict EXECUTE

Otherwise any caller can read arbitrary users’ removal_date.

 CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
 RETURNS TIMESTAMPTZ
 LANGUAGE plpgsql
 SECURITY DEFINER
-SET search_path = public
+SET search_path = public, auth
 AS $$
 DECLARE
     removal_date TIMESTAMPTZ;
 BEGIN
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+      RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     SELECT to_delete_accounts.removal_date INTO removal_date
     FROM public.to_delete_accounts 
     WHERE account_id = user_id;
     
     IF removal_date IS NULL THEN
         RAISE EXCEPTION 'Account with ID % is not marked for deletion', user_id;
     END IF;
     
     RETURN removal_date;
 END;
 $$;
+
+REVOKE ALL ON FUNCTION public.get_account_removal_date(uuid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.get_account_removal_date(uuid) TO authenticated, service_role;

72-108: Make delete_user authenticated‑only, idempotent, and avoid storing API keys

  • Guard unauthenticated calls.
  • Use ON CONFLICT for idempotency and to extend the scheduled date safely.
  • Don’t persist raw API keys in removed_data.
  • Add search_path and restrict EXECUTE.
-CREATE OR REPLACE FUNCTION "public"."delete_user" () RETURNS "void" LANGUAGE "plpgsql"
-SECURITY DEFINER AS $$
+CREATE OR REPLACE FUNCTION public.delete_user() RETURNS void
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
+AS $$
 DECLARE
   user_id_fn uuid;
   user_email text;
 BEGIN
   -- Get the current user ID and email
-  SELECT "auth"."uid"() INTO user_id_fn;
-  SELECT "email" INTO user_email FROM "auth"."users" WHERE "id" = user_id_fn;
+  SELECT auth.uid() INTO user_id_fn;
+  IF user_id_fn IS NULL THEN
+    RAISE EXCEPTION 'authentication required' USING ERRCODE = '28000';
+  END IF;
+  SELECT email INTO user_email FROM auth.users WHERE id = user_id_fn;
   
   -- Trigger the queue-based deletion process
   -- This cancels the subscriptions of the user's organizations
-  PERFORM "pgmq"."send"(
+  PERFORM pgmq.send(
     'on_user_delete'::text,
-    "jsonb_build_object"(
+    jsonb_build_object(
       'user_id', user_id_fn,
       'email', user_email
     )
   );
   
   -- Mark the user for deletion
-  INSERT INTO "public"."to_delete_accounts" (
-    "account_id", 
-    "removal_date", 
-    "removed_data"
-  ) VALUES 
-  (
-    user_id_fn, 
-    NOW() + INTERVAL '30 days', 
-    "jsonb_build_object"('email', user_email, 'apikeys', (SELECT "jsonb_agg"("to_jsonb"(a.*)) FROM "public"."apikeys" a WHERE a."user_id" = user_id_fn))
-  );
+  INSERT INTO public.to_delete_accounts (account_id, removal_date, removed_data)
+  VALUES (
+    user_id_fn,
+    NOW() + INTERVAL '30 days',
+    jsonb_build_object('email', user_email)
+  )
+  ON CONFLICT (account_id) DO UPDATE
+    SET removal_date = GREATEST(EXCLUDED.removal_date, public.to_delete_accounts.removal_date);
 
   -- Delete the API keys
-  DELETE FROM "public"."apikeys" WHERE "public"."apikeys"."user_id" = user_id_fn;
+  DELETE FROM public.apikeys WHERE public.apikeys.user_id = user_id_fn;
 END;
 $$;
+
+REVOKE ALL ON FUNCTION public.delete_user() FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.delete_user() TO authenticated, service_role;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80732fa and 1c308c5.

📒 Files selected for processing (3)
  • src/modules/auth.ts (2 hunks)
  • src/types/supabase.types.ts (32 hunks)
  • supabase/migrations/20250906094709_better_account_delete.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/auth.ts
🧰 Additional context used
📓 Path-based instructions (1)
{src,supabase/functions/_backend}/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce code style per @antfu/eslint-config: single quotes and no semicolons

Files:

  • src/types/supabase.types.ts
🧬 Code graph analysis (1)
src/types/supabase.types.ts (1)
supabase/functions/_backend/utils/supabase.types.ts (2)
  • Json (1-7)
  • Database (9-2080)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (5)
supabase/migrations/20250906094709_better_account_delete.sql (2)

16-24: LGTM: deny‑all RLS baseline

Good default posture for a sensitive queue table. Service role can bypass via role privileges.


121-151: Cannot confirm FK cascades; verify before simplifying deletion

No FOREIGN KEY or REFERENCES … ON DELETE CASCADE clauses for public.users(id) or to_delete_accounts(account_id) were found in your migrations. Confirm that auth.users(id)public.users(id) and to_delete_accounts(account_id)public.users(id) both declare ON DELETE CASCADE before removing the individual DELETE statements in the cleanup loop.

src/types/supabase.types.ts (3)

1236-1267: Types for to_delete_accounts match SQL

Shapes align with the migration (FK to users, created_at non-null, removal_date required). Good.


1486-1488: RPC surface added for get_account_removal_date

Signature and return type look correct; will work once the SQL privacy gate is in place.

Ensure the SQL migration updates per earlier comments so this RPC can be safely exposed to clients.


1782-1785: RPC surface for is_account_disabled

Type shape is fine. Security relies on the SQL function; please apply the privacy gate there.

Comment on lines +169 to +175
-- Create a cron job to run the account deletion function every minute
-- This will process and permanently delete accounts that have passed their removal_date
SELECT "cron"."schedule"(
'delete-expired-accounts', -- job name
'* * * * *', -- cron expression (every minute)
'SELECT "public"."delete_accounts_marked_for_deletion"();' -- SQL command
); No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make pg_cron job creation idempotent

Repeated deploys may create duplicate jobs or fail. Guard schedule.

-  SELECT "cron"."schedule"(
-    'delete-expired-accounts',           -- job name
-    '* * * * *',                        -- cron expression (every minute)
-    'SELECT "public"."delete_accounts_marked_for_deletion"();'  -- SQL command
-  );
+DO $$
+BEGIN
+  -- Try to schedule; ignore if already present
+  PERFORM cron.schedule(
+    'delete-expired-accounts',
+    '* * * * *',
+    'SELECT public.delete_accounts_marked_for_deletion();'
+  );
+EXCEPTION
+  WHEN others THEN
+    -- Best-effort idempotency; optionally check cron.job for an existing entry
+    NULL;
+END $$;
🤖 Prompt for AI Agents
In supabase/migrations/20250906094709_better_account_delete.sql around lines 169
to 175, the pg_cron job is created unconditionally which can produce duplicates
on repeated deploys; wrap the cron.schedule call in a DO block that checks
cron.job (e.g., WHERE jobname = 'delete-expired-accounts') and only calls
cron.schedule (via PERFORM) if no existing job with that name exists so the job
creation is idempotent.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
messages/es.json (4)

370-370: Optional: gender-inclusive variant.

Consider “Estoy seguro/a” if your UX guideline prefers inclusive forms. Otherwise fine.


516-516: Natural phrasing in Spanish.

Use “que se eliminarán” instead of “a ser eliminadas”.

-  "organizations-to-be-deleted": "Organizaciones a ser eliminadas",
+  "organizations-to-be-deleted": "Organizaciones que se eliminarán",

520-520: Preserve “paid” semantics.

Original key implies paid subscriptions; reflect as “de pago”.

-  "paid-subscriptions-to-cancel": "Suscripciones a cancelar",
+  "paid-subscriptions-to-cancel": "Suscripciones de pago a cancelar",

730-733: Improve headings and terminology (accent in “súper”, “de pago”).

Align casing with Spanish sentence case and fix terminology.

-  "warning-organizations-will-be-deleted": "Advertencia: Las Organizaciones Serán Eliminadas",
-  "warning-organizations-will-be-deleted-message": "Eres el único super administrador en las siguientes organizaciones. Estas organizaciones se eliminarán permanentemente cuando se elimine tu cuenta:",
-  "warning-paid-subscriptions": "Advertencia: Suscripciones Activas",
-  "warning-paid-subscriptions-message": "Las siguientes organizaciones tienen suscripciones pagadas activas. Estas suscripciones se cancelarán inmediatamente:",
+  "warning-organizations-will-be-deleted": "Advertencia: se eliminarán organizaciones",
+  "warning-organizations-will-be-deleted-message": "Eres el único súper administrador en las siguientes organizaciones. Estas organizaciones se eliminarán permanentemente cuando se elimine tu cuenta:",
+  "warning-paid-subscriptions": "Advertencia: suscripciones de pago activas",
+  "warning-paid-subscriptions-message": "Las siguientes organizaciones tienen suscripciones de pago activas. Estas suscripciones se cancelarán inmediatamente:",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c308c5 and 7ea8d30.

📒 Files selected for processing (1)
  • messages/es.json (6 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (2)
messages/es.json (2)

155-155: LGTM: clear CTA.


686-686: LGTM.

Comment thread messages/es.json
Comment on lines +18 to +23
"account-deletion-final-warning": "Esta acción no puede deshacerse. Su cuenta y todos los datos asociados serán eliminados permanentemente.",
"account-deletion-requested": "Has solicitado que se elimine tu cuenta.",
"account-deletion-restore": "Por favor contacta <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Soporte de Capgo</a> si deseas restaurarla",
"account-deletion-timer": "Tu cuenta dejará de existir",
"account-deletion-timer-in": "Tu cuenta dejará de existir en",
"account-deletion-very-soon": "Muy pronto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Spanish consistency and grammar (informal “tú”, missing preposition).

  • Keep consistent “tú” form used elsewhere (“Tu cuenta…”, not “Su”).
  • Add missing “al” before “soporte”.

Apply this diff:

-  "account-deletion-final-warning": "Esta acción no puede deshacerse. Su cuenta y todos los datos asociados serán eliminados permanentemente.",
-  "account-deletion-requested": "Has solicitado que se elimine tu cuenta.",
-  "account-deletion-restore": "Por favor contacta <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Soporte de Capgo</a> si deseas restaurarla",
+  "account-deletion-final-warning": "Esta acción no se puede deshacer. Tu cuenta y todos los datos asociados se eliminarán permanentemente.",
+  "account-deletion-requested": "Has solicitado la eliminación de tu cuenta.",
+  "account-deletion-restore": "Contacta al <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>soporte de Capgo</a> si deseas restaurarla",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"account-deletion-final-warning": "Esta acción no puede deshacerse. Su cuenta y todos los datos asociados serán eliminados permanentemente.",
"account-deletion-requested": "Has solicitado que se elimine tu cuenta.",
"account-deletion-restore": "Por favor contacta <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Soporte de Capgo</a> si deseas restaurarla",
"account-deletion-timer": "Tu cuenta dejará de existir",
"account-deletion-timer-in": "Tu cuenta dejará de existir en",
"account-deletion-very-soon": "Muy pronto",
"account-deletion-final-warning": "Esta acción no se puede deshacer. Tu cuenta y todos los datos asociados se eliminarán permanentemente.",
"account-deletion-requested": "Has solicitado la eliminación de tu cuenta.",
"account-deletion-restore": "Contacta al <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>soporte de Capgo</a> si deseas restaurarla",
"account-deletion-timer": "Tu cuenta dejará de existir",
"account-deletion-timer-in": "Tu cuenta dejará de existir en",
"account-deletion-very-soon": "Muy pronto",
🤖 Prompt for AI Agents
In messages/es.json around lines 18 to 23, the Spanish text uses inconsistent
pronouns and misses a preposition: change "Su cuenta..." to the informal "Tu
cuenta..." to match the rest of the file, and insert the missing preposition
"al" before "Soporte de Capgo" in the account-deletion-restore value (keeping
the existing anchor tag and attributes intact).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
eslint.config.js (1)

30-39: Tighten and simplify exceptions.

Keep exceptions minimal and document the sanitization used in these components. You can also fold them into the prior override with per-override ignores for a single source of truth.

Apply this diff to consolidate:

   {
     // Vue-specific overrides
     files: ['**/*.vue'],
+    // Exceptions: allow v-html in vetted components (kept out of this rule)
+    ignores: [
+      'src/components/Table.vue',
+      'src/components/TableLog.vue',
+    ],
     rules: {
       // Globally disallow v-html
       'vue/no-v-html': 'error',
     },
   },
-  {
-    // Exceptions: allow v-html in vetted components
-    files: [
-      'src/components/Table.vue',
-      'src/components/TableLog.vue',
-    ],
-    rules: {
-      'vue/no-v-html': 'off',
-    },
-  },

Alternatively, drop the config-level exception and add inline disables at the exact use sites to keep the blast radius minimal.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea8d30 and 868b3ad.

📒 Files selected for processing (1)
  • eslint.config.js (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-08T17:29:59.365Z
Learnt from: CR
PR: Cap-go/capgo#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T17:29:59.365Z
Learning: Applies to {src,supabase/functions/_backend}/**/*.{ts,tsx,js,jsx,vue} : Enforce code style per antfu/eslint-config: single quotes and no semicolons

Applied to files:

  • eslint.config.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (1)
eslint.config.js (1)

6-6: Enables Vue rules via antfu — good.

This turns on Vue plugin support and aligns with the repo’s single-quote/no-semicolon style. No issues.

Comment thread eslint.config.js
Comment on lines +22 to +29
{
// Vue-specific overrides
files: ['**/*.vue'],
rules: {
// Globally disallow v-html
'vue/no-v-html': 'error',
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Block v-html globally: good default; verify no unintended violations.

Great for XSS hardening. Please confirm new pages (e.g., src/pages/accountDisabled.vue, src/pages/settings/Account.vue) and recent i18n strings don’t rely on v-html.

Run to spot any usages outside the approved components:

If translations need markup, prefer component interpolation () or a dedicated SafeHtml component with sanitization (e.g., DOMPurify) instead of v-html.

I can draft a SafeHtml component or refactor examples using if helpful.


🏁 Script executed:

#!/bin/bash
# List all v-html occurrences in Vue files
rg -n --glob 'src/**/*.vue' -C2 '\bv-html\b' || true

# Show only those not in the vetted components
rg -n --glob 'src/**/*.vue' '\bv-html\b' | rg -v 'src/components/(Table|TableLog)\.vue' || true

Length of output: 3331


Remove unauthorized v-html usages and add necessary exceptions

  • src/pages/accountDisabled.vue:113 still uses v-html; replace with <i18n-t> interpolation or a sanitized SafeHtml component.
  • src/components/Table.vue (lines 387, 410, 416) and TableLog.vue (line 419) rely on v-html—either add // eslint-disable-next-line vue/no-v-html overrides or refactor into a dedicated sanitization component.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
supabase/migrations/20250906094709_better_account_delete.sql (4)

31-45: Gate probe to self or service_role; lock EXECUTE; set search_path

As written, anyone can probe any user’s disabled state and the function inherits default PUBLIC EXECUTE. Add a privacy check, and REVOKE/GRANT.

-CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
-RETURNS BOOLEAN
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
+CREATE OR REPLACE FUNCTION public.is_account_disabled(user_id UUID)
+RETURNS BOOLEAN
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
 AS $$
 BEGIN
-    -- Check if the user_id exists in the to_delete_accounts table
+    -- Allow service_role to query any user; others only themselves
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+      RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     RETURN EXISTS (
         SELECT 1 
         FROM public.to_delete_accounts 
         WHERE account_id = user_id
     );
 END;
 $$;
+
+-- Restrict execution
+REVOKE ALL ON FUNCTION public.is_account_disabled(uuid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.is_account_disabled(uuid) TO authenticated, service_role;

48-70: Same privacy gate for removal_date; restrict EXECUTE; set search_path

Anyone can read another user’s removal_date; fix similarly.

-CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
-RETURNS TIMESTAMPTZ
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
+CREATE OR REPLACE FUNCTION public.get_account_removal_date(user_id UUID)
+RETURNS TIMESTAMPTZ
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
 AS $$
 DECLARE
     removal_date TIMESTAMPTZ;
 BEGIN
+    IF current_user <> 'service_role' AND user_id IS DISTINCT FROM auth.uid() THEN
+      RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501';
+    END IF;
     -- Get the removal_date for the user_id
     SELECT to_delete_accounts.removal_date INTO removal_date
     FROM public.to_delete_accounts 
     WHERE account_id = user_id;
@@
 END;
 $$;
+
+REVOKE ALL ON FUNCTION public.get_account_removal_date(uuid) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.get_account_removal_date(uuid) TO authenticated, service_role;

72-108: delete_user lacks auth guard, search_path hardening, idempotency; avoid embedding API key rows

  • Add explicit search_path.
  • Reject unauthenticated calls early.
  • Use ON CONFLICT to upsert and extend the window.
  • Don’t store API key rows in removed_data.
  • Lock EXECUTE to authenticated/service_role.
-CREATE OR REPLACE FUNCTION "public"."delete_user" () RETURNS "void" LANGUAGE "plpgsql"
-SECURITY DEFINER AS $$
+CREATE OR REPLACE FUNCTION public.delete_user () RETURNS void
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, auth
+AS $$
 DECLARE
   user_id_fn uuid;
   user_email text;
 BEGIN
   -- Get the current user ID and email
-  SELECT "auth"."uid"() INTO user_id_fn;
-  SELECT "email" INTO user_email FROM "auth"."users" WHERE "id" = user_id_fn;
+  SELECT auth.uid() INTO user_id_fn;
+  IF user_id_fn IS NULL THEN
+    RAISE EXCEPTION 'authentication required' USING ERRCODE = '28000';
+  END IF;
+  SELECT email INTO user_email FROM auth.users WHERE id = user_id_fn;
@@
-  PERFORM "pgmq"."send"(
+  PERFORM pgmq.send(
     'on_user_delete'::text,
-    "jsonb_build_object"(
+    jsonb_build_object(
       'user_id', user_id_fn,
       'email', user_email
     )
   );
@@
-  INSERT INTO "public"."to_delete_accounts" (
-    "account_id", 
-    "removal_date", 
-    "removed_data"
+  INSERT INTO public.to_delete_accounts (
+    account_id, 
+    removal_date, 
+    removed_data
   ) VALUES 
   (
     user_id_fn, 
     NOW() + INTERVAL '30 days', 
-    "jsonb_build_object"('email', user_email, 'apikeys', (SELECT "jsonb_agg"("to_jsonb"(a.*)) FROM "public"."apikeys" a WHERE a."user_id" = user_id_fn))
-  );
+    jsonb_build_object('email', user_email)
+  )
+  ON CONFLICT (account_id) DO UPDATE
+    SET removal_date = GREATEST(EXCLUDED.removal_date, public.to_delete_accounts.removal_date);
@@
-  DELETE FROM "public"."apikeys" WHERE "public"."apikeys"."user_id" = user_id_fn;
+  DELETE FROM public.apikeys WHERE public.apikeys.user_id = user_id_fn;
 END;
 $$;
+
+REVOKE ALL ON FUNCTION public.delete_user() FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.delete_user() TO authenticated, service_role;

172-178: Make pg_cron scheduling idempotent

Repeated deploys can error or create dup jobs. Wrap with a guard.

-  SELECT "cron"."schedule"(
-    'delete-expired-accounts',           -- job name
-    '* * * * *',                        -- cron expression (every minute)
-    'SELECT "public"."delete_accounts_marked_for_deletion"();'  -- SQL command
-  );
+DO $$
+BEGIN
+  IF NOT EXISTS (
+    SELECT 1 FROM cron.job WHERE jobname = 'delete-expired-accounts'
+  ) THEN
+    PERFORM cron.schedule(
+      'delete-expired-accounts',
+      '* * * * *',
+      'SELECT public.delete_accounts_marked_for_deletion();'
+    );
+  END IF;
+END $$;
🧹 Nitpick comments (10)
supabase/tests/29_test_delete_accounts_marked_for_deletion.sql (7)

9-15: Harden SECURITY DEFINER helper with explicit search_path

Set a safe search_path to avoid function-hijack risks during tests.

-CREATE OR REPLACE FUNCTION create_test_user_for_deletion(
+CREATE OR REPLACE FUNCTION create_test_user_for_deletion(
   user_id UUID,
   user_email TEXT
 ) RETURNS VOID
 LANGUAGE plpgsql
-SECURITY DEFINER
+SECURITY DEFINER
+SET search_path = public, auth, extensions
 AS $$

18-33: Confirm auth.users insert shape won’t drift

Directly inserting many columns into auth.users is brittle across Supabase upgrades (column set/constraints can change). Prefer a test helper that provisions users or keep the column list minimal and stable. At least verify this passes CI against your current Postgres/Supabase version.

Do you want me to adapt this to a helper-based user factory to reduce coupling?


48-60: Test 2 is a weak proxy for “returns table”

Querying information_schema for a name collision could still pass/flake. Stronger: assert the function’s output columns via a typed record or by selecting (f()). Example:

--- a
-SELECT
-  ok ( ... ) ;
+WITH r AS (SELECT * FROM delete_accounts_marked_for_deletion())
+SELECT ok(
+  (SELECT array_agg(attname ORDER BY attname)
+   FROM pg_attribute 
+   WHERE attrelid = 'public.delete_accounts_marked_for_deletion'::regprocedure::oid) IS NULL
+  , 'function does not materialize a table'
+);

Alternatively, simply assert that (SELECT deleted_count, deleted_user_ids FROM delete_accounts_marked_for_deletion()) returns a single row with expected types.


156-176: Also assert deleted_user_ids contains the expected UUID

You validate count on first run and 0 on second, but not the contents. Add an assertion that the returned array contains 2222…22 on first run.


205-245: Multiple-deletion: assert set equality of IDs, not just count

Check the array has exactly {3333…33,4444…44}.


308-346: Strengthen inconsistent-state test with result assertions

lives_ok + cleanup check is good. Also assert deleted_count = 1 and that deleted_user_ids contains 7777…77.


350-363: Add permission tests for is_account_disabled/get_account_removal_date

Once those RPCs are privacy-gated (self or service_role), mirror this throws_ok pattern to ensure regular users can’t probe other accounts.

supabase/migrations/20250906094709_better_account_delete.sql (3)

3-3: Prefer IDENTITY over SERIAL

SERIAL is legacy and creates a separate sequence. Use GENERATED ALWAYS (or BY DEFAULT) AS IDENTITY for cleaner DDL and easier resets.

-  id SERIAL PRIMARY KEY,
+  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,

21-24: Consider FORCE ROW LEVEL SECURITY for defense-in-depth

If you expect even table owners to be constrained (outside superuser/service_role), add FORCE RLS. Non-blocking given SEC DEFINER usage.

ALTER TABLE public.to_delete_accounts FORCE ROW LEVEL SECURITY;

110-161: Set safe search_path for SECURITY DEFINER

Even though objects are schema-qualified, set search_path explicitly for consistency and safety.

 CREATE OR REPLACE FUNCTION "public"."delete_accounts_marked_for_deletion" () 
 RETURNS TABLE(deleted_count INTEGER, deleted_user_ids UUID[])
 LANGUAGE "plpgsql"
-SECURITY DEFINER
+SECURITY DEFINER
+SET search_path = public, auth
 AS $$
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 868b3ad and c54374c.

📒 Files selected for processing (2)
  • supabase/migrations/20250906094709_better_account_delete.sql (1 hunks)
  • supabase/tests/29_test_delete_accounts_marked_for_deletion.sql (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run tests
  • GitHub Check: Analyze (java-kotlin)

Comment thread src/pages/accountDisabled.vue Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/pages/accountDisabled.vue (1)

22-23: Use browser-safe timer type (duplicate of prior comment).

Replace Node-specific NodeJS.Timeout with a cross-env-safe type.

-let intervalId: NodeJS.Timeout | null = null
+let intervalId: ReturnType<typeof setInterval> | null = null
🧹 Nitpick comments (5)
src/pages/accountDisabled.vue (3)

137-140: Render the anchor only if there’s visible link text.

Prevents empty, focusable anchors when translators omit the link text.

-          <a :href="restoreParts.href" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline hover:text-blue-600">{{ restoreParts.linkText }}</a>
+          <a
+            v-if="restoreParts.linkText"
+            :href="restoreParts.href"
+            target="_blank"
+            rel="noopener noreferrer"
+            class="text-blue-500 underline hover:text-blue-600"
+          >{{ restoreParts.linkText }}</a>

86-101: Avoid comparing translated strings for control flow.

Using timeRemaining === t('account-deletion-very-soon') is brittle. Compute a boolean for “imminent” instead and use it in the template.

 const timeRemaining = computed(() => {
   if (loading.value) {
     return t('loading')
   }
 
   if (error.value || !deletionDate.value) {
     return error.value || t('error')
   }
 
   const diff = deletionDate.value.getTime() - currentTime.value.getTime()
 
   if (diff <= 0) {
     return t('account-deletion-very-soon')
   }

Add (nearby, after timeRemaining):

+const isDeletionImminent = computed(() => {
+  if (loading.value || error.value || !deletionDate.value) return false
+  return deletionDate.value.getTime() - currentTime.value.getTime() <= 0
+})

And update the label:

-            {{ timeRemaining === t('account-deletion-very-soon') ? t('account-deletion-timer') : t('account-deletion-timer-in') }}
+            {{ isDeletionImminent ? t('account-deletion-timer') : t('account-deletion-timer-in') }}

Also applies to: 143-144


81-84: Minor cleanup after clearing the interval.

Null out the handle post-clear to avoid accidental reuse.

 onUnmounted(() => {
   if (intervalId) {
     clearInterval(intervalId)
+    intervalId = null
   }
 })
src/components/Table.vue (2)

7-7: Import PropType/VNodeChild for proper typing of RenderCell.

You’re introducing a component; bring in Vue’s PropType and VNodeChild as type-only imports to strongly type the renderer.

-import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'
+import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'
+import type { PropType, VNodeChild } from 'vue'

295-305: Strongly type RenderCell props; remove confusing Function casts.

Function as unknown as () => ((item) => any) is misleading and harms DX. Use PropType and return VNodeChild; drop the generic on defineComponent to avoid conflicts with runtime props.

-// Helper component to render VNode content from a column's renderFunction
-const RenderCell = defineComponent<{ renderer?: (item: any) => any, item: any }>({
+// Helper component to render VNode content from a column's renderFunction
+const RenderCell = defineComponent({
   name: 'RenderCell',
   props: {
-    renderer: Function as unknown as () => ((item: any) => any) | undefined,
-    item: { type: Object as any, required: true },
+    renderer: Function as PropType<(item: any) => VNodeChild | null | undefined>,
+    item: { type: Object as PropType<any>, required: true },
   },
   setup(props) {
     return () => (props.renderer ? (props.renderer as any)(props.item) : null)
   },
 })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c54374c and 2a95def.

📒 Files selected for processing (7)
  • src/components/Table.vue (4 hunks)
  • src/components/TableLog.vue (1 hunks)
  • src/components/comp_def.ts (1 hunks)
  • src/components/tables/AppTable.vue (2 hunks)
  • src/pages/accountDisabled.vue (1 hunks)
  • src/pages/settings/organization/Members.vue (2 hunks)
  • src/typed-router.d.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{src,supabase/functions/_backend}/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce code style per @antfu/eslint-config: single quotes and no semicolons

Files:

  • src/components/comp_def.ts
  • src/typed-router.d.ts
  • src/components/TableLog.vue
  • src/components/Table.vue
  • src/pages/settings/organization/Members.vue
  • src/components/tables/AppTable.vue
  • src/pages/accountDisabled.vue
src/**/*.vue

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vue 3 with Composition API and <script setup> syntax in Vue components

Files:

  • src/components/TableLog.vue
  • src/components/Table.vue
  • src/pages/settings/organization/Members.vue
  • src/components/tables/AppTable.vue
  • src/pages/accountDisabled.vue
src/pages/**/*.vue

📄 CodeRabbit inference engine (CLAUDE.md)

Use file-based routing; place route components under src/pages/

Files:

  • src/pages/settings/organization/Members.vue
  • src/pages/accountDisabled.vue
🧠 Learnings (2)
📚 Learning: 2025-09-08T17:29:59.366Z
Learnt from: CR
PR: Cap-go/capgo#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T17:29:59.366Z
Learning: Applies to src/pages/**/*.vue : Use file-based routing; place route components under src/pages/

Applied to files:

  • src/typed-router.d.ts
📚 Learning: 2025-09-08T17:29:59.366Z
Learnt from: CR
PR: Cap-go/capgo#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T17:29:59.366Z
Learning: Applies to src/layouts/**/*.vue : Place reusable page layout components under src/layouts/

Applied to files:

  • src/typed-router.d.ts
🪛 GitHub Check: CodeQL
src/pages/accountDisabled.vue

[failure] 30-30: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (8)
src/typed-router.d.ts (2)

22-23: Route mapping for /accountDisabled looks correct.

Matches file-based routing and naming patterns already present (e.g., /ApiKeys). No action needed.


70-73: File-path map for src/pages/accountDisabled.vue is consistent.

Route name and view mapping align with unplugin-vue-router expectations.

src/components/TableLog.vue (1)

419-421: LGTM! Dynamic component rendering is a significant improvement over v-html.

The switch from v-html to dynamic component rendering aligns with Vue best practices and eliminates XSS vulnerabilities from HTML injection. This change is consistent with the broader refactor across the codebase to use renderFunction for complex cell content.

src/components/comp_def.ts (1)

40-42: Well-designed API addition for safe cell rendering.

The new renderFunction property provides a clean, type-safe alternative to HTML string manipulation. This is a much better approach than the previous displayFunction + allowHtml + sanitizeHtml pattern, as it leverages Vue's virtual DOM for secure rendering.

src/pages/settings/organization/Members.vue (1)

115-133: Excellent implementation of VNode-based cell rendering.

The renderFunction implementation properly constructs VNodes using Vue's h function, providing a secure and performant way to render complex cell content. The avatar logic with fallback to initials is well-structured and follows Vue best practices.

src/components/tables/AppTable.vue (1)

56-73: Clean VNode construction for app table cells.

The implementation correctly uses Vue's h function to build the cell content programmatically. The structure with avatar fallback to initials mirrors the pattern used in Members.vue, maintaining consistency across the codebase.

src/components/Table.vue (2)

399-403: LGTM: safe per-cell rendering via RenderCell.

Cleanly routes complex cell rendering through a function without using raw HTML.


429-433: LGTM: body cell RenderCell usage.

Consistent with header usage; avoids raw HTML while enabling rich cells.

Comment thread src/components/Table.vue
Comment on lines 419 to +424
<button
class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
@click.stop="col.onClick ? col.onClick(elem) : () => {}"
v-html="col.icon"
/>
>
<component :is="col.icon" />
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Icon rendering via dynamic component: good security upgrade; add a11y label and verify icons are components.

Switching from HTML strings to components is safer. Consider adding an aria-label/title for icon-only buttons, and verify all col.icon/action.icon usages are components (not strings).

Apply a small accessibility tweak:

-                      <button
-                        class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
-                        @click.stop="col.onClick ? col.onClick(elem) : () => {}"
-                      >
+                      <button
+                        class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
+                        :aria-label="col.ariaLabel ?? col.label ?? 'action'"
+                        :title="col.ariaLabel ?? col.label ?? 'action'"
+                        @click.stop="col.onClick ? col.onClick(elem) : () => {}"
+                      >
                         <component :is="col.icon" />
                       </button>

Run this to spot legacy string icons or v-html:


🏁 Script executed:

#!/bin/bash
# Find any icon props still using string literals (legacy HTML/SVG)
rg -nP -C2 --type=ts --type=tsx --type=vue '\bicon\s*:\s*[\'"]' || true

# Find residual v-html usages
rg -nP -C2 --type=vue 'v-html\s*=' || true

# Quick scan of renderFunction definitions
rg -nP -C1 --type=ts --type=vue '\brenderFunction\s*:\s*\('

Length of output: 68


🏁 Script executed:

#!/bin/bash
# Corrected quoting to detect legacy string icons, v-html, and renderFunction usages

# Find any icon props still using string literals (legacy HTML/SVG)
rg -nP -C2 --type=ts --type=tsx --type=vue "\\bicon\\s*:\\s*['\"]" || true

# Find residual v-html usages
rg -nP -C2 --type=vue "v-html\\s*=" || true

# Quick scan of renderFunction definitions
rg -nP -C1 --type=ts --type=vue "\\brenderFunction\\s*:\\s*\\(" || true

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Search for legacy string icons across TS/TSX/Vue files
rg -nP -C2 "\\bicon\\s*:\\s*['\"]" -g '*.ts' -g '*.tsx' -g '*.vue' || true

# Find residual v-html usages in Vue files
rg -nP -C2 "v-html\\s*=" -g '*.vue' || true

# Quick scan of renderFunction definitions in TS and Vue files
rg -nP -C1 "\\brenderFunction\\s*:\\s*\\(" -g '*.ts' -g '*.vue' || true

Length of output: 21531


Ensure icon props use component references and add ARIA labels

  • Add accessible labels to icon‐only buttons:
    <button
      … 
  • :aria-label="col.ariaLabel ?? col.label ?? 'action'"
  • :title="col.ariaLabel ?? col.label ?? 'action'"
    @click.stop="col.onClick ? col.onClick(elem) : () => {}"
<component :is="col.icon" />
``` - Replace all string-literal icons in UI code with imported Vue components: • src/components/dashboard/StepsApp.vue (icon: '👶') • src/components/dashboard/StepsBundle.vue (icon: '👶') • src/pages/settings/organization/Plans.vue (icon: '💳') • src/pages/settings/organization/Usage.vue (icon: '💳') • src/modules/auth.ts (icon: '✅')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
@click.stop="col.onClick ? col.onClick(elem) : () => {}"
v-html="col.icon"
/>
>
<component :is="col.icon" />
</button>
<button
class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
:aria-label="col.ariaLabel ?? col.label ?? 'action'"
:title="col.ariaLabel ?? col.label ?? 'action'"
@click.stop="col.onClick ? col.onClick(elem) : () => {}"
>
<component :is="col.icon" />
</button>
🤖 Prompt for AI Agents
In src/components/Table.vue around lines 419-424, the icon-only button uses a
dynamic component and a click ternary that returns a no-op function and lacks an
accessible label; update columns so their icon fields are Vue component
references (import the emoji icons as components in the files listed in the
review and replace string literals with those component references), ensure the
button includes an aria-label (e.g., aria-label="col.ariaLabel || col.label ||
'action'") or binds to a computed label on the column, and change the click
handler to invoke the function (e.g., @click.stop="col.onClick ?
col.onClick(elem) : null") so the handler actually runs.

Comment thread src/pages/accountDisabled.vue Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
messages/pt-br.json (1)

20-20: Fix malformed, unsafe HTML anchor and brand typos in account-deletion-restore.

Remove spaces around attributes, fix Tailwind class token, add rel="noopener noreferrer", and don’t translate brand/class tokens.

Apply:

-  "account-deletion-restore": "Entre em contato com <a href = 'https://support.capgo.app/' target='_blank' class='text-blue-500 hover: text-blue-600'> Suporte ao capto </a> Se você quiser restaurá-lo",
+  "account-deletion-restore": "Entre em contato com <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>o suporte da Capgo</a> se quiser restaurá-la"
messages/tr.json (1)

20-20: Fix malformed, unsafe HTML anchor in account-deletion-restore.

Normalize attributes/classes and add rel="noopener noreferrer".

-  "account-deletion-restore": "Lütfen <a href = 'https://support.capgo.app/' target='_blank' class='text-blue-500 hover: text-blue-600'> capgo desteği </a> Geri yüklemek istiyorsanız",
+  "account-deletion-restore": "Lütfen <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo desteği</a> ile iletişime geçin, hesabınızı geri yüklemek istiyorsanız",
messages/ru.json (1)

20-20: Fix malformed, unsafe HTML anchor in account-deletion-restore.

Remove attribute spacing, fix class token, add rel, and localize only visible text.

-  "account-deletion-restore": "Пожалуйста, свяжитесь с <a href = 'https://support.capgo.app/' target='_blank' class='text-blue-500 hover: text-blue-600'> поддержка Capgo </a> Если вы хотите его восстановить",
+  "account-deletion-restore": "Пожалуйста, свяжитесь с <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>службой поддержки Capgo</a>, если вы хотите восстановить учётную запись",
messages/en.json (2)

20-20: Harden external link (reverse‑tabnabbing).

Add rel="noopener noreferrer".

-  "account-deletion-restore": "Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> if you would like to restore it",
+  "account-deletion-restore": "Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> if you would like to restore it",

182-193: Add rel='noopener noreferrer' to all target='_blank' anchors in this file.

Security hardening across support links.

-  "cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>capgo support</a> and check browser logs",
+  "cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>capgo support</a> and check browser logs",
-  "cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
-  "cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
-  "cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",
+  "cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",
-  "cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",
+  "cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",
-  "error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

Also applies to: 342-342

🧹 Nitpick comments (7)
messages/pt-br.json (2)

341-344: Unify error tone and grammar.

Prefer consistent impersonal phrasing (“Não é possível …”).

-  "error-loading-deletion-date": "Não é possível encontrar a data de exclusão da sua conta",
-  "error-message-invitation": "Ocorreu um erro. Por favor, entre em contato com o suporte da Capgo.",
-  "error-no-user-id": "Não consigo encontrar sua conta de usuário",
+  "error-loading-deletion-date": "Não é possível encontrar a data de exclusão da sua conta",
+  "error-message-invitation": "Ocorreu um erro. Por favor, entre em contato com o suporte da Capgo.",
+  "error-no-user-id": "Não é possível encontrar sua conta de usuário",

372-372: Improve translation of “I am sure”.

“Tenho certeza” is the idiomatic PT‑BR choice.

-  "i-am-sure": "estou certo",
+  "i-am-sure": "Tenho certeza",
messages/tr.json (2)

21-23: Make “timer-in” translatable; avoid concatenation trap.

“account-deletion-timer-in” equals the base string and won’t read correctly when a time value is appended. Switch to a parameterized message.

-  "account-deletion-timer": "Hesabınız var olmayı bırakacak",
-  "account-deletion-timer-in": "Hesabınız var olmayı bırakacak",
+  "account-deletion-timer": "Hesabınız var olmayı bırakacak",
+  "account-deletion-timer-in": "Hesabınız {time} içinde var olmayı bırakacak"

Follow-up: pass {time} from the component instead of string concatenation.


341-344: Prefer neutral/impersonal error voice.

Use “bulunamadı” instead of first-person “bulamıyorum”.

-  "error-no-user-id": "Kullanıcı hesabınızı bulamıyorum",
+  "error-no-user-id": "Kullanıcı hesabınız bulunamadı",
messages/ru.json (2)

372-372: Gender-inclusive wording.

Use “Я уверен(а)” to avoid gendered phrasing.

-  "i-am-sure": "я уверен",
+  "i-am-sure": "я уверен(а)",

522-523: Natural phrasing for “paid-subscriptions-to-cancel”.

More idiomatic RU.

-  "paid-subscriptions-to-cancel": "Подписки должны быть отменены",
+  "paid-subscriptions-to-cancel": "Подписки для отмены",
messages/en.json (1)

20-20: Prefer rendering links in components, not translations.

Consider moving the out of i18n and injecting a link via parameters/slots to avoid XSS and grammar-order issues for RTL/locales.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a95def and 874f433.

📒 Files selected for processing (14)
  • messages/de.json (7 hunks)
  • messages/en.json (8 hunks)
  • messages/es.json (7 hunks)
  • messages/fr.json (7 hunks)
  • messages/id.json (7 hunks)
  • messages/it.json (7 hunks)
  • messages/ja.json (7 hunks)
  • messages/ko.json (7 hunks)
  • messages/pl.json (7 hunks)
  • messages/pt-br.json (7 hunks)
  • messages/ru.json (7 hunks)
  • messages/tr.json (7 hunks)
  • messages/vi.json (7 hunks)
  • messages/zh-cn.json (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • messages/ko.json
  • messages/zh-cn.json
  • messages/id.json
  • messages/it.json
  • messages/de.json
  • messages/pl.json
  • messages/ja.json
  • messages/vi.json
  • messages/es.json
  • messages/fr.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests

…zation support

- Removed petite-vue-i18n dependency from package.json
- Updated imports in all components and pages to use vue-i18n
- Adjusted i18n usage in the accountDisabled page to utilize component interpolation
- Modified vite.config.mts to reflect the change in i18n module
@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​vue-i18n@​11.1.12991009594100

View full report

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Sep 9, 2025

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (30)
src/pages/forgot_password.vue (3)

117-125: Prevent stuck spinner: always reset isLoading in submit()

Early returns inside step2() can leave the UI loading forever. Wrap submit() in try/finally so isLoading is cleared regardless of path.

Apply:

 async function submit(form: { email: string, password: string, password_confirm: string }) {
-  isLoading.value = true
-  if (step.value === 1) {
-    await step1(form)
-  }
-  else if (step.value === 2 && route.hash) {
-    await step2(form)
-  }
+  isLoading.value = true
+  try {
+    if (step.value === 1) {
+      await step1(form)
+    }
+    else if (step.value === 2 && route.hash) {
+      await step2(form)
+    }
+  }
+  finally {
+    isLoading.value = false
+  }
 }

56-58: Handle AAL retrieval errors explicitly

Guard against failures from getAuthenticatorAssuranceLevel() to avoid undefined access on data.

Apply:

-  const aal = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
-  const { currentLevel, nextLevel } = aal.data!
+  const aal = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
+  if (aal.error) {
+    setErrors('forgot-password', [aal.error.message], {})
+    console.error('Cannot get AAL level', aal.error)
+    return
+  }
+  const { currentLevel, nextLevel } = aal.data!

89-96: Sanitize MFA code to digits and validate length

replace(' ', '') removes only the first space. Strip all non-digits and reject short inputs before verify.

Apply:

-            const { data: _verify, error: errorVerify } = await supabase.auth.mfa.verify({
+            const code = mfaCode.value.replace(/\D/g, '')
+            if (code.length !== 6) {
+              toast.error(t('invalid-mfa-code'))
+              return false
+            }
+            const { data: _verify, error: errorVerify } = await supabase.auth.mfa.verify({
               factorId: factor.id,
               challengeId: challenge.id,
-              code: mfaCode.value.replace(' ', ''),
+              code,
             })
src/pages/settings/ChangePassword.vue (2)

11-17: Missing import for useDisplayStore causes runtime error

useDisplayStore() is used but not imported.

 import { useSupabase } from '~/services/supabase'
 import { useDialogV2Store } from '~/stores/dialogv2'
+import { useDisplayStore } from '~/stores/display'

28-46: Form error messages target wrong form id

setErrors uses 'forgot-password' but the form id is 'change-pass', so errors won’t show.

-      setErrors('forgot-password', [mfaError.message], {})
+      setErrors('change-pass', [mfaError.message], {})
...
-      setErrors('forgot-password', ['Cannot find MFA factor'], {})
+      setErrors('change-pass', ['Cannot find MFA factor'], {})
...
-      setErrors('forgot-password', [errorChallenge.message], {})
+      setErrors('change-pass', [errorChallenge.message], {})
src/pages/onboarding/set_password.vue (2)

112-127: Nested inside breaks submit behavior

Invalid HTML; only one button should wrap spinner/label.

-                    <div>
-                      <button type="submit" class="inline-flex items-center justify-center w-full">
-                        <svg v-if="isLoading" class="inline-block w-5 h-5 mr-3 -ml-1 text-gray-900 align-middle animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                          <circle
-                            class="opacity-25"
-                            cx="12"
-                            cy="12"
-                            r="10"
-                            stroke="currentColor"
-                            stroke-width="4"
-                          />
-                          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
-                        </svg>
-                        <button v-if="!isLoading" type="submit" class="inline-flex items-center justify-center w-full px-4 py-4 text-base font-semibold text-white transition-all duration-200 border border-transparent rounded-md bg-muted-blue-700 focus:bg-blue-700 hover:bg-blue-700 focus:outline-hidden">
-                          {{ t('validate') }}
-                        </button>
-                      </button>
-                    </div>
+                    <div>
+                      <button type="submit" class="inline-flex items-center justify-center w-full px-4 py-4 text-base font-semibold text-white transition-all duration-200 border border-transparent rounded-md bg-muted-blue-700 focus:bg-blue-700 hover:bg-blue-700 focus:outline-hidden">
+                        <svg v-if="isLoading" class="inline-block w-5 h-5 mr-3 -ml-1 text-gray-900 align-middle animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                          <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
+                          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
+                        </svg>
+                        <span v-else>{{ t('validate') }}</span>
+                      </button>
+                    </div>

39-46: Redirect occurs even when password update fails

Only navigate after a successful update.

-  if (updateError)
-    setErrors('set-password', [updateError.message], {})
-  else
-    toast.success(t('changed-password-suc'))
-  router.push('/onboarding/activation')
+  if (updateError) {
+    setErrors('set-password', [updateError.message], {})
+  } else {
+    toast.success(t('changed-password-suc'))
+    router.push('/onboarding/activation')
+  }

src/pages/settings/organization/index.vue (2)

98-103: Typo in payload breaks email update

emial should be email.

   const { error } = await supabase.functions.invoke('private/set_org_email', {
     body: {
-      emial: form.email,
+      email: form.email,
       org_id: orgCopy.gid,
     },
   })

221-223: Typo in template attribute prevents styling

clasclass.

-          <div clas="dark:text-gray-100">
+          <div class="dark:text-gray-100">
             {{ t('modify-org-info') }}
           </div>
src/components/tables/ChannelTable.vue (1)

183-196: Fix undefined variable in delete dialog description

name is not defined; should use one.name.

Apply this diff:

   dialogStore.openDialog({
     title: t('alert-confirm-delete'),
-    description: `${t('alert-not-reverse-message')} ${t('alert-delete-message')} ${name}?`,
+    description: `${t('alert-not-reverse-message')} ${t('alert-delete-message')} ${one.name}?`,
     buttons: [
src/components/dashboard/AppSetting.vue (3)

2-2: Missing Organization type import (TS error)

confirmTransferAppOwnership uses Organization but it isn’t imported.

-import type { OrganizationRole } from '~/stores/organization'
+import type { Organization, OrganizationRole } from '~/stores/organization'

277-283: Fix Supabase Storage bucket/path usage

.from expects a bucket name, not a path. Current code will target a non-existent bucket like images/org/... and fail. Use bucket 'images' and put the path in the object key.

-          const { error } = await supabase.storage
-            .from(`images/org/${appRef.value?.owner_org.id}/${props.appId}`)
-            .upload('icon', blob, {
-              contentType: mimeType,
-            })
+          const { error } = await supabase.storage
+            .from('images')
+            .upload(`org/${appRef.value?.owner_org.id}/${props.appId}/icon`, blob, {
+              contentType: mimeType,
+            })
-          const { data: signedURLData } = await supabase
-            .storage
-            .from(`images/org/${appRef.value?.owner_org.id}/${props.appId}`)
-            .getPublicUrl('icon')
+          const { data: signedURLData } = await supabase
+            .storage
+            .from('images')
+            .getPublicUrl(`org/${appRef.value?.owner_org.id}/${props.appId}/icon`)

Also applies to: 289-297


392-400: Fix transfer history lookup (logic/type bug)

transferHistory is assigned to the entire app object, not its history array. Access the proper property before length/sort.

-  const transferHistory: { transferred_at: string }[] = (appRef.value as any) ?? []
-  if (!transferHistory || transferHistory.length === 0)
+  const transferHistory = ((appRef.value as any)?.transfer_history ?? []) as { transferred_at: string }[]
+  if (transferHistory.length === 0)
     return

If the actual property name differs, adjust accordingly.

src/pages/invitation.vue (1)

30-31: Password policy mismatch (UI vs validation)

UI says “at least 12 characters” and “at least two special characters”, but validation enforces 6 and 1. Align these to avoid weak passwords and user confusion.

-const hasMinLength = computed(() => password.value.length >= 6)
+const hasMinLength = computed(() => password.value.length >= 12)
...
-  return symbolCount >= 1
+  return symbolCount >= 2

Also applies to: 43-44

src/pages/login.vue (2)

223-231: Untrusted “message” from URL hash shown to users

You render a message taken directly from the URL fragment. If the toast renders HTML, this is an XSS vector. Prefer a whitelist of known messages or sanitize/escape.

-  if (message) {
+  if (message) {
+    // only allow known messages or escape content
+    const allowed = new Set(['Email change confirmed', 'Magic link confirmed'])
+    const safeMessage = allowed.has(message) ? message : t('something-went-wrong-try-again-later')
     isLoading.value = false
     return setTimeout(() => {
-      toast.success(message, {
+      toast.success(safeMessage, {
         duration: 7000,
       })
     }, 400)
   }

186-205: Non-null assertion on mfaFactor can crash

If no verified factor exists, mfaFactor!.id will throw. Add a guard.

-    const mfaFactor = mfaFactors?.all.find(factor => factor.status === 'verified')
+    const mfaFactor = mfaFactors?.all.find(factor => factor.status === 'verified')
+    if (!mfaFactor) {
+      // fallback: continue login or prompt to enroll 2FA
+      await nextLogin()
+      return
+    }
...
-    const { data: challenge, error: errorChallenge } = await supabase.auth.mfa.challenge({ factorId: mfaFactor!.id })
+    const { data: challenge, error: errorChallenge } = await supabase.auth.mfa.challenge({ factorId: mfaFactor.id })
...
-    mfaLoginFactor.value = mfaFactor!
+    mfaLoginFactor.value = mfaFactor
src/pages/delete_account.vue (1)

95-99: Form error target uses wrong FormKit id

Errors are set on 'login-account' but the form id is 'delete-account'. Users won’t see errors.

-    setErrors('login-account', [error.message], {})
+    setErrors('delete-account', [error.message], {})
src/components/LineChartStats.vue (1)

167-186: chartData not reactive to prop changes

chartData is initialized once from computed values; subsequent updates to props.data won’t reflect unless you rebuild datasets. Make it computed.

-const chartData = ref<ChartData<'line'>>({
-  labels: monthdays(),
-  datasets: [{
-    label: props.title,
-    data: accumulateData.value,
-    borderColor: props.colors[400],
-    backgroundColor: props.colors[200],
-    tension: 0.3,
-    pointRadius: 2,
-    pointBorderWidth: 0,
-  }, {
-    label: t('prediction'),
-    data: projectionData.value,
-    borderColor: 'transparent',
-    backgroundColor: props.colors[200],
-    tension: 0.9,
-    pointRadius: 2,
-    pointBorderWidth: 0,
-  }],
-})
+const chartData = computed<ChartData<'line'>>(() => ({
+  labels: monthdays(),
+  datasets: [{
+    label: props.title,
+    data: accumulateData.value,
+    borderColor: props.colors[400],
+    backgroundColor: props.colors[200],
+    tension: 0.3,
+    pointRadius: 2,
+    pointBorderWidth: 0,
+  }, {
+    label: t('prediction'),
+    data: projectionData.value,
+    borderColor: 'transparent',
+    backgroundColor: props.colors[200],
+    tension: 0.9,
+    pointRadius: 2,
+    pointBorderWidth: 0,
+  }],
+}))
-  <Line :data="chartData" height="auto" :options="(chartOptions as any)" :plugins="[inlineAnnotationPlugin]" />
+  <Line :data="chartData" height="auto" :options="(chartOptions as any)" :plugins="[inlineAnnotationPlugin]" />

Also applies to: 219-221

src/components/tables/BundleTable.vue (2)

226-231: Fix null checks in Supabase filters (.neq with null won’t work)

PostgREST requires IS/IS NOT for null. Using .neq(..., null) won’t filter anything.

Apply:

-    req = req.eq('deleted', filters.value.deleted)
-    if (filters.value['external-storage'])
-      req = req.neq('external_url', null)
-    if (filters.value.encrypted)
-      req = req.neq('session_key', null)
+    req = req.eq('deleted', filters.value.deleted)
+    if (filters.value['external-storage'])
+      req = req.not('external_url', 'is', null)
+    if (filters.value.encrypted)
+      req = req.not('session_key', 'is', null)

215-224: Harden search: use ilike consistently and escape input to avoid broken OR filter

  • Use ilike for case-insensitive name search.
  • Escape %, _ and commas in user input before embedding into the OR string; otherwise certain inputs produce invalid filters.
-    if (search.value) {
+    if (search.value) {
+      const escapeILike = (s: string) => s.replaceAll('%', '\\%').replaceAll('_', '\\_').replaceAll(',', '\\,').replaceAll(')', '\\)')
+      const q = escapeILike(search.value)
       if (channelsToSearch && channelsToSearch.length > 0) {
         // Search by both version name or linked channel
-        req = req.or(`name.ilike.%${search.value}%,id.in.(${channelsToSearch.join(',')})`)
+        req = req.or(`name.ilike.%${q}%,id.in.(${channelsToSearch.join(',')})`)
       }
       else {
         // Search by version name only
-        req = req.like('name', `%${search.value}%`)
+        req = req.ilike('name', `%${q}%`)
       }
     }
src/pages/ApiKeys.vue (1)

1-8: Missing Ref type import causes TS error

columns uses Ref<TableColumn[]> but Ref isn’t imported.

 <script setup lang="ts">
 import type { TableColumn } from '~/components/comp_def'
 import type { Database } from '~/types/supabase.types'
 import { FormKit } from '@formkit/vue'
-import { computed, ref, watch } from 'vue'
+import { computed, ref, watch } from 'vue'
+import type { Ref } from 'vue'
 import { useI18n } from 'vue-i18n'
vite.config.mts (1)

35-39: Vue I18n plugin include path likely wrong (messages vs locales)

You build the locales list from ./messages/, but the unplugin include still points to locales/**. This can prevent bundling of JSON messages.

-    VueI18n({
-      module: 'vue-i18n',
-      runtimeOnly: true,
-      compositionOnly: true,
-      fullInstall: true,
-      include: [path.resolve(__dirname, 'locales/**')],
-    }),
+    VueI18n({
+      module: 'vue-i18n',
+      runtimeOnly: true,
+      compositionOnly: true,
+      fullInstall: true,
+      include: [path.resolve(__dirname, 'messages/**')],
+    }),

Also applies to: 150-157

src/components/dashboard/StepsBundle.vue (2)

168-175: Supabase realtime options: typo and broken filter

  • Property should be filter, not filtr.
  • The filter string is malformed (missing column for the second eq). Likely intended to filter by app_id.

Apply:

-        {
-          event: 'INSERT',
-          schema: 'public',
-          table: 'app_versions',
-          filtr: `owner_org=eq.${organizationStore.currentOrganization?.gid}&eq.${props.appId}`,
-        },
+        {
+          event: 'INSERT',
+          schema: 'public',
+          table: 'app_versions',
+          filter: `owner_org=eq.${organizationStore.currentOrganization?.gid}&app_id=eq.${props.appId}`,
+        },

136-140: isLoading can stay stuck on early returns in getKey

When main.user?.id is falsy or after upsert/recursion paths, isLoading may never reset.

Use try/finally or guard-reset:

 async function getKey(retry = true): Promise<void> {
   isLoading.value = true
-  if (!main?.user?.id)
-    return
+  if (!main?.user?.id) {
+    isLoading.value = false
+    return
+  }
   const { data, error } = await supabase
@@
-  isLoading.value = false
+  isLoading.value = false
 }

Also applies to: 157-158

src/components/dashboard/UsageCard.vue (2)

30-33: Missing import for useOrganizationStore

useOrganizationStore is used but not imported, causing a runtime error.

 import { getDaysInCurrentMonth } from '~/services/date'
 import { useMainStore } from '~/stores/main'
+import { useOrganizationStore } from '~/stores/organization'

139-141: Undefined template identifier 'unit'

Template references unit, but only props.unit exists.

-          {{ total?.toLocaleString() }} {{ unit }}
+          {{ total?.toLocaleString() }} {{ props.unit }}
src/services/stripe.ts (1)

42-45: Supabase getSession misuse

getSession returns an object; current truthy check can pass when there is no session. Destructure properly.

-  const session = await supabase.auth.getSession()
-  if (!session)
-    return
+  const { data: { session }, error: sessionErr } = await supabase.auth.getSession()
+  if (sessionErr || !session)
+    return
@@
-  const session = await supabase.auth.getSession()
-  if (!session)
-    return
+  const { data: { session }, error: sessionErr2 } = await supabase.auth.getSession()
+  if (sessionErr2 || !session)
+    return

Also applies to: 79-85

src/pages/settings/Account.vue (1)

441-446: MFA handler checks wrong variable on unenroll

Checks error from enroll instead of unregisterError.

-    if (error)
-      console.error('Cannot unregister MFA', unregisterError)
+    if (unregisterError)
+      console.error('Cannot unregister MFA', unregisterError)
@@
-  if (didCancel2) {
+  if (didCancel2) {
     // User closed the window, go ahead and unregister mfa
     const { error: unregisterError } = await supabase.auth.mfa.unenroll({ factorId: data.id })
-    if (error)
-      console.error('Cannot unregister MFA', unregisterError)
+    if (unregisterError)
+      console.error('Cannot unregister MFA', unregisterError)
   }

Also applies to: 489-494

src/components/Table.vue (1)

103-110: URL filter params logic drops previously set filters.

delete('filter', key) wipes all “filter” params; subsequent appends in the same loop may be lost. Clear once, then append only truthy filters.

-  if (props.filters) {
-    Object.entries(props.filters).forEach(([key, value]) => {
-      if (value)
-        params.append('filter', key)
-      else
-        params.delete('filter', key)
-    })
-  }
+  if (props.filters) {
+    // Reset then append active filters
+    params.delete('filter')
+    Object.entries(props.filters).forEach(([key, value]) => {
+      if (value)
+        params.append('filter', key)
+    })
+  }
src/pages/app/p/[package].channel.[channel].vue (1)

600-602: Untranslated user-facing string

toast.info('ignore-compatibility') will show raw English key/string. Use i18n.

-  else if (localDependencies.length === 0) {
-    toast.info('ignore-compatibility')
-  }
+  else if (localDependencies.length === 0) {
+    toast.info(t('ignore-compatibility'))
+  }
♻️ Duplicate comments (5)
messages/vi.json (1)

20-20: Account restore string now uses a placeholder — fixed

The earlier malformed anchor is gone; {link} placeholder is safer and consistent with other locales. Nice fix.

src/pages/settings/Account.vue (2)

40-104: Make canProceed meaningful; guard RPC results

members may be null/undefined; also canProceed always true even on errors.

 async function checkOrganizationImpact() {
@@
-  const orgsToBeDeleted: string[] = []
-  const paidOrgsToBeDeleted: Array<{ name: string, planName: string, orgId: string }> = []
+  const orgsToBeDeleted: string[] = []
+  const paidOrgsToBeDeleted: Array<{ name: string, planName: string, orgId: string }> = []
+  let hadError = false
@@
-      if (error) {
+      if (error) {
         console.error('Error getting org members:', error)
+        hadError = true
         continue
       }
@@
-      const superAdminCount = members.filter(member =>
+      const list = Array.isArray(members) ? members : []
+      const superAdminCount = list.filter(member =>
         member.role === 'super_admin' && !member.is_tmp,
       ).length
@@
-        try {
+        try {
           const isPaying = await isPayingOrg(org.gid)
           if (isPaying) {
             const planNameFromDb = await getCurrentPlanNameOrg(org.gid)
@@
-        catch (error) {
+        catch (error) {
           console.error('Error checking payment status for org:', org.name, error)
+          hadError = true
         }
       }
@@
-  return { orgsToBeDeleted, paidOrgsToBeDeleted, canProceed: true }
+  return { orgsToBeDeleted, paidOrgsToBeDeleted, canProceed: !hadError }

179-198: Localize final confirmation content; use new i18n keys

Final dialog uses hardcoded English via Teleport.

   dialogStore.openDialog({
     title: t('are-u-sure'),
-    description: '', // We'll use Teleport for custom content
+    description: '', // Teleport below shows localized content
@@
-    <Teleport v-if="dialogStore.showDialog && dialogStore.dialogOptions?.title === t('are-u-sure')" to="#dialog-v2-content" defer>
-      <div class="text-base text-gray-500 dark:text-gray-400">
-        <p class="mb-4">
-          This action cannot be undone. Your account and all associated data will be permanently deleted.
-        </p>
-        <p class="font-medium text-gray-700 dark:text-gray-300">
-          Your account will be deleted after 30 days
-        </p>
-      </div>
-    </Teleport>
+    <Teleport v-if="dialogStore.showDialog && dialogStore.dialogOptions?.title === t('are-u-sure')" to="#dialog-v2-content" defer>
+      <div class="text-base text-gray-500 dark:text-gray-400">
+        <p class="mb-4" v-html="t('account-deletion-final-warning')" />
+        <p class="font-medium text-gray-700 dark:text-gray-300">
+          {{ t('account-deletion-timer-in') }} 30 {{ t('days') || 'days' }}
+        </p>
+      </div>
+    </Teleport>

Optional: fetch exact date via a get_account_removal_date RPC and render it instead of “30”.

Also applies to: 730-739

messages/en.json (1)

182-193: Add rel="noopener noreferrer" to external links opened in new tabs.

Prevents reverse‑tabnabbing and is a low-effort hardening. Also keep HTML in i18n minimal/safe.

-  "cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>capgo support</a> and check browser logs",
+  "cannot-find-unknown-version": "Cannot find unknown version, please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a> and check browser logs",

-  "cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "cannot-get-apps": "Cannot get apps. Please check the browser console and contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

-  "cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "cannot-invite-user": "Cannot invite user to organization. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",

-  "cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",
+  "cannot-rescind-invitation": "Cannot rescind invitation, please look in the browser console and report this error to the <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support team</a>",

-  "cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",
+  "cannot-transfer-app": "Cannot transfer organization. Please check the browser console and report the error to <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>.",

-  "error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
+  "error-message-invitation": "Error happened. Please contact <a href='https://support.capgo.app/' target='_blank' rel='noopener noreferrer' class='text-blue-500 underline hover:text-blue-600'>Capgo support</a>",
#!/bin/bash
# Find external anchors with target="_blank" missing rel="noopener noreferrer" across locales
rg -nP --glob 'messages/*.json' "<a[^>]*target=['\"]_blank['\"][^>]*>" -C0 | rg -nPv "rel=['\"]noopener noreferrer['\"]"

Also applies to: 342-342

src/components/Table.vue (1)

423-428: Add ARIA label/title to icon-only button for a11y.

Improves screen-reader usability. Also ensure click handler doesn’t create a no-op function unnecessarily.

-                      <button
-                        class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
-                        @click.stop="col.onClick ? col.onClick(elem) : () => {}"
-                      >
+                      <button
+                        class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded-md cursor-pointer"
+                        :aria-label="col.ariaLabel ?? col.label ?? 'action'"
+                        :title="col.ariaLabel ?? col.label ?? 'action'"
+                        @click.stop="col.onClick ? col.onClick(elem) : null"
+                      >
                         <component :is="col.icon" />
                       </button>
🧹 Nitpick comments (56)
src/components/TabSidebar.vue (5)

28-39: Watch only activeTab and avoid redundant navigations

Watching the whole props is deep and noisy; also router.push may re-push the same path.

Apply:

-watch(props, (p) => {
-  // console.log('activeTab', p.activeTab)
-  const tab = findTab(p.activeTab)
-  if (!tab || props.noRoute)
-    return
-  if (tab.onClick) {
-    tab.onClick(p.activeTab)
-  }
-  else {
-    router.push(tab.key)
-  }
-})
+watch(() => props.activeTab, (active) => {
+  const tab = findTab(active)
+  if (!tab || props.noRoute)
+    return
+  if (tab.onClick)
+    tab.onClick(active)
+  else if (router.currentRoute.value.path !== tab.key)
+    router.push(tab.key)
+})

40-42: Guard update emission to prevent churn

Emit only when the value actually changes.

Apply:

-watch(router.currentRoute, (p) => {
-  emit('update:activeTab', p.path)
-})
+watch(router.currentRoute, (p) => {
+  if (p.path !== props.activeTab)
+    emit('update:activeTab', p.path)
+})

43-48: Drop noisy console.log (and consider removing onMounted entirely)

Remove debug log; optional: rely on { immediate: true } on one of the watches and delete this block.

Apply:

 onMounted(() => {
   if (props.activeTab && props.activeTab !== router.currentRoute.value.path) {
-    console.log('activeTab', props.activeTab)
     openLink(props.activeTab)
   }
 })

60-61: Use stable keys and prefer id from key, not label

Index keys cause unnecessary re-renders; labels may not be unique and can contain punctuation.

Apply:

-              <li v-for="(m, i) in tabs" :key="i" class="mr-0.5 md:mb-0.5 md:mr-0 w-full" @click="openLink(m.key)">
-                <button :id="`tab-${m.label}`" class="flex items-center whitespace-nowrap rounded-sm px-2.5 py-2 hover:bg-gray-400 w-full first-letter:uppercase cursor-pointer" :class="{ 'text-blue-600 hover:text-blue-800': isActive(m.key), 'text-slate-400 hover:text-slate-100': !isActive(m.key) }">
+              <li v-for="m in tabs" :key="m.key" class="mr-0.5 md:mb-0.5 md:mr-0 w-full" @click="openLink(m.key)">
+                <button :id="`tab-${m.key}`" class="flex items-center whitespace-nowrap rounded-sm px-2.5 py-2 hover:bg-gray-400 w-full first-letter:uppercase cursor-pointer" :class="{ 'text-blue-600 hover:text-blue-800': isActive(m.key), 'text-slate-400 hover:text-slate-100': !isActive(m.key) }">

70-70: Class on doesn’t style slotted content

If styling is intended, wrap the slot.

Apply:

-        <slot class="h-full overflow-y-scroll" />
+        <div class="h-full overflow-y-scroll">
+          <slot />
+        </div>
src/pages/forgot_password.vue (2)

42-42: De-duplicate loading state management (optional)

With submit() guarding isLoading, these lines become redundant.

Apply:

-  isLoading.value = false

Also applies to: 105-105


67-70: Fix misleading log when no verified factor is found

The console references mfaError even though the error is about an absent verified factor.

Apply:

-      setErrors('forgot-password', ['Cannot find MFA factor'], {})
-      console.error('Cannot get MFA factors', mfaError)
+      setErrors('forgot-password', ['Cannot find a verified MFA factor'], {})
+      console.error('No verified MFA factor found')
src/pages/resend_email.vue (1)

74-76: Localize hard-coded “Submit” label

Use a translation key for consistency with the rest of the page.

-                      <div v-if="!isLoading" class="inline-flex items-center justify-center w-full px-4 py-4 text-base font-semibold text-white transition-all duration-200 border border-transparent rounded-md bg-muted-blue-700 focus:bg-blue-700 hover:bg-blue-700 focus:outline-hidden">
-                        Submit
-                      </div>
+                      <div v-if="!isLoading" class="inline-flex items-center justify-center w-full px-4 py-4 text-base font-semibold text-white transition-all duration-200 border border-transparent rounded-md bg-muted-blue-700 focus:bg-blue-700 hover:bg-blue-700 focus:outline-hidden">
+                        {{ t('submit') }}
+                      </div>
src/pages/settings/Notifications.vue (2)

19-34: Avoid manual flip; use the emitted value and only commit on success

Current handlers invert state locally before the API call and ignore failures. Prefer accepting the new value from Toggle and persisting only when updateUser succeeds.

Apply this diff:

-async function submitNotif() {
-  isLoading.value = true
-  enableNotifications.value = !enableNotifications.value
+async function submitNotif(newValue: boolean) {
+  isLoading.value = true
   const activation = main.auth?.user_metadata?.activation || {}
   const { data, error } = await supabase.auth.updateUser({
     data: {
       activation: {
         ...activation,
-        enableNotifications: enableNotifications.value,
+        enableNotifications: newValue,
       },
     },
   })
-  if (!error && data)
-    main.auth = data.user
+  if (!error && data) {
+    enableNotifications.value = newValue
+    main.auth = data.user
+  }
   isLoading.value = false
 }
-async function submitDoi() {
-  isLoading.value = true
-  console.log('submitDoi')
-
-  optForNewsletters.value = !optForNewsletters.value
+async function submitDoi(newValue: boolean) {
+  isLoading.value = true
   const activation = main.auth?.user_metadata?.activation || {}
   const { data, error } = await supabase.auth.updateUser({
     data: {
       activation: {
         ...activation,
-        optForNewsletters: optForNewsletters.value,
+        optForNewsletters: newValue,
       },
     },
   })
-  if (!error && data)
-    main.auth = data.user
-  console.log('main.auth', data)
+  if (!error && data) {
+    optForNewsletters.value = newValue
+    main.auth = data.user
+  }
   isLoading.value = false
 }

And in template:

-              <Toggle
-                :value="enableNotifications"
-                @change="submitNotif()"
-              />
+              <Toggle :value="enableNotifications" @change="submitNotif($event)" />
...
-              <Toggle
-                :value="optForNewsletters"
-                @change="submitDoi()"
-              />
+              <Toggle :value="optForNewsletters" @change="submitDoi($event)" />

Also applies to: 68-77


37-37: Remove debug logs

Leftover console.log calls.

Apply this diff:

-  console.log('submitDoi')
...
-  console.log('main.auth', data)

Also applies to: 51-51

src/pages/onboarding/verify_email.vue (2)

44-55: Upsert instead of insert to avoid duplicate-key failures

If users row already exists (e.g., retries), insert will fail. Use upsert on id.

Apply this diff:

-  const { error } = await supabase
-    .from('users')
-    .insert(
+  const { error } = await supabase
+    .from('users')
+    .upsert(
       {
         id: user.value?.id,
         first_name: user.value?.user_metadata.first_name,
         last_name: user.value?.user_metadata.last_name,
         email: user.value?.email ?? '',
         image_url: '',
-      },
-    )
+      },
+      { onConflict: 'id' },
+    )

69-75: Avoid repeated delayed calls; trigger once on mount

watchEffect + setTimeout can schedule multiple updateDb calls. Prefer onMounted with a single timeout or watch with immediate: true.

Apply this diff:

-watchEffect(() => {
-  if (route.path === '/onboarding/verify_email') {
-    setTimeout(async () => {
-      await updateDb()
-    }, 500)
-  }
-})
+onMounted(() => {
+  if (route.path === '/onboarding/verify_email')
+    setTimeout(updateDb, 500)
+})
src/components/tables/LogTable.vue (1)

106-121: Harden external call with timeout

Add a ky timeout to avoid hanging requests when the stats API is slow.

Apply this diff:

-    const dataD = await ky
-      .post(`${defaultApiHost}/private/stats`, {
+    const dataD = await ky
+      .post(`${defaultApiHost}/private/stats`, {
         headers: {
           'Content-Type': 'application/json',
           'authorization': `Bearer ${currentJwt ?? ''}`,
         },
+        timeout: 10000,
         body: JSON.stringify({
           appId: props.appId,
           devicesId: props.deviceId ? [props.deviceId] : undefined,
           search: search.value ? search.value : undefined,
           order: columns.value.filter(elem => elem.sortable).map(elem => ({ key: elem.key as string, sortable: elem.sortable })),
           rangeStart: paginatedRange.value.rangeStart,
           rangeEnd: paginatedRange.value.rangeEnd,
         }),
       })
src/components/tables/HistoryTable.vue (2)

33-37: Show actual deployer; select email and/or display name

Column uses user?.email but the query doesn’t fetch email and the type doesn’t include it.

Apply this diff:

   user?: {
     id: string
     first_name: string
     last_name: string
+    email?: string
   }
   {
     label: t('deployed-by'),
     key: 'created_by',
     mobile: false,
-    displayFunction: item => item.user?.email || '-',
+    displayFunction: item => item.user?.email
+      ?? [item.user?.first_name, item.user?.last_name].filter(Boolean).join(' ')
+      || '-',
   },
       `,
         ),
-        user:created_by (
-          id,
-          first_name,
-          last_name
-        )
+        user:created_by (
+          id,
+          first_name,
+          last_name,
+          email
+        )
       `, { count: 'exact' })

Also applies to: 125-126, 174-192


279-281: Trigger initial fetch

watch without immediate won’t run on first render. Make it immediate.

Apply this diff:

-watch([() => props.channelId, () => props.appId], () => {
-  fetchDeployHistory()
-})
+watch([() => props.channelId, () => props.appId], () => {
+  fetchDeployHistory()
+}, { immediate: true })
src/components/MobileStats.vue (2)

55-61: Remove debug log

Minor cleanup.

Apply this diff:

-  console.log('loadData mobile data')

85-92: Locale-aware date formatting

Currently uses fixed 'MMMM D, YYYY HH:mm' (English month names). Consider Intl.DateTimeFormat with current locale or dayjs.locale(locale.value).

src/components/tables/ChannelTable.vue (2)

328-331: Watch props explicitly or use immediate

Watching the reactive props object can be brittle. Watch specific keys and run immediately.

Apply this diff:

-watch(props, async () => {
-  await refreshData()
-})
+watch([() => props.appId], async () => {
+  await refreshData()
+}, { immediate: true })

339-339: i18n for static label

Hardcode 'Filters' -> t('filters') for consistency.

Apply this diff:

-      filter-text="Filters"
+      :filter-text="t('filters')"
src/pages/app/p/[package].vue (3)

76-76: Localize hardcoded hover tooltips

These user-facing strings should go through i18n.

Proposed change:

-    hoverLabel: 'Click to explore the channel list',
+    hoverLabel: t('click-to-explore-channel-list'),
-    hoverLabel: 'Click to explore the bundle list',
+    hoverLabel: t('click-to-explore-bundle-list'),
-    hoverLabel: 'Click to explore the device list',
+    hoverLabel: t('click-to-explore-device-list'),
-    hoverLabel: 'Click to explore the logs',
+    hoverLabel: t('click-to-explore-logs'),

I can add these keys across locales if you want.

Also applies to: 82-82, 88-88, 94-94


108-113: Parallelize independent awaits in loadAppInfo

capgoVersion, updatesNb, and devicesNb can be fetched concurrently to reduce latency.

-    const promises = []
-    capgoVersion.value = await getCapgoVersion(id.value, app.value?.last_version)
-    updatesNb.value = await main.getTotalStatsByApp(id.value, organizationStore.currentOrganization?.subscription_start)
-    devicesNb.value = await main.getTotalMauByApp(id.value, organizationStore.currentOrganization?.subscription_start)
+    const promises = []
+    const [version, updates, devices] = await Promise.all([
+      getCapgoVersion(id.value, app.value?.last_version),
+      main.getTotalStatsByApp(id.value, organizationStore.currentOrganization?.subscription_start),
+      main.getTotalMauByApp(id.value, organizationStore.currentOrganization?.subscription_start),
+    ])
+    capgoVersion.value = version
+    updatesNb.value = updates
+    devicesNb.value = devices

4-4: Avoid reactive churn when syncing tab to query

Use watch on ActiveTab instead of a broad watchEffect, and import watch.

-import { computed, ref, watchEffect } from 'vue'
+import { computed, ref, watch, watchEffect } from 'vue'
-watchEffect(() => {
-  router.replace({ query: { ...route.query, tab: ActiveTab.value } })
-})
+watch(() => ActiveTab.value, (val) => {
+  router.replace({ query: { ...route.query, tab: val } })
+})

Also applies to: 168-170

src/components/dashboard/DropdownProfile.vue (2)

62-62: Drop unnecessary await on useSupabase()

useSupabase appears synchronous; awaiting it is redundant.

-  const supabase = await useSupabase()
+  const supabase = useSupabase()

106-114: Use success toast when unspoofing

Unspoof is a success path; show a success toast instead of error.

-  if (unspoofUser()) {
-    toast.error('Stop Spoofed, will reload')
+  if (unspoofUser()) {
+    toast.success('Stop Spoofed, will reload')

Also consider localizing all toast strings in this component.

src/pages/app/modules_test.vue (2)

147-147: Conform to repo code style (no semicolons)

Remove the trailing semicolon to align with @antfu/eslint-config.

-  toast.success(`runMethod: ${JSON.stringify(m)}`);
+  toast.success(`runMethod: ${JSON.stringify(m)}`)

147-158: Localize toast messages

To keep UX consistent with the i18n migration, route these toasts through t().

I can draft keys like run-method, res-method, err-method and wire them here if you want.

src/pages/invitation.vue (1)

175-176: Localize remaining headings

These headings are plain text; switch to t(...) for consistency.

-                  <h2 class="mb-3 text-lg font-medium text-center text-gray-700 dark:text-gray-300">
-                    Organization
-                  </h2>
+                  <h2 class="mb-3 text-lg font-medium text-center text-gray-700 dark:text-gray-300">
+                    {{ t('organization') }}
+                  </h2>
-                  <h2 class="mb-3 text-lg font-medium text-center text-gray-700 dark:text-gray-300">
-                    Your login details
-                  </h2>
+                  <h2 class="mb-3 text-lg font-medium text-center text-gray-700 dark:text-gray-300">
+                    {{ t('your-login-details') }}
+                  </h2>
-                      <h3 class="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
-                        Password Requirements:
-                      </h3>
+                      <h3 class="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
+                        {{ t('password-requirements') }}
+                      </h3>

I can add these keys to all locales.

Also applies to: 199-201, 215-216

src/components/dashboard/AppCard.vue (1)

24-26: Rename typo: openSettngs → openSettings

Minor readability nit; rename function and its usages.

-function openSettngs(app: Database['public']['Tables']['apps']['Row']) {
+function openSettings(app: Database['public']['Tables']['apps']['Row']) {
   router.push(`/app/p/${appIdToUrl(app.app_id)}/settings`)
 }
...
-        class="flex items-center p-3 mx-auto truncate rounded-lg hover:bg-gray-400 hover:text-white" @click.stop="openSettngs(app)"
+        class="flex items-center p-3 mx-auto truncate rounded-lg hover:bg-gray-400 hover:text-white" @click.stop="openSettings(app)"

Also applies to: 120-123

src/modules/i18n.ts (2)

9-13: Set explicit defaults for locale and fallback

Avoids empty-locale flicker and provides sane fallback.

-export const i18n = createI18n({
-  legacy: false,
-  locale: '',
-  messages: {},
-})
+export const i18n = createI18n({
+  legacy: false,
+  locale: 'en',
+  fallbackLocale: 'en',
+  messages: {},
+})

66-69: Improve locale detection for regional variants

Use full navigator.language, then fall back to base and finally to 'en'.

-  let lang = localStorage.getItem('lang') ?? window.navigator.language.split('-')[0]
-  if (!(lang in languages))
-    lang = 'en'
+  let lang = localStorage.getItem('lang') ?? window.navigator.language
+  if (!(lang in languages)) {
+    const base = lang.split('-')[0]
+    lang = (Object.keys(languages).find(k => k.startsWith(base)) ?? 'en')
+  }
src/pages/delete_account.vue (1)

52-66: Align with staged deletion workflow

This still calls delete_user (immediate delete). Given the new 30‑day staged deletion, switch to the new RPC that marks the account for deletion and redirect to the disabled page (e.g., /accountDisabled) with a restoration option.

I can draft the exact changes once you confirm the RPC/route names (e.g., mark_account_for_deletion, get_account_removal_date).

src/pages/settings/organization/Plans.vue (1)

134-135: Remove debug log

Stray console.log in pricing path.

-  getCurrentPlanNameOrg(orgId).then((res) => {
-    console.log('getCurrentPlanNameOrg', res)
+  getCurrentPlanNameOrg(orgId).then((res) => {
src/components/tables/BundleTable.vue (3)

178-188: Guard empty arrays before .in() to avoid PostgREST errors

When dataVersions is empty, .in('id', []) can error. Short-circuit early.

 async function enhenceVersionElems(dataVersions: Database['public']['Tables']['app_versions']['Row'][]) {
-  const { data: dataVersionsMeta } = await supabase
+  if (!dataVersions?.length)
+    return [] as Element[]
+  const { data: dataVersionsMeta } = await supabase

251-258: Same empty-array guard for channel lookup

Avoid .in('version', []) calls.

 async function fetchChannelsForVersions(versions: Element[]) {
-  const versionIds = versions.map(v => v.id)
+  const versionIds = versions.map(v => v.id)
+  if (!versionIds.length) return

178-178: Nit: fix typo in function name

enhenceVersionElems → enhanceVersionElems.

src/pages/ApiKeys.vue (1)

96-154: Batch fetch orgs/apps instead of N single queries

Use a single .in() per table for uncached IDs to cut round-trips and speed large lists. Happy to draft a patch if desired.

vite.config.mts (1)

150-157: Optional: consider removing include entirely if you load messages at runtime

If you dynamically load messages (e.g., via HTTP or import()), you can drop include to speed the build.

src/components/dashboard/StepsBundle.vue (1)

41-51: Static i18n strings won’t react to locale changes

steps is initialized with t() once; switching language won’t update titles.

Compute steps from a computed(() => [...]) that uses t() so it re-runs on locale changes.

src/pages/app/p/[package].bundle.[bundle].vue (3)

247-252: Localize toast message

Literal 'ignore-compatibility' should use i18n.

-      toast.info('ignore-compatibility')
+      toast.info(t('ignore-compatibility'))

722-746: Duplicate id attribute 'open-channel'

The id is used twice (v-if/v-else), producing duplicate IDs.

Remove the id or make them unique (e.g., open-channel-list / open-channel-set) to avoid DOM conflicts.


325-333: Minor: replace single slash with global replace in filename

replace('/') only replaces the first occurrence.

-    const filename = version.value.r2_path?.replace('/', '_')
+    const filename = version.value.r2_path?.replaceAll('/', '_')
src/services/stripe.ts (2)

20-25: Prevent tabnabbing with window.open

Harden new-tab opens by clearing opener.

-        handler: async () => {
-          window.open(url, '_blank')
-        },
+        handler: async () => {
+          const w = window.open(url, '_blank')
+          if (w) w.opener = null
+        },
-  else
-    window.open(link, '_blank')
+  else {
+    const w = window.open(link, '_blank')
+    if (w) w.opener = null
+  }

Also applies to: 31-36


69-72: Localize error toasts

Use existing i18n error strings.

-          else
-            toast.error('Cannot open your portal')
+          else
+            toast.error(t('something-went-wrong-try-again-later'))
-    toast.error('Cannot get your checkout')
+    toast.error(t('something-went-wrong-try-again-later'))

Also applies to: 100-101

messages/vi.json (1)

518-519: Vietnamese phrasing tweaks for clarity

  • “Các tổ chức bị xóa” ⇒ “Các tổ chức sẽ bị xóa”.
  • “Đăng ký bị hủy” ⇒ “Đăng ký cần hủy”.
-"organizations-to-be-deleted": "Các tổ chức bị xóa",
+"organizations-to-be-deleted": "Các tổ chức sẽ bị xóa",
@@
-"paid-subscriptions-to-cancel": "Đăng ký bị hủy",
+"paid-subscriptions-to-cancel": "Đăng ký cần hủy",

Also applies to: 523-523

src/pages/settings/Account.vue (1)

152-176: Subscription cancellation: surface as explicit TODO or block proceed

Currently continues after warning without performing cancellation.

Consider blocking deletion until cancellations succeed, or add a visible TODO in UI. I can help wire Stripe portal links or backend cancellation hooks.

messages/en.json (2)

20-23: Clarify countdown copy to explicitly say “deleted”.

Slightly clearer and consistent with the irreversible deletion language elsewhere.

-  "account-deletion-timer": "Your account will cease to exist",
-  "account-deletion-timer-in": "Your account will cease to exist in",
+  "account-deletion-timer": "Your account will be permanently deleted",
+  "account-deletion-timer-in": "Your account will be permanently deleted in",

522-522: US English consistency: “canceled” (1 L).

Most EN strings use US spelling; align this key.

-  "paid-subscriptions-to-cancel": "Subscriptions to be cancelled",
+  "paid-subscriptions-to-cancel": "Subscriptions to be canceled",
src/components/tables/AppTable.vue (1)

56-73: Use lazy-loading for app icons.

Reduce bandwidth and improve list performance; harmless fallback if unsupported.

-        ? h('img', {
+        ? h('img', {
             src: item.icon_url,
             alt: `App icon ${item.name}`,
             class: 'mr-2 rounded-sm shrink-0 sm:mr-3 d-mask d-mask-squircle',
             width: 42,
             height: 42,
+            loading: 'lazy',
+            decoding: 'async',
           })
src/components/Table.vue (1)

262-265: Watch source should be a getter to track prop changes.

watch(props.elementList, …) evaluates once; use a getter for reactivity.

-watch(props.elementList, () => {
+watch(() => props.elementList, () => {
   selectedRows.value = props.elementList.map(_ => false)
   previousSelectedRow.value = null
-})
+}, { deep: false })
src/pages/app/p/[package].channel.[channel].vue (7)

65-67: Avoid reactive feedback loop risk when syncing tab to route

Using watchEffect here captures route.query and can retrigger itself. Watch ActiveTab directly.

-watchEffect(() => {
-  router.replace({ query: { ...route.query, tab: ActiveTab.value } })
-})
+watch(ActiveTab, tab => {
+  router.replace({ query: { ...route.query, tab } })
+})

185-191: Harden window.open against reverse tabnabbing

Add noopener,noreferrer in features; keeping opener = null is fine as a belt-and-suspenders.

-    const win = window.open(url, '_blank')
+    const win = window.open(url, '_blank', 'noopener,noreferrer')

581-586: Use i18n interpolation instead of manual .replace

Manual string replacement breaks localization. Prefer named placeholders.

-    toast.info(t('channel-not-compatible-with-channel-description').replace('%', 'npx @capgo/cli@latest bundle compatibility'))
+    toast.info(t('channel-not-compatible-with-channel-description', { cmd: 'npx @capgo/cli@latest bundle compatibility' }))
...
-      description: t('set-even-not-compatible').replace('%', 'npx @capgo/cli@latest bundle compatibility'),
+      description: t('set-even-not-compatible', { cmd: 'npx @capgo/cli@latest bundle compatibility' }),

Ensure the locale messages use something like "… {cmd} …".


519-521: Local reactive state may not update when value is falsy

Set disable_auto_update unconditionally after the update to keep UI in sync.

-  if (channel.value?.disable_auto_update)
-    channel.value.disable_auto_update = value
+  if (channel.value)
+    channel.value.disable_auto_update = value

463-470: Variable shadowing: local id hides the outer ref id

Rename for clarity.

-          const id = await getUnknownVersion()
-          if (!id)
+          const unknownVersionId = await getUnknownVersion()
+          if (!unknownVersionId)
             return
-          saveChannelChange('version', id)
+          saveChannelChange('version', unknownVersionId)

204-205: Remove or downgrade debug logs

Stray console.log should be removed or replace error cases with console.error.

-  console.log(role.value)
+  // role value available if needed for debugging

-  console.log('openBundle', channel.value.version.id)

-  console.log('saveChannelChange', key, val)

-              console.log('error', iosError ?? hiddenError)
+              console.error('error', iosError ?? hiddenError)

Also applies to: 229-230, 309-309, 391-393


108-121: Platform inference from UUID casing is brittle

Equal counts (e.g., mostly digits) forces an error. Consider explicitly asking the user to choose platform when ambiguous.

I can draft a small platform selection dialog if you want.

Comment thread src/components/Table.vue
Comment on lines +354 to 358
<button v-if="pisSelectAllEnabled" class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="selectedRows = selectedRows.map(() => true); emit('selectRow', selectedRows)">
<span class="text-sm">{{ t('select_all') }}</span>
</button>
<button v-if="props.massSelect && selectedRows.find(val => val)" class=" self-end mr-2 inline-flex items-center border border-gray-300 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="emit('massDelete')">
<button v-if="isSelectAllEnabled" class=" self-end mr-2 inline-flex items-center border border-gray-300 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="emit('massDelete')">
<IconTrash class="text-red-500 h-[24px]" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Build failure: typo in template variable (“pisSelectAllEnabled”).

Breaks vue-tsc and hides “Select all” control.

-      <button v-if="pisSelectAllEnabled" class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="selectedRows = selectedRows.map(() => true); emit('selectRow', selectedRows)">
+      <button v-if="isSelectAllEnabled" class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="selectedRows = selectedRows.map(() => true); emit('selectRow', selectedRows)">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button v-if="pisSelectAllEnabled" class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="selectedRows = selectedRows.map(() => true); emit('selectRow', selectedRows)">
<span class="text-sm">{{ t('select_all') }}</span>
</button>
<button v-if="props.massSelect && selectedRows.find(val => val)" class=" self-end mr-2 inline-flex items-center border border-gray-300 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="emit('massDelete')">
<button v-if="isSelectAllEnabled" class=" self-end mr-2 inline-flex items-center border border-gray-300 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer" type="button" @click="emit('massDelete')">
<IconTrash class="text-red-500 h-[24px]" />
<button
v-if="isSelectAllEnabled"
class="inline-flex items-center self-end px-3 py-2 ml-auto mr-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer"
type="button"
@click="selectedRows = selectedRows.map(() => true); emit('selectRow', selectedRows)"
>
<span class="text-sm">{{ t('select_all') }}</span>
</button>
<button
v-if="isSelectAllEnabled"
class="self-end mr-2 inline-flex items-center border border-gray-300 rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-white focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700 cursor-pointer"
type="button"
@click="emit('massDelete')"
>
<IconTrash class="text-red-500 h-[24px]" />
</button>
🧰 Tools
🪛 GitHub Actions: Deploy Preview Environment

[error] 354-354: vue-tsc --noEmit: TS2551: Property 'pisSelectAllEnabled' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<Props, { reset: (...args: any[]) => void; reload: (...args: any[]) => void; add: (...args: any[]) => void; next: (...args: any[]) => void; prev: (...args: any[]) => void; fastForward: (...args: any[]) => void; fastBackward: (...args: any[]) => void; ... 6 more ...}'. Did you mean 'isSelectAllEnabled'?'

🤖 Prompt for AI Agents
In src/components/Table.vue around lines 354 to 358 the template uses a typo'd
prop/variable name "pisSelectAllEnabled" which causes vue-tsc build failure and
hides the "Select all" button; change "pisSelectAllEnabled" to the correct
"isSelectAllEnabled" (matching the other usage in the template/props) so the
conditional renders properly and rebuilds successfully.

Comment on lines +5 to 6
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for useRouter causes runtime error

useRouter is used but not imported.

Apply this diff:

 import { computed, ref, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
 import { toast } from 'vue-sonner'

Also applies to: 45-46

🤖 Prompt for AI Agents
In src/components/tables/HistoryTable.vue around lines 5-6 (and also affecting
lines 45-46), the runtime error is caused by using useRouter without importing
it; add the missing import for useRouter from 'vue-router' at the top of the
file and ensure any other places that reference useRouter have the proper import
or use the router instance from setup() so the symbol is defined before use.

Comment on lines 3 to 5
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

onMounted is used but not imported

This will break at runtime/build. Import it from vue.

Apply:

-import { watch } from 'vue'
+import { onMounted, watch } from 'vue'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
🤖 Prompt for AI Agents
In src/components/TabSidebar.vue around lines 3 to 5, onMounted is referenced in
the component but not imported from 'vue', which will cause build/runtime
errors; update the import from 'vue' to include onMounted (e.g., add onMounted
to the existing import alongside watch) so the lifecycle hook is available.

Comment on lines 6 to +8
import { useDebounceFn } from '@vueuse/core'
import { useI18n } from 'petite-vue-i18n'
import { ref, watch, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing imports for computed/onClickOutside/useTemplateRef cause runtime errors

computed (used at Lines 57, 61), onClickOutside (Line 55), and useTemplateRef (Line 53) are referenced but not imported. Unless you rely on auto-imports, this will break.

Apply this diff:

-import { useDebounceFn } from '@vueuse/core'
-import { ref, watch, watchEffect } from 'vue'
+import { onClickOutside, useDebounceFn } from '@vueuse/core'
+import { computed, ref, watch, watchEffect, useTemplateRef } from 'vue'

If you do use auto-imports, consider removing the explicit ref/watch/watchEffect import for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useDebounceFn } from '@vueuse/core'
import { useI18n } from 'petite-vue-i18n'
import { ref, watch, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { onClickOutside, useDebounceFn } from '@vueuse/core'
import { computed, ref, watch, watchEffect, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
🤖 Prompt for AI Agents
In src/pages/app/p/[package].channel.[channel].vue around lines 6 to 8, the file
references computed (lines ~57/61), onClickOutside (line ~55), and
useTemplateRef (line ~53) but does not import them; update the imports to
include computed from 'vue', onClickOutside from '@vueuse/core' and
useTemplateRef from '@vueuse/core' (or their correct source in your project),
and optionally consolidate/remove explicit ref/watch/watchEffect imports if you
rely on auto-imports so the import list is consistent.

Comment on lines 2 to 6
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSupabase } from '~/services/supabase'
import { useMainStore } from '~/stores/main'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Import missing: useDisplayStore not imported (runtime error risk)

useDisplayStore is used but not imported here (unlike verify_email.vue). If auto-import isn’t configured for stores, this will crash.

Apply this diff:

 import { ref } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useSupabase } from '~/services/supabase'
 import { useMainStore } from '~/stores/main'
+import { useDisplayStore } from '~/stores/display'

Also applies to: 13-14

🤖 Prompt for AI Agents
In src/pages/settings/Notifications.vue around lines 2 to 6 (and also lines
13-14), the component uses useDisplayStore but does not import it which will
cause a runtime crash if stores aren't auto-imported; add an explicit import for
useDisplayStore from the correct store module (e.g., import { useDisplayStore }
from '~/stores/display') at the top alongside the other imports, and ensure any
references to useDisplayStore in the later lines remain unchanged so they
resolve correctly.

Comment on lines +4 to 5
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for useRouter causes runtime error

useRouter is referenced but not imported.

Apply this diff:

 import { defineExpose, defineProps, ref } from 'vue'
 import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
 import { toast } from 'vue-sonner'

Also applies to: 12-13

🤖 Prompt for AI Agents
In src/pages/settings/organization/DeleteOrgDialog.vue around lines 4-5 (and
also in the nearby import block at lines 12-13), the composition API function
useRouter is referenced but not imported; add an import for useRouter from
'vue-router' to the imports list so the runtime reference resolves, and update
any related import ordering or grouping as needed.

Comment on lines 5 to 7
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing imports: watch/useDisplayStore cause runtime errors

watch() and useDisplayStore() are used but not imported.

-import { computed, onMounted, ref } from 'vue'
+import { computed, onMounted, ref, watch } from 'vue'
+import { useDisplayStore } from '~/stores/display'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
<script setup lang='ts'>
import { computed, onMounted, ref, watch } from 'vue'
import { useDisplayStore } from '~/stores/display'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
// …rest of your setup code…
</script>
🤖 Prompt for AI Agents
In src/pages/settings/organization/index.vue lines 5-7, the file imports
computed, onMounted, ref but not watch and also references useDisplayStore
without importing it; add watch to the Vue import (e.g., import { computed,
onMounted, ref, watch } from 'vue') and import useDisplayStore from the display
store module (e.g., import { useDisplayStore } from '@/stores/display' or the
correct relative path to your store) so both symbols are available at runtime.

@riderx riderx merged commit 5a0454a into main Sep 9, 2025
21 of 22 checks passed
@riderx riderx deleted the better_account_delete branch September 9, 2025 18:36
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Sep 9, 2025

🧹 Preview Environment Cleaned Up

The preview environment for this PR has been successfully removed from Cloudflare.

Deleted:

  • Frontend: capgo-preview-1191
  • API Worker: capgo-api-preview-1191
  • Files Worker: capgo-files-preview-1191
  • Plugin Worker: capgo-plugin-preview-1191

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants