import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams } from 'react-router-dom'; import { api } from '@/api/client'; import type { CreatedApiKey } from '@/api/types'; import { humanMessage } from '@/auth/AuthProvider'; import { ProjectNav } from '@/components/ProjectNav'; import { RequiresAdmin } from '@/components/RequiresAdmin'; /** * Clés API et prise en main. * * Deux idées gouvernent cet écran : * * 1. **Le secret n'apparaît qu'une fois**, immédiatement après la création, dans * un encart qu'on ne peut pas manquer. Il n'est pas stocké en clair : le * réafficher est techniquement impossible, et le dire franchement évite de * laisser croire qu'on pourra le retrouver plus tard. * * 2. **Le mode d'emploi est SOUS la clé**, pas dans une documentation à part. * Le moment où quelqu'un crée une clé est exactement celui où il cherche * quoi en faire. */ export function IntegrationPage() { const { projectUuid = '' } = useParams(); const queryClient = useQueryClient(); const [created, setCreated] = useState(null); const stats = useQuery({ queryKey: ['stats', projectUuid], queryFn: () => api.stats(projectUuid) }); const data = useQuery({ queryKey: ['api-keys', projectUuid], queryFn: () => api.apiKeys(projectUuid) }); const revoke = useMutation({ mutationFn: (uuid: string) => api.revokeApiKey(projectUuid, uuid), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['api-keys', projectUuid] }), }); // Le serveur refuse déjà ; ici on refuse LISIBLEMENT. if (stats.isSuccess && !stats.data.viewer.canAdminister) { return ; } return (

Intégration

Clés d'accès et prise en main du CLI.

{created !== null && setCreated(null)} />} e.slug)} scopes={(data.data?.scopes ?? []).map((s) => s.value)} onCreated={(key) => { setCreated(key); void queryClient.invalidateQueries({ queryKey: ['api-keys', projectUuid] }); }} />

Clés existantes

{data.data?.keys.length === 0 && (

Aucune clé. Créez-en une pour brancher une application.

)} {data.data?.keys.map((key) => (

{key.name}

{key.prefix}…

{key.environment} {key.scopes.join(' · ')} {/* Une clé jamais utilisée est soit oubliée, soit remplacée sans avoir été révoquée. Dans les deux cas elle devrait disparaître. */} {key.revokedAt !== null ? 'révoquée' : key.lastUsedAt === null ? 'jamais utilisée' : `vue le ${new Date(key.lastUsedAt).toLocaleDateString('fr-FR')}`} {key.isUsable && ( )}
))}
); } function SecretBanner({ created, onDismiss }: { created: CreatedApiKey; onDismiss: () => void }) { const [copied, setCopied] = useState(false); return (

{created.warning}

{created.secret}

Elle n'est pas stockée en clair : la réafficher est impossible. Si vous la perdez, révoquez-la et créez-en une nouvelle.

); } function CreateKeyForm({ projectUuid, environments, scopes, onCreated, }: { projectUuid: string; environments: string[]; scopes: string[]; onCreated: (key: CreatedApiKey) => void; }) { const [name, setName] = useState(''); const [environment, setEnvironment] = useState(''); const [selected, setSelected] = useState(['translations:read']); const [error, setError] = useState(null); const create = useMutation({ mutationFn: () => api.createApiKey(projectUuid, { name, environment: environment || (environments[0] ?? ''), scopes: selected, }), onSuccess: (key) => { setError(null); setName(''); onCreated(key); }, onError: (caught) => setError(humanMessage(caught)), }); return (
{ e.preventDefault(); create.mutate(); }} className="mt-6 rounded-lg border border-ink-200 bg-white p-5 shadow-sm" >

Créer une clé

setName(e.target.value)} placeholder="CI application web" className="min-w-56 flex-1 rounded-md border border-ink-300 px-3 py-2 text-sm outline-none focus:border-accent" />

Le nom est ce qui permettra de savoir quoi révoquer dans six mois.

Permissions

{scopes.map((scope) => { const on = selected.includes(scope); const isWrite = scope.includes('write') || scope.includes('publish'); return ( ); })}
{/* Les scopes d'écriture sont d'une autre couleur : une clé de production ne devrait en porter aucun, et l'écart visuel rend l'erreur difficile à commettre sans la voir. */}

Une clé de production n'a besoin que de lecture. Les permissions d'écriture (en orange) sont réservées aux clés de développement et d'intégration continue.

{error !== null && (

{error}

)}
); } function GettingStarted({ project }: { project: string }) { return (

Brancher une application

  1. 1. Installer le CLI

    {`curl -sSL https://tq-slator.internal/tqs.phar -o /usr/local/bin/tqs\nchmod +x /usr/local/bin/tqs`}
  2. 2. Lier le dépôt

    {`export TQS_API_KEY=tqs_…\ntqs init`}

    Projet, environnement, plateformes et langues sont déduits de la clé. Le fichier tqs.config.json produit se committe — il ne contient aucun secret.

  3. 3. Envoyer les clés du code

    {`tqs push --dry-run # voir le diff\ntqs push # appliquer`}
  4. 4. Récupérer les traductions en intégration continue

    {`tqs pull\ntqs pull --check # échoue si des fichiers ne sont pas commités\ntqs status --fail-under=80 # échoue si une langue passe sous 80 %`}

Les fichiers récupérés sont commités dans votre dépôt : votre production ne dépend donc jamais de la disponibilité de TQ-Slator. Projet visé :{' '} {project}

); } function Snippet({ children }: { children: string }) { return (
            {children}
        
); }