From e730ee709b1310b617257173c23fdd312db45792 Mon Sep 17 00:00:00 2001 From: bryan Date: Mon, 15 Jun 2026 20:53:29 +0100 Subject: [PATCH] Polish admin console policy wiring --- app/[locale]/admin/page.tsx | 46 ++++++++++--------- app/api/sessions/[sessionId]/start/route.ts | 26 +++++++---- app/api/sessions/route.ts | 14 ++++-- lib/policy/defaults.ts | 8 ++-- .../20260615120000_admin_policy_settings.sql | 8 ++-- ...0260615143000_admin_policy_french_copy.sql | 30 ++++++++++++ 6 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 supabase/migrations/20260615143000_admin_policy_french_copy.sql diff --git a/app/[locale]/admin/page.tsx b/app/[locale]/admin/page.tsx index cd83b25..e0bae87 100644 --- a/app/[locale]/admin/page.tsx +++ b/app/[locale]/admin/page.tsx @@ -2,6 +2,7 @@ import { unstable_noStore as noStore } from 'next/cache'; import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; import type { ReactNode } from 'react'; +import { ArrowLeft } from 'lucide-react'; import { Link } from '@/i18n/navigation'; import type { AppLocale } from '@/i18n/routing'; @@ -43,8 +44,9 @@ export default async function AdminPolicyPage({
+
@@ -346,38 +348,38 @@ function TextField({ function getCopy(locale: AppLocale) { if (locale === 'fr') { return { - back: 'Retour au dashboard', - title: 'Console administration', + back: 'Retour au tableau de bord', + title: "Console d'administration", description: - 'Modifie les regles produit qui pilotent les quotas, les limites de session et les conditions d acces.', - secured: 'Acces admin', - saved: 'Parametres enregistres.', + "Modifie les règles produit qui pilotent les quotas, les limites de session et les conditions d'accès.", + secured: 'Accès administrateur', + saved: 'Paramètres enregistrés.', save: 'Enregistrer', policyMatrix: 'Matrice produit', - accessRules: 'Regles d acces', + accessRules: "Règles d'accès", userStatus: 'Statut utilisateur', - sessionLimit: 'Limite session', - unlockCondition: 'Condition de deblocage', - newTrialUser: 'Nouvel utilisateur trial', - consistentTrialUser: 'Utilisateur trial regulier', + sessionLimit: 'Limite de session', + unlockCondition: 'Condition de déblocage', + newTrialUser: "Nouvel utilisateur d'essai", + consistentTrialUser: "Utilisateur d'essai régulier", paidUser: 'Utilisateur payant', - highRiskUser: 'Utilisateur a risque', - sessionsToComplete: 'Sessions a completer', - questionLimit: 'Limite questions', - fullAccess: 'Acces complet', + highRiskUser: 'Utilisateur à risque', + sessionsToComplete: 'Sessions à compléter', + questionLimit: 'Limite de questions', + fullAccess: 'Accès complet', freeTrial: 'Essai gratuit', freeQuestionLimit: 'Questions gratuites', - warningThreshold: 'Seuil avertissement', - sessionDefaults: 'Creation de session', - defaultQuestions: 'Questions par defaut', + warningThreshold: "Seuil d'avertissement", + sessionDefaults: 'Création de session', + defaultQuestions: 'Questions par défaut', maxQuestions: 'Questions max', minimumMembers: 'Membres minimum', - timersAndCompletion: 'Timers et completion', + timersAndCompletion: 'Timers et complétion', perQuestionTimer: 'Timer par question', globalTimer: 'Timer global', maxTimer: 'Timer max', - completionMin: 'Min completion', - completionMax: 'Max completion', + completionMin: 'Complétion min', + completionMax: 'Complétion max', }; } diff --git a/app/api/sessions/[sessionId]/start/route.ts b/app/api/sessions/[sessionId]/start/route.ts index 722ad8b..591c40a 100644 --- a/app/api/sessions/[sessionId]/start/route.ts +++ b/app/api/sessions/[sessionId]/start/route.ts @@ -2,9 +2,10 @@ import { NextResponse } from 'next/server'; import { getTranslations } from 'next-intl/server'; import type { AppLocale } from '@/i18n/routing'; -import { getUserTierCapabilities } from '@/lib/billing/user-tier'; +import { deriveUserTier, getUserTierCapabilities } from '@/lib/billing/user-tier'; import { createGroupNotifications } from '@/lib/notifications/in-app'; import { createPerfTracker } from '@/lib/observability/perf'; +import { getAppPolicySettings } from '@/lib/policy/app-policy'; import { createInitialQuestionFast } from '@/lib/session/flow'; import { createSupabaseAdminClient } from '@/lib/supabase/admin'; import { createSupabaseServerClient } from '@/lib/supabase/server'; @@ -69,6 +70,7 @@ export async function POST(request: Request, { params }: RouteContext) { perf.setContext({ locale }); const supabase = createSupabaseServerClient(); + const policy = await getAppPolicySettings(); const { data: fastRows, error: fastError } = await ( supabase.schema('public') as unknown as { rpc: ( @@ -157,12 +159,12 @@ export async function POST(request: Request, { params }: RouteContext) { ) .eq('id', sessionId) .maybeSingle(), - admin - .schema('public') - .from('users') - .select('user_tier') - .eq('id', user.id) - .maybeSingle(), + admin + .schema('public') + .from('users') + .select('questions_answered, has_valid_payment_method, subscription_status') + .eq('id', user.id) + .maybeSingle(), ]); perf.step('session_and_tier_loaded'); @@ -197,7 +199,15 @@ export async function POST(request: Request, { params }: RouteContext) { ); } - const userTier = userTierResult.data?.user_tier ?? 'locked'; + const userTier = userTierResult.data + ? deriveUserTier({ + questionsAnswered: userTierResult.data.questions_answered ?? 0, + hasValidPaymentMethod: + userTierResult.data.has_valid_payment_method ?? false, + subscriptionStatus: userTierResult.data.subscription_status ?? 'none', + policy, + }) + : 'locked'; if (!getUserTierCapabilities(userTier).canJoinSessions) { return NextResponse.json( { ok: false, message: await getFeedback('upgradeRequiredToJoinSession') }, diff --git a/app/api/sessions/route.ts b/app/api/sessions/route.ts index 6cff360..32ffeab 100644 --- a/app/api/sessions/route.ts +++ b/app/api/sessions/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'; import { getTranslations } from 'next-intl/server'; import type { AppLocale } from '@/i18n/routing'; -import { getUserTierCapabilities } from '@/lib/billing/user-tier'; +import { deriveUserTier, getUserTierCapabilities } from '@/lib/billing/user-tier'; import { hasEmailEnv } from '@/lib/env'; import { APP_EVENTS } from '@/lib/logging/events'; import { logAppEvent } from '@/lib/logging/logger'; @@ -221,7 +221,7 @@ export async function POST(request: Request) { admin .schema('public') .from('users') - .select('user_tier') + .select('questions_answered, has_valid_payment_method, subscription_status') .eq('id', user.id) .maybeSingle(), admin @@ -253,7 +253,15 @@ export async function POST(request: Request) { ); } - const userTier = userTierResult.data?.user_tier ?? 'locked'; + const userTier = userTierResult.data + ? deriveUserTier({ + questionsAnswered: userTierResult.data.questions_answered ?? 0, + hasValidPaymentMethod: + userTierResult.data.has_valid_payment_method ?? false, + subscriptionStatus: userTierResult.data.subscription_status ?? 'none', + policy, + }) + : 'locked'; if (!getUserTierCapabilities(userTier).canCreateSession) { return NextResponse.json( { diff --git a/lib/policy/defaults.ts b/lib/policy/defaults.ts index ac20af2..e6f5ffd 100644 --- a/lib/policy/defaults.ts +++ b/lib/policy/defaults.ts @@ -39,13 +39,13 @@ export const DEFAULT_APP_POLICY_SETTINGS: AppPolicySettings = { completionMinMembers: 2, completionMaxMembers: 5, consistentTrialUnlockConditionEn: 'Maintain review completion', - consistentTrialUnlockConditionFr: 'Maintenir la revision', + consistentTrialUnlockConditionFr: 'Maintenir la révision', paidUnlockConditionEn: 'Immediate access', - paidUnlockConditionFr: 'Acces immediat', + paidUnlockConditionFr: 'Accès immédiat', highRiskSessionLimitEn: 'Suggested smaller sessions', - highRiskSessionLimitFr: 'Sessions plus courtes suggerees', + highRiskSessionLimitFr: 'Sessions plus courtes suggérées', highRiskConditionEn: 'Low completion or poor consistency', - highRiskConditionFr: 'Faible completion ou faible regularite', + highRiskConditionFr: 'Faible complétion ou faible régularité', }; export type SessionCreationPolicy = Pick< diff --git a/supabase/migrations/20260615120000_admin_policy_settings.sql b/supabase/migrations/20260615120000_admin_policy_settings.sql index bce4c47..f481f0c 100644 --- a/supabase/migrations/20260615120000_admin_policy_settings.sql +++ b/supabase/migrations/20260615120000_admin_policy_settings.sql @@ -15,13 +15,13 @@ create table if not exists public.app_policy_settings ( completion_min_members integer not null default 2 check (completion_min_members between 1 and 100), completion_max_members integer not null default 5 check (completion_max_members between 1 and 100), consistent_trial_unlock_condition_en text not null default 'Maintain review completion', - consistent_trial_unlock_condition_fr text not null default 'Maintenir la revision', + consistent_trial_unlock_condition_fr text not null default 'Maintenir la révision', paid_unlock_condition_en text not null default 'Immediate access', - paid_unlock_condition_fr text not null default 'Acces immediat', + paid_unlock_condition_fr text not null default 'Accès immédiat', high_risk_session_limit_en text not null default 'Suggested smaller sessions', - high_risk_session_limit_fr text not null default 'Sessions plus courtes suggerees', + high_risk_session_limit_fr text not null default 'Sessions plus courtes suggérées', high_risk_condition_en text not null default 'Low completion or poor consistency', - high_risk_condition_fr text not null default 'Faible completion ou faible regularite', + high_risk_condition_fr text not null default 'Faible complétion ou faible régularité', updated_by uuid references public.users(id) on delete set null, updated_at timestamptz not null default timezone('utc', now()), constraint app_policy_settings_trial_warning_lte_limit diff --git a/supabase/migrations/20260615143000_admin_policy_french_copy.sql b/supabase/migrations/20260615143000_admin_policy_french_copy.sql new file mode 100644 index 0000000..9b9ff40 --- /dev/null +++ b/supabase/migrations/20260615143000_admin_policy_french_copy.sql @@ -0,0 +1,30 @@ +alter table public.app_policy_settings + alter column consistent_trial_unlock_condition_fr set default 'Maintenir la révision', + alter column paid_unlock_condition_fr set default 'Accès immédiat', + alter column high_risk_session_limit_fr set default 'Sessions plus courtes suggérées', + alter column high_risk_condition_fr set default 'Faible complétion ou faible régularité'; + +update public.app_policy_settings +set + consistent_trial_unlock_condition_fr = case + when consistent_trial_unlock_condition_fr = 'Maintenir la revision' + then 'Maintenir la révision' + else consistent_trial_unlock_condition_fr + end, + paid_unlock_condition_fr = case + when paid_unlock_condition_fr = 'Acces immediat' + then 'Accès immédiat' + else paid_unlock_condition_fr + end, + high_risk_session_limit_fr = case + when high_risk_session_limit_fr = 'Sessions plus courtes suggerees' + then 'Sessions plus courtes suggérées' + else high_risk_session_limit_fr + end, + high_risk_condition_fr = case + when high_risk_condition_fr = 'Faible completion ou faible regularite' + then 'Faible complétion ou faible régularité' + else high_risk_condition_fr + end, + updated_at = timezone('utc', now()) +where id = 'default';