* Implement company and account deletion features - Add event types for company and account deletion to CoreEvent. - Enhance Supabase middleware to handle company context resolution and cookie management for archived companies. - Create API routes for deleting accounts and companies, including necessary validations and event emissions. - Implement tests for account and company deletion endpoints to ensure proper functionality and error handling. - Add retention notice component to inform users about bookkeeping data retention during destructive actions. - Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws. * feat: enhance account deletion process and update user notifications * Add service client for onboarding completion check and update escape hatch visibility * Enhance invite flow and email handling for company members * Refactor company context and RLS policies for active company isolation - Update `switchCompany` to remove unnecessary revalidation as client handles navigation. - Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships. - Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility. - Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership. - Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization. - Implement `CompanyTabSync` component for real-time active company enforcement across tabs. - Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`. * feat: implement viewer role enforcement for write permissions - Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company. - Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions. - Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers. - Created tests to verify the behavior of the viewer role and write permissions. - Added database migration to enforce read-only access for viewers at the database level.
69 lines
2.5 KiB
PL/PgSQL
69 lines
2.5 KiB
PL/PgSQL
-- Replace hard-delete with anonymization: retain auth.users as a tombstone.
|
|
--
|
|
-- Why: BFL 7 kap. 2§ requires räkenskapsinformation to be preserved for 7
|
|
-- years. companies.created_by references auth.users ON DELETE CASCADE, so
|
|
-- hard-deleting the user would wipe retained verifikationer. Instead we:
|
|
-- 1) verify zero owned, non-archived companies
|
|
-- 2) remove the user from all company/team memberships
|
|
-- 3) delete per-user operational state (prefs, api keys)
|
|
-- 4) anonymize the profile
|
|
-- The API route caller is responsible for banning the auth.users row via
|
|
-- the admin API afterwards (SQL can't reach supabase.auth.admin).
|
|
|
|
CREATE OR REPLACE FUNCTION public.anonymize_user_account(target_user_id uuid)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
blocker_count int;
|
|
BEGIN
|
|
IF auth.uid() IS DISTINCT FROM target_user_id THEN
|
|
RAISE EXCEPTION 'Can only delete your own account';
|
|
END IF;
|
|
|
|
-- Precondition: zero owned, non-archived companies
|
|
SELECT count(*) INTO blocker_count
|
|
FROM public.company_members cm
|
|
JOIN public.companies c ON c.id = cm.company_id
|
|
WHERE cm.user_id = target_user_id
|
|
AND cm.role = 'owner'
|
|
AND c.archived_at IS NULL;
|
|
|
|
IF blocker_count > 0 THEN
|
|
RAISE EXCEPTION 'Cannot delete account: user still owns % active compan(y/ies)', blocker_count
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
-- Remove memberships
|
|
DELETE FROM public.company_members WHERE user_id = target_user_id;
|
|
DELETE FROM public.team_members WHERE user_id = target_user_id;
|
|
|
|
-- Remove per-user operational state
|
|
DELETE FROM public.user_preferences WHERE user_id = target_user_id;
|
|
DELETE FROM public.api_keys WHERE user_id = target_user_id;
|
|
|
|
-- Anonymize profile tombstone
|
|
UPDATE public.profiles
|
|
SET email = NULL,
|
|
full_name = NULL,
|
|
avatar_url = NULL,
|
|
deleted_at = now(),
|
|
anonymized_at = now(),
|
|
updated_at = now()
|
|
WHERE id = target_user_id;
|
|
|
|
-- NOTE: auth.users row, companies.created_by FK, and audit_log.user_id
|
|
-- are intentionally left intact. They now reference a banned, anonymized
|
|
-- tombstone row that exists solely to keep BFL-retained data valid.
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.anonymize_user_account(uuid) TO authenticated;
|
|
|
|
-- The old delete_user_account RPC hard-deleted auth.users, which cascaded
|
|
-- away retained verifikationer via companies.created_by. It is incompatible
|
|
-- with the retention model and must be removed.
|
|
DROP FUNCTION IF EXISTS public.delete_user_account(uuid);
|