Skip to content

Comments

feat: add user account restore during 30-day grace period#1657

Open
ToriChanIntegration wants to merge 4 commits intoCap-go:mainfrom
ToriChanIntegration:feature/user-account-restore
Open

feat: add user account restore during 30-day grace period#1657
ToriChanIntegration wants to merge 4 commits intoCap-go:mainfrom
ToriChanIntegration:feature/user-account-restore

Conversation

@ToriChanIntegration
Copy link

@ToriChanIntegration ToriChanIntegration commented Feb 23, 2026

Summary

Allows users to restore their account during the 30-day deletion grace period.

Changes

  1. SQL Function (supabase/migrations/20260223000000_add_restore_user_function.sql):

    • Adds restore_user() RPC function
    • Removes user from to_delete_accounts table
    • Restores API keys from removed_data
  2. Frontend (src/pages/restore_account.vue):

    • New page similar to delete_account
    • Requires reauthentication to restore
    • Green button styling to indicate restoration
  3. Routing (src/main.ts):

    • Added /restore_account to guest paths
  4. Translations (messages/en.json):

    • Added keys for restore flow

Test Plan

Manual Testing

  1. Test account deletion:

    • Go to /delete_account
    • Enter email and password
    • Complete captcha
    • Confirm deletion
    • Verify account is added to to_delete_accounts table
  2. Test account restoration:

    • Within 30 days, go to /restore_account
    • Enter same email and password
    • Complete captcha
    • Confirm restoration
    • Verify account is removed from to_delete_accounts table
    • Verify API keys are restored

Automated Testing

The existing delete-user-reauth.test.ts can be extended to test the restore flow by:

  1. Creating a test user
  2. Marking them for deletion (insert into to_delete_accounts)
  3. Calling restore_user() RPC
  4. Verifying the user is restored

Usage

Users who requested deletion receive instructions about the 30-day grace period. During this time, they can visit /restore_account to cancel the deletion and restore their account.

Summary by CodeRabbit

  • New Features

    • Account restoration: Users can now restore their deleted accounts within a 30-day grace period following deletion.
  • Improvements

    • Enhanced account deletion and restoration flows with improved form validation and user confirmation dialogs.

Handle 429 (rate limit) and 500 (server error) with exponential backoff
to fix CI failures in POST /device operations test
- Each test now uses randomUUID() for device IDs
- No shared state between tests
- POST test now verifies device was created (uncommented)
- DELETE test creates its own device to delete
- All concurrent tests use unique identifiers
- Removes flaky retry logic
- Add restore_user() SQL function to allow users to cancel deletion
- Add restore_account.vue page for users to restore their account
- Add /restore_account route to main.ts
- Add translations for restore flow
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Adds an account restoration flow: new UI component and page for restore, Supabase RPC function to restore users and API keys, i18n string updates, route whitelist update, and isolated device test refactors.

Changes

Cohort / File(s) Summary
Internationalization
messages/en.json
Replaced several literal punctuation/emojis with Unicode escapes and added six new localization keys for the restore flow.
Routing & Page
src/main.ts, src/pages/restore_account.vue
Added /restore_account to guest routes and added a restore page that renders the AccountAction component in restore mode.
New Account UI Component
src/components/AccountAction.vue, src/pages/delete_account.vue
Introduced AccountAction component (delete/restore flows, form, captcha, reauth, RPC calls); delete_account.vue now uses this component.
Database Migration
supabase/migrations/20260223000000_add_restore_user_function.sql
Added public.restore_user() PL/pgSQL function to reinstate deleted users and their API keys from to_delete_accounts within the 30-day grace period.
Tests
tests/device.test.ts
Refactored device tests for per-test isolation using random UUIDs; removed fetchWithRetry import; added explicit create/verify/delete sequences.

Sequence Diagram

sequenceDiagram
    actor User
    participant RestorePage as Restore Page (Vue)
    participant Captcha as Turnstile Captcha
    participant Auth as Supabase Auth
    participant RPC as restore_user RPC
    participant DB as Database

    User->>RestorePage: Fill email & password
    RestorePage->>Captcha: Render widget
    User->>Captcha: Complete challenge
    Captcha->>RestorePage: Return token
    User->>RestorePage: Submit form
    RestorePage->>Auth: Sign-in (email/password, captcha)
    Auth->>RestorePage: Return session / claims
    RestorePage->>User: Show confirmation dialog
    User->>RestorePage: Confirm restore
    RestorePage->>RPC: Call restore_user()
    RPC->>DB: Lookup to_delete_accounts and restore API keys
    DB->>RPC: Return success
    RPC->>RestorePage: Return success
    RestorePage->>Auth: Sign out
    RestorePage->>User: Show success & redirect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • riderx

Poem

🐰 Hopping back through gates once closed,

A stitch of keys, a name reposed,
Captcha nods and RPC sings,
Restored accounts and brighter springs,
Welcome home — the rabbit brings a rose.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature added: allowing users to restore their account within a 30-day grace period after deletion.
Description check ✅ Passed The description covers the main changes, provides a detailed test plan with both manual and automated testing steps, and explains the feature's purpose and usage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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: 5

🧹 Nitpick comments (3)
tests/device.test.ts (1)

71-103: Consider using it.concurrent() for isolated tests.

Lines 73 and 105 use it() instead of it.concurrent(). Since each test generates its own unique deviceId via randomUUID(), they are fully isolated and safe to run in parallel. As per coding guidelines, "it.concurrent() instead of it() to run tests in parallel within the same test file."

♻️ Proposed change
-  it('link device', async () => {
+  it.concurrent('link device', async () => {

Same for unlink device at Line 135.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/device.test.ts` around lines 71 - 103, Change the tests that are safe
to run in parallel from it(...) to it.concurrent(...): locate the '[POST]
/device operations' tests named "link device" and "unlink device" (they generate
unique IDs with randomUUID()), and replace their it(...) declarations with
it.concurrent(...) so the tests run concurrently while still remaining isolated;
ensure any other tests in the same describe that share state are not converted.
src/pages/restore_account.vue (2)

67-72: getClaims() call and userId extraction are unnecessary.

The userId is extracted but never passed to the RPC call on Line 75. The SQL function restore_user() internally uses auth.uid() to identify the caller, making this block dead code.

♻️ Remove the unnecessary claims check
-            const { data: claimsData, error: claimsError } = await supabase.auth.getClaims()
-            const userId = claimsData?.claims?.sub
-            if (claimsError || !userId) {
-              isLoading.value = false
-              return setErrors('restore-account', [t('something-went-wrong-try-again-later')], {})
-            }
-
             // Restore user using RPC function
             const { error: restoreError } = await supabase.rpc('restore_user')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/restore_account.vue` around lines 67 - 72, Remove the unnecessary
supabase.auth.getClaims() call and its userId extraction/check: the SQL function
restore_user() uses auth.uid() so getClaims() and the guard that sets
isLoading.value = false and calls setErrors('restore-account', ...) are dead
code; delete the block that calls getClaims(), removes userId usage, and let the
subsequent supabase.rpc('restore_user') call handle errors as before (ensure
existing error handling around the rpc call remains intact).

148-151: restoreAccount() is not awaited — cleanup on dismiss may race.

The restoreAccount() call on Line 150 returns a promise (it awaits dialogStore.onDialogDismiss() internally), but the result is discarded. If the dialog handler errors or the user navigates away quickly, the finally block cleanup may not behave as expected.

♻️ Await the call
-    restoreAccount()
+    await restoreAccount()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/restore_account.vue` around lines 148 - 151, The call to
restoreAccount() is not awaited causing potential race with cleanup; update the
dismiss handler to await the promise returned by restoreAccount() (the function
that internally awaits dialogStore.onDialogDismiss()) and propagate or handle
errors (e.g., try/catch around await restoreAccount()) so the finally cleanup
runs only after restoreAccount completes; locate the invocation near
captchaComponent.value?.reset() and replace the fire-and-forget call with an
awaited call and proper error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/restore_account.vue`:
- Around line 195-199: The FormKit password input has a malformed :placeholder
attribute (in the FormKit component with id="passwordInput") —
:placeholder="t('password') is missing the closing double-quote; fix by adding
the missing closing quote so the attribute reads :placeholder="t('password')"
and verify the rest of the props (name="password", :label="t('password')",
:prefix-icon="iconPassword", :disabled="isLoading", validation="required:trim",
enterkeyhint="send", autocomplete="current-password") remain on the same element
and are properly quoted.
- Around line 134-142: The error handling uses setErrors('login-account', ...)
but the actual form id is "restore-account", so error messages never show;
update the setErrors call in the error branch (where setErrors is invoked) to
use 'restore-account' instead of 'login-account' so submitted errors target the
correct form (leave the captcha reset logic in captchaComponent.value?.reset()
and the toast calls unchanged).
- Around line 38-39: The handler currently declares an unused local variable
const supabaseClient = useSupabase() inside the handler; remove this unused
declaration so the handler uses the existing outer supabase instance (the outer
supabase variable used throughout the handler) or, if you intended to use the
local client, replace uses of the outer supabase with supabaseClient
consistently; target the handler function and the useSupabase() call to either
delete the local assignment or update usages accordingly.

In `@supabase/migrations/20260223000000_add_restore_user_function.sql`:
- Around line 66-68: Remove the unnecessary privilege granted to the anonymous
role by deleting the GRANT EXECUTE line for "anon" in the migration that deals
with the restore_user function; keep GRANT EXECUTE for "authenticated" and
"service_role" only so restore_user (the SECURITY DEFINER function that checks
auth.uid()) cannot be invoked by unauthenticated callers.
- Around line 32-56: The FOR loop is treating apikey_record as if it were a
jsonb rather than the RECORD returned by jsonb_array_elements (its field is
value), and there are invalid casts: replace apikey_record->>'key' etc. with
apikey_record.value->>'key' and apikey_record.value->'field' where appropriate;
change the enum cast from
(apikey_record->>'mode')::"public"."apikeys"."mode"%TYPE to the actual enum type
(apikey_record.value->>'mode')::"public"."key_mode"; build arrays properly using
ARRAY(SELECT jsonb_array_elements_text(apikey_record.value->'limited_to_apps'))
for text[] and ARRAY(SELECT
jsonb_array_elements_text(apikey_record.value->'limited_to_orgs')::uuid) for
uuid[] to match the apikeys.limited_to_orgs column; and keep the expires_at cast
as (apikey_record.value->>'expires_at')::timestamptz while inserting into
"public"."apikeys" with user_id_fn and the other fields.

---

Nitpick comments:
In `@src/pages/restore_account.vue`:
- Around line 67-72: Remove the unnecessary supabase.auth.getClaims() call and
its userId extraction/check: the SQL function restore_user() uses auth.uid() so
getClaims() and the guard that sets isLoading.value = false and calls
setErrors('restore-account', ...) are dead code; delete the block that calls
getClaims(), removes userId usage, and let the subsequent
supabase.rpc('restore_user') call handle errors as before (ensure existing error
handling around the rpc call remains intact).
- Around line 148-151: The call to restoreAccount() is not awaited causing
potential race with cleanup; update the dismiss handler to await the promise
returned by restoreAccount() (the function that internally awaits
dialogStore.onDialogDismiss()) and propagate or handle errors (e.g., try/catch
around await restoreAccount()) so the finally cleanup runs only after
restoreAccount completes; locate the invocation near
captchaComponent.value?.reset() and replace the fire-and-forget call with an
awaited call and proper error handling.

In `@tests/device.test.ts`:
- Around line 71-103: Change the tests that are safe to run in parallel from
it(...) to it.concurrent(...): locate the '[POST] /device operations' tests
named "link device" and "unlink device" (they generate unique IDs with
randomUUID()), and replace their it(...) declarations with it.concurrent(...) so
the tests run concurrently while still remaining isolated; ensure any other
tests in the same describe that share state are not converted.

Comment on lines 134 to 142
if (error) {
console.error('error', error)
setErrors('login-account', [error.message], {})
if (error.message.includes('captcha')) {
captchaComponent.value?.reset()
toast.error(t('captcha-fail'))
return
}
toast.error(t('invalid-auth'))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Wrong form ID in setErrors — errors won't display.

Line 136 calls setErrors('login-account', ...) but the form is id="restore-account" (Line 178). The error messages will target a non-existent form and silently disappear.

🐛 Fix the form ID
-    setErrors('login-account', [error.message], {})
+    setErrors('restore-account', [error.message], {})
📝 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
if (error) {
console.error('error', error)
setErrors('login-account', [error.message], {})
if (error.message.includes('captcha')) {
captchaComponent.value?.reset()
toast.error(t('captcha-fail'))
return
}
toast.error(t('invalid-auth'))
if (error) {
console.error('error', error)
setErrors('restore-account', [error.message], {})
if (error.message.includes('captcha')) {
captchaComponent.value?.reset()
toast.error(t('captcha-fail'))
return
}
toast.error(t('invalid-auth'))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/restore_account.vue` around lines 134 - 142, The error handling
uses setErrors('login-account', ...) but the actual form id is
"restore-account", so error messages never show; update the setErrors call in
the error branch (where setErrors is invoked) to use 'restore-account' instead
of 'login-account' so submitted errors target the correct form (leave the
captcha reset logic in captchaComponent.value?.reset() and the toast calls
unchanged).

Comment on lines 195 to 199
<FormKit
id="passwordInput" type="password" :placeholder="t('password')
name="password" :label="t('password')" :prefix-icon="iconPassword" :disabled="isLoading"
validation="required:trim" enterkeyhint="send" autocomplete="current-password"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Template parse error: missing closing quote on :placeholder.

Line 196 has :placeholder="t('password') without a closing ". This causes the ESLint parsing errors reported by static analysis and will break the component at build/render time.

🐛 Fix the missing closing quote
                  <FormKit
-                    id="passwordInput" type="password" :placeholder="t('password')
-                    name="password" :label="t('password')" :prefix-icon="iconPassword" :disabled="isLoading"
+                    id="passwordInput" type="password" :placeholder="t('password')"
+                    name="password" :label="t('password')" :prefix-icon="iconPassword" :disabled="isLoading"
                     validation="required:trim" enterkeyhint="send" autocomplete="current-password"
                   />
📝 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
<FormKit
id="passwordInput" type="password" :placeholder="t('password')
name="password" :label="t('password')" :prefix-icon="iconPassword" :disabled="isLoading"
validation="required:trim" enterkeyhint="send" autocomplete="current-password"
/>
<FormKit
id="passwordInput" type="password" :placeholder="t('password')"
name="password" :label="t('password')" :prefix-icon="iconPassword" :disabled="isLoading"
validation="required:trim" enterkeyhint="send" autocomplete="current-password"
/>
🧰 Tools
🪛 ESLint

[error] 197-197: Parsing error: ',' expected.

(vue/no-parsing-error)


[error] 197-197: Parsing error: missing-whitespace-between-attributes.

(vue/no-parsing-error)


[error] 197-197: Parsing error: unexpected-character-in-attribute-name.

(vue/no-parsing-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/restore_account.vue` around lines 195 - 199, The FormKit password
input has a malformed :placeholder attribute (in the FormKit component with
id="passwordInput") — :placeholder="t('password') is missing the closing
double-quote; fix by adding the missing closing quote so the attribute reads
:placeholder="t('password')" and verify the rest of the props (name="password",
:label="t('password')", :prefix-icon="iconPassword", :disabled="isLoading",
validation="required:trim", enterkeyhint="send",
autocomplete="current-password") remain on the same element and are properly
quoted.

Comment on lines +32 to +56
FOR apikey_record IN SELECT * FROM jsonb_array_elements(apikeys_json)
LOOP
-- Check if this API key already exists (by key value)
IF NOT EXISTS (
SELECT 1 FROM "public"."apikeys" WHERE "key" = apikey_record->>'key'
) THEN
INSERT INTO "public"."apikeys" (
"user_id",
"key",
"mode",
"name",
"limited_to_orgs",
"limited_to_apps",
"expires_at"
) VALUES (
user_id_fn,
apikey_record->>'key',
(apikey_record->>'mode')::"public"."apikeys"."mode"%TYPE,
apikey_record->>'name',
COALESCE((apikey_record->'limited_to_orgs')::text[], ARRAY[]::text[]),
COALESCE((apikey_record->'limited_to_apps')::text[], ARRAY[]::text[]),
(apikey_record->>'expires_at')::timestamptz
);
END IF;
END LOOP;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, examine the migration file to understand context
echo "=== Examining the migration file ==="
cat -n supabase/migrations/20260223000000_add_restore_user_function.sql | head -100

Repository: Cap-go/capgo

Length of output: 2779


🏁 Script executed:

#!/bin/bash
# Search for apikeys table definition to understand the mode column type
echo "=== Searching for apikeys table schema ==="
fd -e sql -x cat {} \; | grep -A 30 "CREATE TABLE.*apikeys" | head -50

Repository: Cap-go/capgo

Length of output: 1850


🏁 Script executed:

#!/bin/bash
# Search for any enum definitions related to apikeys mode
echo "=== Searching for enum type definitions ==="
fd -e sql -x cat {} \; | grep -B2 -A5 "CREATE TYPE.*ENUM"

Repository: Cap-go/capgo

Length of output: 4268


🏁 Script executed:

#!/bin/bash
# Check if there are any existing functions that use jsonb_array_elements with RECORD patterns
echo "=== Searching for similar jsonb_array_elements patterns in the codebase ==="
rg -A 10 "jsonb_array_elements" --type=sql | head -100

Repository: Cap-go/capgo

Length of output: 7344


🏁 Script executed:

#!/bin/bash
# Verify PL/pgSQL RECORD behavior with jsonb_array_elements in a quick documentation check
# Check if there are any tests or real usage patterns in the migrations that might clarify the behavior
echo "=== Checking for PL/pgSQL loops with jsonb_array_elements ==="
rg -A 5 "FOR.*IN SELECT.*FROM jsonb_array_elements" --type=sql

Repository: Cap-go/capgo

Length of output: 763


🌐 Web query:

PostgreSQL PL/pgSQL FOR loop jsonb_array_elements RECORD field access syntax

💡 Result:

In PL/pgSQL, a FOR ... IN SELECT loop assigns each row of the query result to the loop target. So with jsonb_array_elements(), you usually want to name the returned column and then use dot notation on the RECORD.

Pattern 1 (loop variable is RECORD)

jsonb_array_elements(jsonb) returns a set with a single output column named value (type jsonb). (postgresql.org)

DO $$
DECLARE
  r record;
  j jsonb := '[{"id":1},{"id":2}]';
BEGIN
  FOR r IN
    SELECT value
    FROM jsonb_array_elements(j) AS t(value)
  LOOP
    RAISE NOTICE 'id=%', (r.value ->> 'id');  -- record-field access: r.value
  END LOOP;
END $$;

Record field access is record_variable.field_name (dot notation). (pgtutorial.com)

Pattern 2 (loop variable is a scalar jsonb, no RECORD field)

If you don’t need a record, select the element directly into a jsonb loop variable:

DO $$
DECLARE
  elem jsonb;
  j jsonb := '[{"id":1},{"id":2}]';
BEGIN
  FOR elem IN
    SELECT jsonb_array_elements(j)
  LOOP
    RAISE NOTICE 'id=%', (elem ->> 'id');     -- elem is jsonb, no ".value"
  END LOOP;
END $$;

Most “record field access syntax” issues here come from forgetting that the RECORD doesn’t contain the JSON object itself—it contains a row with a column (commonly value) that holds the JSON. (postgresql.org)

Citations:


Critical: API key restoration loop has multiple syntax and type errors that will cause runtime failures.

When using FOR apikey_record IN SELECT * FROM jsonb_array_elements(apikeys_json), the loop variable is a RECORD with a single column named value. You must use apikey_record.value->>'key' instead of apikey_record->>'key' to access nested JSON fields (dot notation is required for RECORD field access).

Additionally:

  1. Line 49: %TYPE is only valid in DECLARE blocks for variable declarations — it cannot be used in cast expressions. Use the actual enum type: "public"."key_mode".
  2. Lines 51–52: Direct cast (jsonb_value)::text[] does not work in PostgreSQL. Use ARRAY(SELECT jsonb_array_elements_text(...)) instead.
  3. Line 51: limited_to_orgs should be cast to uuid[], not text[] (per table schema).
🐛 Proposed fix for the API key restoration loop
     FOR apikey_record IN SELECT * FROM jsonb_array_elements(apikeys_json)
     LOOP
       -- Check if this API key already exists (by key value)
       IF NOT EXISTS (
-        SELECT 1 FROM "public"."apikeys" WHERE "key" = apikey_record->>'key'
+        SELECT 1 FROM "public"."apikeys" WHERE "key" = apikey_record.value->>'key'
       ) THEN
         INSERT INTO "public"."apikeys" (
           "user_id",
           "key",
           "mode",
           "name",
           "limited_to_orgs",
           "limited_to_apps",
           "expires_at"
         ) VALUES (
           user_id_fn,
-          apikey_record->>'key',
-          (apikey_record->>'mode')::"public"."apikeys"."mode"%TYPE,
-          apikey_record->>'name',
-          COALESCE((apikey_record->'limited_to_orgs')::text[], ARRAY[]::text[]),
-          COALESCE((apikey_record->'limited_to_apps')::text[], ARRAY[]::text[]),
-          (apikey_record->>'expires_at')::timestamptz
+          apikey_record.value->>'key',
+          (apikey_record.value->>'mode')::"public"."key_mode",
+          apikey_record.value->>'name',
+          COALESCE(ARRAY(SELECT jsonb_array_elements_text(apikey_record.value->'limited_to_orgs'))::uuid[], ARRAY[]::uuid[]),
+          COALESCE(ARRAY(SELECT jsonb_array_elements_text(apikey_record.value->'limited_to_apps')), ARRAY[]::character varying[]),
+          (apikey_record.value->>'expires_at')::timestamptz
         );
       END IF;
     END LOOP;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260223000000_add_restore_user_function.sql` around
lines 32 - 56, The FOR loop is treating apikey_record as if it were a jsonb
rather than the RECORD returned by jsonb_array_elements (its field is value),
and there are invalid casts: replace apikey_record->>'key' etc. with
apikey_record.value->>'key' and apikey_record.value->'field' where appropriate;
change the enum cast from
(apikey_record->>'mode')::"public"."apikeys"."mode"%TYPE to the actual enum type
(apikey_record.value->>'mode')::"public"."key_mode"; build arrays properly using
ARRAY(SELECT jsonb_array_elements_text(apikey_record.value->'limited_to_apps'))
for text[] and ARRAY(SELECT
jsonb_array_elements_text(apikey_record.value->'limited_to_orgs')::uuid) for
uuid[] to match the apikeys.limited_to_orgs column; and keep the expires_at cast
as (apikey_record.value->>'expires_at')::timestamptz while inserting into
"public"."apikeys" with user_id_fn and the other fields.

Comment on lines +66 to +68
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "authenticated";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "anon";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "service_role";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Granting EXECUTE to anon is unnecessary and widens the attack surface.

The function checks auth.uid() on Line 14 and raises an exception if null, so anonymous callers can never succeed. However, granting to anon still allows unauthenticated users to invoke a SECURITY DEFINER function, which is an unnecessary privilege escalation surface.

🔒 Proposed fix: remove the anon grant
 GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "authenticated";
-GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "anon";
 GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "service_role";
📝 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
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "authenticated";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "anon";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "service_role";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "authenticated";
GRANT EXECUTE ON FUNCTION "public"."restore_user"() TO "service_role";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260223000000_add_restore_user_function.sql` around
lines 66 - 68, Remove the unnecessary privilege granted to the anonymous role by
deleting the GRANT EXECUTE line for "anon" in the migration that deals with the
restore_user function; keep GRANT EXECUTE for "authenticated" and "service_role"
only so restore_user (the SECURITY DEFINER function that checks auth.uid())
cannot be invoked by unauthenticated callers.

- Create AccountAction.vue component for delete/restore account forms
- Both pages now just wrap the component with mode prop
- Reduces code from 552 lines to 156 lines
- Fixes SonarQube 39.7% duplication issue
Copy link
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: 4

🧹 Nitpick comments (3)
src/components/AccountAction.vue (3)

46-46: Redundant Supabase client instantiation inside handleAction.

Line 46 creates supabaseClient via useSupabase(), but supabase (line 19) is already available in the setup scope and is used everywhere else in the function. supabaseClient is only used on line 76 for the .from('users') query. Use the existing supabase reference instead.

♻️ Proposed fix
 async function handleAction() {
-  const supabaseClient = useSupabase()
   isLoading.value = true
   ...
-      const { data: user } = await supabaseClient.from('users').select().eq('id', userId).single()
+      const { data: user } = await supabase.from('users').select().eq('id', userId).single()

Also applies to: 76-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` at line 46, The code unnecessarily calls
useSupabase() to create supabaseClient inside handleAction; remove that
redundant instantiation and replace its single usage (the .from('users') query)
with the existing supabase variable declared in setup (supabase) so handleAction
uses supabase.from('users') instead of supabaseClient.from('users'); ensure you
delete the supabaseClient declaration (useSupabase()) and update any references
within handleAction accordingly.

76-77: Unnecessary user fetch in delete path.

Line 76 fetches the user record only to check it exists (line 77), but the data is never used. The subsequent delete_user RPC (line 78) would fail if the user doesn't exist, making this pre-check redundant. Removing it eliminates a round-trip.

♻️ Proposed simplification
     if (props.mode === 'delete') {
-      const { data: user } = await supabaseClient.from('users').select().eq('id', userId).single()
-      if (!user) { isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
       const { error: deleteError } = await supabase.rpc('delete_user')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` around lines 76 - 77, Remove the
unnecessary pre-fetch of the user record: eliminate the
supabaseClient.from('users').select() call and the subsequent existence check of
the user variable, and instead call the delete_user RPC directly (keep the
isLoading management and error handling around the RPC). Locate the block
referencing supabaseClient.from('users').select() and the local user variable,
remove those lines, and ensure the RPC call to delete_user still handles
failures by setting isLoading.value = false and calling setErrors(formId.value,
[...], {}) when the RPC returns an error.

126-126: Consider using DaisyUI d-btn instead of a raw <button>.

Per coding guidelines, interactive elements should use DaisyUI components. This is likely inherited from the original page code, but since this is a new shared component, it's a good opportunity to align.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` at line 126, Replace the raw <button>
element used in the AccountAction component's template with the DaisyUI button
variant: keep the same attributes (v-if="!isLoading", type="submit") and
preserve binding to buttonClass and buttonText but change the element's classes
to use the DaisyUI component class (use the project's DaisyUI variant such as
"d-btn" or "btn" plus any modifier like "btn-primary") so the button follows the
design system; ensure you only swap the CSS class portion and keep the dynamic
bindings and behavior (isLoading, buttonClass, buttonText) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/AccountAction.vue`:
- Around line 35-43: Computed properties like successMessage, dialogTitle, and
dialogButtonText already return translated strings (they call t(...) inside
their computed), but call sites wrap those values in t(...) again; update usages
to pass the computed values directly (e.g., replace t(successMessage.value) with
successMessage.value, and replace t(dialogTitle.value) and
t(dialogButtonText.value) with dialogTitle.value and dialogButtonText.value
respectively, including in the submit function) so you don't attempt to
re-translate already-translated text.
- Line 108: The heading in AccountAction.vue uses an invalid <p> inside <h1>
(the line rendering {{ pageTitle }} and the "Capgo" brand), so replace the
block-level <p class="inline font-prompt">Capgo</p> with an inline element
(e.g., <span class="inline font-prompt">Capgo</span>) and ensure
spacing/punctuation around the brand remains correct in the <h1> (check the
template that contains pageTitle and the brand span).
- Around line 45-100: Multiple statements are packed on single lines causing
ESLint style errors in handleAction, submit and onMounted; split condensed
statements (e.g., chained if returns and inline finally/ catch blocks) into
separate lines and proper block formatting for readability and to satisfy
style/brace and antfu/if-newline rules (focus around the single-line groups in
handleAction and submit where setErrors/return/isLoading assignments and captcha
resets are combined), reorder imports so computed appears before ref, then run
bun lint:fix to auto-fix remaining issues and re-run lint to verify no remaining
errors; key symbols to check: handleAction, submit, onMounted, formId,
pendingEmail, pendingPassword, confirmCaptchaToken, confirmCaptchaComponent,
captchaComponent, turnstileToken, dialogStore.
- Line 142: Remove the inline route block from the AccountAction.vue component
(the `<route lang="yaml">meta: layout: naked</route>` entry) because
unplugin-vue-router only processes route blocks in src/pages; then add an
equivalent route block to both consuming pages (src/pages/delete_account.vue and
src/pages/restore_account.vue) so each page includes the meta: layout: naked
route metadata for the pages that use the AccountAction component.

---

Nitpick comments:
In `@src/components/AccountAction.vue`:
- Line 46: The code unnecessarily calls useSupabase() to create supabaseClient
inside handleAction; remove that redundant instantiation and replace its single
usage (the .from('users') query) with the existing supabase variable declared in
setup (supabase) so handleAction uses supabase.from('users') instead of
supabaseClient.from('users'); ensure you delete the supabaseClient declaration
(useSupabase()) and update any references within handleAction accordingly.
- Around line 76-77: Remove the unnecessary pre-fetch of the user record:
eliminate the supabaseClient.from('users').select() call and the subsequent
existence check of the user variable, and instead call the delete_user RPC
directly (keep the isLoading management and error handling around the RPC).
Locate the block referencing supabaseClient.from('users').select() and the local
user variable, remove those lines, and ensure the RPC call to delete_user still
handles failures by setting isLoading.value = false and calling
setErrors(formId.value, [...], {}) when the RPC returns an error.
- Line 126: Replace the raw <button> element used in the AccountAction
component's template with the DaisyUI button variant: keep the same attributes
(v-if="!isLoading", type="submit") and preserve binding to buttonClass and
buttonText but change the element's classes to use the DaisyUI component class
(use the project's DaisyUI variant such as "d-btn" or "btn" plus any modifier
like "btn-primary") so the button follows the design system; ensure you only
swap the CSS class portion and keep the dynamic bindings and behavior
(isLoading, buttonClass, buttonText) intact.

Comment on lines +35 to +43
const pageTitle = computed(() => props.mode === 'delete' ? t('leaving') : t('welcome-back'))
const pageSubtitle = computed(() => props.mode === 'delete' ? t('delete-your-account') : t('restore-your-account'))
const buttonText = computed(() => props.mode === 'delete' ? t('delete-account-0') : t('restore-account'))
const buttonClass = computed(() => props.mode === 'delete' ? 'bg-muted-blue-700 hover:bg-blue-700 focus:bg-blue-700' : 'bg-green-500 hover:bg-green-600 focus:bg-green-600')
const dialogTitle = computed(() => props.mode === 'delete' ? t('are-u-sure') : t('are-you-sure-restore'))
const dialogButtonText = computed(() => props.mode === 'delete' ? t('button-remove') : t('button-restore'))
const dialogButtonRole = computed(() => props.mode === 'delete' ? 'danger' : 'primary')
const successMessage = computed(() => props.mode === 'delete' ? t('account-deleted-successfully') : t('account-restored-successfully'))
const formId = computed(() => props.mode === 'delete' ? 'delete-account' : 'restore-account')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Double t() translation on computed strings.

The computed properties on lines 35–43 already return translated strings (e.g., t('are-u-sure')). But the call sites wrap them in t() again:

  • Line 85: t(successMessage.value) → translates an already-translated string.
  • Line 97: t(dialogTitle.value) and t(dialogButtonText.value) → same problem.

This will likely produce incorrect/missing text if vue-i18n doesn't find the translated string as a key, or it will silently return the already-translated value (depending on fallbackWarn / missingWarn settings).

🐛 Proposed fix — remove the outer `t()` at usage sites
-    toast.success(t(successMessage.value))
+    toast.success(successMessage.value)

And in the submit function (line 97), change:

-    dialogStore.openDialog({ id: `${formId.value}-confirm`, title: t(dialogTitle.value), buttons: [{ text: t(dialogButtonText.value), role: dialogButtonRole.value, handler: handleAction }, ...] })
+    dialogStore.openDialog({ id: `${formId.value}-confirm`, title: dialogTitle.value, buttons: [{ text: dialogButtonText.value, role: dialogButtonRole.value, handler: handleAction }, ...] })

Also applies to: 85-85, 97-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` around lines 35 - 43, Computed properties
like successMessage, dialogTitle, and dialogButtonText already return translated
strings (they call t(...) inside their computed), but call sites wrap those
values in t(...) again; update usages to pass the computed values directly
(e.g., replace t(successMessage.value) with successMessage.value, and replace
t(dialogTitle.value) and t(dialogButtonText.value) with dialogTitle.value and
dialogButtonText.value respectively, including in the submit function) so you
don't attempt to re-translate already-translated text.

Comment on lines +45 to +100
async function handleAction() {
const supabaseClient = useSupabase()
isLoading.value = true
try {
if (!pendingEmail.value || !pendingPassword.value) {
isLoading.value = false
return setErrors(formId.value, [t('invalid-auth')], {})
}
if (captchaKey.value && !confirmCaptchaToken.value) {
isLoading.value = false
return setErrors(formId.value, [t('captcha-required', 'Captcha verification is required')], {})
}
const { error: reauthError } = await supabase.auth.signInWithPassword({
email: pendingEmail.value,
password: pendingPassword.value,
options: captchaKey.value ? { captchaToken: confirmCaptchaToken.value } : undefined,
})
if (reauthError) {
confirmCaptchaToken.value = ''
confirmCaptchaComponent.value?.reset()
isLoading.value = false
if (reauthError.message.includes('captcha')) toast.error(t('captcha-fail'))
return setErrors(formId.value, [t('invalid-auth')], {})
}
const { data: claimsData, error: claimsError } = await supabase.auth.getClaims()
const userId = claimsData?.claims?.sub
if (claimsError || !userId) {
isLoading.value = false
return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {})
}
if (props.mode === 'delete') {
const { data: user } = await supabaseClient.from('users').select().eq('id', userId).single()
if (!user) { isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
const { error: deleteError } = await supabase.rpc('delete_user')
if (deleteError) { if (deleteError.message?.includes('reauth_required')) { isLoading.value = false; return setErrors(formId.value, [t('invalid-auth')], {}) }; isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
} else {
const { error: restoreError } = await supabase.rpc('restore_user')
if (restoreError) { isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
}
await supabase.auth.signOut()
toast.success(t(successMessage.value))
router.replace('/login')
} catch { isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
finally { isLoading.value = false; pendingEmail.value = ''; pendingPassword.value = ''; confirmCaptchaToken.value = ''; confirmCaptchaComponent.value?.reset() }
}

async function submit(form: { email: string, password: string }) {
isLoading.value = true
if (captchaKey.value && !turnstileToken.value) { isLoading.value = false; setErrors(formId.value, [t('captcha-required', 'Captcha verification is required')], {}); return }
const { error } = await supabase.auth.signInWithPassword({ email: form.email, password: form.password, options: captchaKey.value ? { captchaToken: turnstileToken.value } : undefined })
isLoading.value = false
if (error) { setErrors(formId.value, [error.message], {}); if (error.message.includes('captcha')) { captchaComponent.value?.reset(); toast.error(t('captcha-fail')); return }; toast.error(t('invalid-auth')) }
else { pendingEmail.value = form.email; pendingPassword.value = form.password; turnstileToken.value = ''; captchaComponent.value?.reset(); dialogStore.openDialog({ id: `${formId.value}-confirm`, title: t(dialogTitle.value), buttons: [{ text: t(dialogButtonText.value), role: dialogButtonRole.value, handler: handleAction }, { text: t('button-cancel'), role: 'cancel', handler: () => {} }] }); await dialogStore.onDialogDismiss() }
}

onMounted(() => { hideLoader() })
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

ESLint errors must be resolved — linting is a commit gate.

Static analysis flags 15+ ESLint errors across handleAction, submit, and onMounted — primarily style/max-statements-per-line, style/brace-style, and antfu/if-newline violations. These are caused by cramming multiple statements onto single lines (e.g., lines 77, 79, 82, 87, 88, 93, 96, 97, 100).

Per coding guidelines: "Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues." Many of these will auto-fix, but the densely-packed lines (especially 79, 96–97) will likely need manual reformatting.

Also on line 4: import ordering (computed should come before ref).

♻️ Example: expand line 79 into readable multi-line form
-      const { error: deleteError } = await supabase.rpc('delete_user')
-      if (deleteError) { if (deleteError.message?.includes('reauth_required')) { isLoading.value = false; return setErrors(formId.value, [t('invalid-auth')], {}) }; isLoading.value = false; return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {}) }
+      const { error: deleteError } = await supabase.rpc('delete_user')
+      if (deleteError) {
+        isLoading.value = false
+        if (deleteError.message?.includes('reauth_required'))
+          return setErrors(formId.value, [t('invalid-auth')], {})
+        return setErrors(formId.value, [t('something-went-wrong-try-again-later')], {})
+      }

As per coding guidelines: "Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues in frontend files."

🧰 Tools
🪛 ESLint

[error] 66-66: Expect newline after if

(antfu/if-newline)


[error] 77-77: This line has 3 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 79-79: This line has 6 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 80-80: Closing curly brace appears on the same line as the subsequent block.

(style/brace-style)


[error] 82-82: This line has 3 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 87-87: Closing curly brace appears on the same line as the subsequent block.

(style/brace-style)


[error] 87-87: This line has 2 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 88-88: This line has 5 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 93-93: This line has 4 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 96-96: This line has 7 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 97-97: This line has 6 statements. Maximum allowed is 1.

(style/max-statements-per-line)


[error] 100-100: This line has 2 statements. Maximum allowed is 1.

(style/max-statements-per-line)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` around lines 45 - 100, Multiple statements
are packed on single lines causing ESLint style errors in handleAction, submit
and onMounted; split condensed statements (e.g., chained if returns and inline
finally/ catch blocks) into separate lines and proper block formatting for
readability and to satisfy style/brace and antfu/if-newline rules (focus around
the single-line groups in handleAction and submit where
setErrors/return/isLoading assignments and captcha resets are combined), reorder
imports so computed appears before ref, then run bun lint:fix to auto-fix
remaining issues and re-run lint to verify no remaining errors; key symbols to
check: handleAction, submit, onMounted, formId, pendingEmail, pendingPassword,
confirmCaptchaToken, confirmCaptchaComponent, captchaComponent, turnstileToken,
dialogStore.

<div class="px-4 my-auto mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center">
<img src="/capgo.webp" alt="logo" class="mx-auto mb-6 w-1/6 rounded-sm invert dark:invert-0">
<h1 class="text-3xl font-bold leading-tight text-black sm:text-4xl lg:text-5xl dark:text-white">{{ pageTitle }} <p class="inline font-prompt">Capgo</p> ?</h1>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid HTML: <p> nested inside <h1>.

A <p> (block-level) element inside an <h1> is invalid per the HTML spec. Use <span> instead to keep the inline styling for the "Capgo" brand name.

Proposed fix
-        <h1 class="text-3xl font-bold leading-tight text-black sm:text-4xl lg:text-5xl dark:text-white">{{ pageTitle }} <p class="inline font-prompt">Capgo</p> ?</h1>
+        <h1 class="text-3xl font-bold leading-tight text-black sm:text-4xl lg:text-5xl dark:text-white">{{ pageTitle }} <span class="font-prompt">Capgo</span> ?</h1>
📝 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
<h1 class="text-3xl font-bold leading-tight text-black sm:text-4xl lg:text-5xl dark:text-white">{{ pageTitle }} <p class="inline font-prompt">Capgo</p> ?</h1>
<h1 class="text-3xl font-bold leading-tight text-black sm:text-4xl lg:text-5xl dark:text-white">{{ pageTitle }} <span class="font-prompt">Capgo</span> ?</h1>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` at line 108, The heading in
AccountAction.vue uses an invalid <p> inside <h1> (the line rendering {{
pageTitle }} and the "Capgo" brand), so replace the block-level <p class="inline
font-prompt">Capgo</p> with an inline element (e.g., <span class="inline
font-prompt">Capgo</span>) and ensure spacing/punctuation around the brand
remains correct in the <h1> (check the template that contains pageTitle and the
brand span).

</section>
</template>

<route lang="yaml">meta: layout: naked</route>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, check AccountAction.vue for the <route> block
echo "=== AccountAction.vue (checking for <route> block) ==="
if [ -f "src/components/AccountAction.vue" ]; then
  wc -l src/components/AccountAction.vue
  echo ""
  tail -20 src/components/AccountAction.vue
else
  echo "File not found"
fi

echo ""
echo "=== Searching for <route> blocks in AccountAction.vue ==="
rg -n "<route" src/components/AccountAction.vue || echo "No <route> found"

Repository: Cap-go/capgo

Length of output: 2278


🏁 Script executed:

#!/bin/bash

# Check if delete_account.vue and restore_account.vue exist and their content
echo "=== delete_account.vue ==="
if [ -f "src/pages/delete_account.vue" ]; then
  wc -l src/pages/delete_account.vue
  echo "--- First 30 lines ---"
  head -30 src/pages/delete_account.vue
  echo "--- Last 10 lines ---"
  tail -10 src/pages/delete_account.vue
else
  echo "File not found"
fi

echo ""
echo "=== restore_account.vue ==="
if [ -f "src/pages/restore_account.vue" ]; then
  wc -l src/pages/restore_account.vue
  echo "--- First 30 lines ---"
  head -30 src/pages/restore_account.vue
  echo "--- Last 10 lines ---"
  tail -10 src/pages/restore_account.vue
else
  echo "File not found"
fi

Repository: Cap-go/capgo

Length of output: 874


🏁 Script executed:

#!/bin/bash

# Check unplugin-vue-router configuration
echo "=== Looking for unplugin-vue-router config ==="
find . -maxdepth 3 -type f \( -name "vite.config.*" -o -name "vitest.config.*" -o -name "vue.config.*" -o -name "tsconfig.json" \) | head -10

echo ""
echo "=== Searching for unplugin-vue-router in config files ==="
rg -l "unplugin-vue-router\|@unplugin/vue-router" --type ts --type js | head -5

echo ""
echo "=== Checking vite.config files ==="
fd -e "vite.config.*" -x cat {}

Repository: Cap-go/capgo

Length of output: 317


🏁 Script executed:

#!/bin/bash

# Search for route blocks across the codebase to understand usage
echo "=== All <route> blocks in the codebase ==="
rg -n "<route" --type vue | head -20

Repository: Cap-go/capgo

Length of output: 143


🏁 Script executed:

#!/bin/bash

echo "=== vite.config.mts content ==="
cat vite.config.mts

Repository: Cap-go/capgo

Length of output: 4781


🏁 Script executed:

#!/bin/bash

echo "=== Searching for <route> blocks in src/pages/ ==="
find src/pages -name "*.vue" -exec grep -l "<route" {} \;

echo ""
echo "=== All <route> blocks in pages directory ==="
find src/pages -name "*.vue" -exec grep -n "<route" {} +

Repository: Cap-go/capgo

Length of output: 6130


🏁 Script executed:

#!/bin/bash

echo "=== Checking package.json for unplugin-vue-router ==="
rg "unplugin-vue-router\|@unplugin/vue-router" package.json

echo ""
echo "=== Checking if there are any other <route> blocks in components ==="
find src/components -name "*.vue" -exec grep -n "<route" {} +

Repository: Cap-go/capgo

Length of output: 1136


Move <route> block from component to consuming pages — unplugin-vue-router only processes routes in src/pages/.

The <route lang="yaml">meta: layout: naked</route> block in src/components/AccountAction.vue is silently ignored because unplugin-vue-router only processes files in src/pages/ by default. This configuration must be added to the pages that consume this component instead.

🔧 Fix — add route block to both page files

Remove line 142 from src/components/AccountAction.vue.

Then add this to both src/pages/delete_account.vue and src/pages/restore_account.vue:

<route lang="yaml">
meta:
  layout: naked
</route>
🧰 Tools
🪛 ESLint

[error] 142-142: A line break is required after ''.

(vue/block-tag-newline)


[error] 142-142: A line break is required before ''.

(vue/block-tag-newline)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AccountAction.vue` at line 142, Remove the inline route block
from the AccountAction.vue component (the `<route lang="yaml">meta: layout:
naked</route>` entry) because unplugin-vue-router only processes route blocks in
src/pages; then add an equivalent route block to both consuming pages
(src/pages/delete_account.vue and src/pages/restore_account.vue) so each page
includes the meta: layout: naked route metadata for the pages that use the
AccountAction component.

@sonarqubecloud
Copy link

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.

1 participant