feat: add user locale preference to user_preferences table (#555)
* feat: add user locale preference to user_preferences table
- Introduced a new column 'locale' in the user_preferences table to store per-user UI language preferences.
- Added a CHECK constraint to ensure only supported locales ('sv', 'en') are allowed.
- Triggered a schema reload notification for the changes.
chore: declare CSS module support in TypeScript
- Added a declaration for CSS modules in globals.d.ts to enable TypeScript support for importing CSS files.
* feat: add Swish as an invoice payment method in company settings
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { shouldEnforceMfa } from '@/lib/auth/mfa'
|
||||
import { DEFAULT_LOCALE, LOCALE_COOKIE, isLocale } from '@/i18n/config'
|
||||
import { userHasPassword } from '@/lib/auth/has-password'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
@@ -135,7 +136,7 @@ export async function updateSession(request: NextRequest) {
|
||||
|
||||
// MFA required but user has no factor enrolled yet → force enrollment
|
||||
// Skip for users with no companies (still setting up)
|
||||
const companyIdForMfa = await resolveCompanyForMiddleware(supabase, user.id, request)
|
||||
const { companyId: companyIdForMfa } = await resolveCompanyForMiddleware(supabase, user.id, request)
|
||||
if (companyIdForMfa) {
|
||||
const { data: factors } = await supabase.auth.mfa.listFactors()
|
||||
const hasVerifiedFactor = factors?.totp?.some(f => f.status === 'verified')
|
||||
@@ -152,7 +153,7 @@ export async function updateSession(request: NextRequest) {
|
||||
|
||||
// Company context resolution
|
||||
const cookieCompanyId = request.cookies.get('gnubok-company-id')?.value
|
||||
const companyId = await resolveCompanyForMiddleware(supabase, user.id, request)
|
||||
const { companyId, locale: dbLocale } = await resolveCompanyForMiddleware(supabase, user.id, request)
|
||||
|
||||
// If the cookie pointed at a company we can no longer resolve (e.g.
|
||||
// archived), clear it so the browser stops sending it.
|
||||
@@ -160,6 +161,20 @@ export async function updateSession(request: NextRequest) {
|
||||
supabaseResponse.cookies.set('gnubok-company-id', '', { path: '/', maxAge: 0 })
|
||||
}
|
||||
|
||||
// Sync the locale cookie from user_preferences. This keeps next-intl's
|
||||
// request config (which reads the cookie) consistent with the DB value
|
||||
// without forcing every RSC render to query the database itself.
|
||||
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value
|
||||
const effectiveLocale = isLocale(dbLocale) ? dbLocale : DEFAULT_LOCALE
|
||||
if (cookieLocale !== effectiveLocale) {
|
||||
supabaseResponse.cookies.set(LOCALE_COOKIE, effectiveLocale, {
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
}
|
||||
|
||||
// Routes that stay accessible when the user has no active company.
|
||||
// Needed so a user who archived their last company can still delete
|
||||
// their account without being trapped on /onboarding forever.
|
||||
@@ -230,14 +245,16 @@ async function resolveCompanyForMiddleware(
|
||||
supabase: ReturnType<typeof createServerClient>,
|
||||
userId: string,
|
||||
_request: NextRequest
|
||||
): Promise<string | null> {
|
||||
): Promise<{ companyId: string | null; locale: string | null }> {
|
||||
// 1. user_preferences (authoritative)
|
||||
const { data: prefs } = await supabase
|
||||
.from('user_preferences')
|
||||
.select('active_company_id')
|
||||
.select('active_company_id, locale')
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle()
|
||||
|
||||
const locale = (prefs?.locale as string | undefined) ?? null
|
||||
|
||||
if (prefs?.active_company_id) {
|
||||
const { data: membership } = await supabase
|
||||
.from('company_members')
|
||||
@@ -247,7 +264,7 @@ async function resolveCompanyForMiddleware(
|
||||
.is('companies.archived_at', null)
|
||||
.maybeSingle()
|
||||
|
||||
if (membership) return membership.company_id
|
||||
if (membership) return { companyId: membership.company_id, locale }
|
||||
}
|
||||
|
||||
// 2. Fallback: first non-archived membership by created_at
|
||||
@@ -260,7 +277,7 @@ async function resolveCompanyForMiddleware(
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (!firstCompany) return null
|
||||
if (!firstCompany) return { companyId: null, locale }
|
||||
|
||||
// Write the fallback back to user_preferences so future RLS lookups
|
||||
// see the same active company without needing this fallback scan.
|
||||
@@ -271,5 +288,5 @@ async function resolveCompanyForMiddleware(
|
||||
{ onConflict: 'user_id' }
|
||||
)
|
||||
|
||||
return firstCompany.company_id
|
||||
return { companyId: firstCompany.company_id, locale }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user