* 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.
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
'use server'
|
|
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { setActiveCompany } from '@/lib/company/context'
|
|
import { revalidatePath } from 'next/cache'
|
|
|
|
export async function switchCompany(companyId: string): Promise<{ error?: string }> {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
if (!user) {
|
|
return { error: 'Unauthorized' }
|
|
}
|
|
|
|
try {
|
|
await setActiveCompany(supabase, user.id, companyId)
|
|
// No revalidatePath — the client performs a hard navigation
|
|
// (window.location.assign) after this action returns, which wipes
|
|
// every React/router/fetch cache wholesale. revalidatePath would be a
|
|
// no-op and would just race with the hard reload.
|
|
return {}
|
|
} catch {
|
|
return { error: 'Du har inte tillgång till detta företag.' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a company from onboarding wizard data.
|
|
*
|
|
* This runs on the server so that if the Next.js server is unavailable when
|
|
* the user clicks the final "Fortsätt" button, the action never reaches
|
|
* Supabase and no ghost company is created. All operations (company,
|
|
* membership, chart of accounts, settings, fiscal period, active company)
|
|
* happen sequentially; if any step after company creation fails the company
|
|
* is rolled back to avoid partial state.
|
|
*/
|
|
export async function createCompanyFromOnboarding(params: {
|
|
teamId: string
|
|
settings: Record<string, unknown>
|
|
fiscalPeriod: {
|
|
startDate: string
|
|
endDate: string
|
|
name: string
|
|
}
|
|
}): Promise<{ companyId?: string; error?: string }> {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
if (!user) {
|
|
return { error: 'Unauthorized' }
|
|
}
|
|
|
|
const entityType = params.settings.entity_type as string | undefined
|
|
if (entityType !== 'enskild_firma' && entityType !== 'aktiebolag') {
|
|
return { error: 'Ogiltig företagsform.' }
|
|
}
|
|
|
|
const companyName = (params.settings.company_name as string | undefined) || 'Mitt företag'
|
|
|
|
// 1. Create company + owner membership atomically via RPC
|
|
const { data: newCompanyId, error: companyError } = await supabase.rpc('create_company_with_owner', {
|
|
p_name: companyName,
|
|
p_entity_type: entityType,
|
|
p_team_id: params.teamId,
|
|
})
|
|
|
|
if (companyError || !newCompanyId) {
|
|
console.error('[createCompanyFromOnboarding] company creation failed', companyError)
|
|
return { error: 'Kunde inte skapa företag. Försök igen.' }
|
|
}
|
|
|
|
// Helper: roll back the company if a subsequent step fails. Deletes in FK order.
|
|
const rollback = async (reason: string, err: unknown) => {
|
|
console.error(`[createCompanyFromOnboarding] rolling back ${newCompanyId}: ${reason}`, err)
|
|
await supabase.from('company_settings').delete().eq('company_id', newCompanyId)
|
|
await supabase.from('fiscal_periods').delete().eq('company_id', newCompanyId)
|
|
await supabase.from('chart_of_accounts').delete().eq('company_id', newCompanyId)
|
|
await supabase.from('company_members').delete().eq('company_id', newCompanyId)
|
|
await supabase.from('companies').delete().eq('id', newCompanyId)
|
|
}
|
|
|
|
// 2. Seed chart of accounts
|
|
const { error: coaError } = await supabase.rpc('seed_chart_of_accounts', {
|
|
p_company_id: newCompanyId,
|
|
p_entity_type: entityType,
|
|
})
|
|
if (coaError) {
|
|
await rollback('COA seeding failed', coaError)
|
|
return { error: 'Kunde inte skapa kontoplan. Försök igen.' }
|
|
}
|
|
|
|
// 3. Save settings (strip UI-only and managed fields)
|
|
const {
|
|
id: _id,
|
|
user_id: _uid,
|
|
company_id: _cid,
|
|
created_at: _ca,
|
|
updated_at: _ua,
|
|
is_first_fiscal_year: _ify,
|
|
first_year_start: _fys,
|
|
first_year_end: _fye,
|
|
...settingsToSave
|
|
} = params.settings
|
|
|
|
const { error: settingsError } = await supabase
|
|
.from('company_settings')
|
|
.upsert(
|
|
{
|
|
...settingsToSave,
|
|
company_id: newCompanyId,
|
|
onboarding_complete: true,
|
|
onboarding_step: 4,
|
|
},
|
|
{ onConflict: 'company_id' },
|
|
)
|
|
|
|
if (settingsError) {
|
|
await rollback('settings upsert failed', settingsError)
|
|
return { error: 'Kunde inte spara inställningar. Försök igen.' }
|
|
}
|
|
|
|
// 4. Create fiscal period
|
|
const { error: periodError } = await supabase.from('fiscal_periods').upsert(
|
|
{
|
|
company_id: newCompanyId,
|
|
name: params.fiscalPeriod.name,
|
|
period_start: params.fiscalPeriod.startDate,
|
|
period_end: params.fiscalPeriod.endDate,
|
|
},
|
|
{ onConflict: 'company_id,period_start,period_end' },
|
|
)
|
|
|
|
if (periodError) {
|
|
await rollback('fiscal period upsert failed', periodError)
|
|
return { error: 'Kunde inte skapa räkenskapsår. Försök igen.' }
|
|
}
|
|
|
|
// 5. Set as active company
|
|
try {
|
|
await setActiveCompany(supabase, user.id, newCompanyId)
|
|
} catch (err) {
|
|
// Non-fatal: the company was created successfully; the user can switch manually
|
|
console.error('[createCompanyFromOnboarding] setActiveCompany failed', err)
|
|
}
|
|
|
|
revalidatePath('/')
|
|
return { companyId: newCompanyId }
|
|
}
|