Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { ExternalLink, KeyRound, Plus, Trash2 } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { ApiTokenCreatedDialog } from "@/components/gmail/settings/automation/api-token-created-dialog"
|
|
import { ApiTokenDriveScopeEditor } from "@/components/gmail/settings/automation/api-token-drive-scope-editor"
|
|
import { ApiTokenAgendaScopeEditor } from "@/components/gmail/settings/automation/api-token-agenda-scope-editor"
|
|
import { ApiTokenMailScopeEditor } from "@/components/gmail/settings/automation/api-token-mail-scope-editor"
|
|
import { ApiTokenPermissionEditor } from "@/components/gmail/settings/automation/api-token-permission-editor"
|
|
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
|
|
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
|
|
import {
|
|
useApiTokens,
|
|
useCreateApiToken,
|
|
useRevokeApiToken,
|
|
} from "@/lib/api/hooks/use-api-tokens"
|
|
import { useAuthReady } from "@/lib/api/use-auth-ready"
|
|
import type { ApiTokenCreated } from "@/lib/api/types"
|
|
import {
|
|
defaultDriveScope,
|
|
defaultAgendaScope,
|
|
defaultMailScope,
|
|
emptyPermissionGrants,
|
|
hasAgendaPermissions,
|
|
hasAnyPermission,
|
|
hasDrivePermissions,
|
|
hasMailPermissions,
|
|
summarizePermissions,
|
|
type ApiTokenPermissionGrant,
|
|
} from "@/lib/mail-automation/api-token-permissions"
|
|
|
|
function formatDate(value?: string) {
|
|
if (!value) return "—"
|
|
try {
|
|
return new Intl.DateTimeFormat("fr-FR", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
}).format(new Date(value))
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
export function ApiTokensPanel() {
|
|
const { ready, authenticated } = useAuthReady()
|
|
const { data: tokens = [], isFetching, isError, refetch, isPending } = useApiTokens()
|
|
const createToken = useCreateApiToken()
|
|
const revokeToken = useRevokeApiToken()
|
|
|
|
const [name, setName] = useState("")
|
|
const [permissions, setPermissions] = useState<ApiTokenPermissionGrant[]>(emptyPermissionGrants)
|
|
const [mailScope, setMailScope] = useState(defaultMailScope)
|
|
const [driveScope, setDriveScope] = useState(defaultDriveScope)
|
|
const [agendaScope, setAgendaScope] = useState(defaultAgendaScope)
|
|
const [created, setCreated] = useState<ApiTokenCreated | null>(null)
|
|
const [createdOpen, setCreatedOpen] = useState(false)
|
|
|
|
const showInitialLoad = ready && authenticated && isPending && tokens.length === 0
|
|
const mailScopeEnabled = hasMailPermissions(permissions)
|
|
const driveScopeEnabled = hasDrivePermissions(permissions)
|
|
const agendaScopeEnabled = hasAgendaPermissions(permissions)
|
|
|
|
const canSubmit =
|
|
name.trim().length > 0 &&
|
|
hasAnyPermission(permissions) &&
|
|
(!mailScopeEnabled || mailScope.all_accounts || mailScope.account_ids.length > 0) &&
|
|
(!driveScopeEnabled || driveScope.all_folders || driveScope.folder_paths.length > 0) &&
|
|
(!agendaScopeEnabled || agendaScope.all_calendars || agendaScope.calendar_ids.length > 0)
|
|
|
|
async function handleCreate() {
|
|
const result = await createToken.mutateAsync({
|
|
name: name.trim(),
|
|
permissions: permissions.filter((g) => g.read || g.write),
|
|
mail_scope: mailScopeEnabled ? mailScope : defaultMailScope(),
|
|
drive_scope: driveScopeEnabled ? driveScope : defaultDriveScope(),
|
|
agenda_scope: agendaScopeEnabled ? agendaScope : defaultAgendaScope(),
|
|
})
|
|
setCreated(result)
|
|
setCreatedOpen(true)
|
|
setName("")
|
|
setPermissions(emptyPermissionGrants())
|
|
setMailScope(defaultMailScope())
|
|
setDriveScope(defaultDriveScope())
|
|
setAgendaScope(defaultAgendaScope())
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
|
|
|
|
<AutomationTabMasonry columns={2}>
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Nouveau token</CardTitle>
|
|
<CardDescription>
|
|
Jetons fine-grained pour agents IA, scripts et intégrations externes. Choisissez
|
|
les permissions, puis restreignez le périmètre mail, Drive et agenda si nécessaire.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-5">
|
|
<p className="text-xs text-muted-foreground">
|
|
Documentation API interactive :{" "}
|
|
<a
|
|
href="/api/docs"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 text-[#1a73e8] hover:underline"
|
|
>
|
|
/api/docs
|
|
<ExternalLink className="size-3" />
|
|
</a>
|
|
</p>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Nom</Label>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Agent tri boîte support"
|
|
/>
|
|
</div>
|
|
|
|
<ApiTokenPermissionEditor grants={permissions} onChange={setPermissions} />
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<ApiTokenMailScopeEditor
|
|
scope={mailScope}
|
|
onChange={setMailScope}
|
|
enabled={mailScopeEnabled}
|
|
/>
|
|
<ApiTokenDriveScopeEditor
|
|
scope={driveScope}
|
|
onChange={setDriveScope}
|
|
enabled={driveScopeEnabled}
|
|
/>
|
|
<ApiTokenAgendaScopeEditor
|
|
scope={agendaScope}
|
|
onChange={setAgendaScope}
|
|
enabled={agendaScopeEnabled}
|
|
className="lg:col-span-2"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
disabled={!canSubmit || createToken.isPending}
|
|
className="w-full sm:w-auto"
|
|
onClick={handleCreate}
|
|
>
|
|
<Plus className="mr-1.5 size-3.5" />
|
|
Générer le token
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<KeyRound className="size-4" />
|
|
Tokens actifs
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Révoquez un jeton compromis ou inutilisé à tout moment.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{showInitialLoad ? (
|
|
<p className="text-sm text-muted-foreground">Chargement…</p>
|
|
) : tokens.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">Aucun token API pour le moment.</p>
|
|
) : (
|
|
<ul className="divide-y divide-border rounded-lg border border-border">
|
|
{tokens.map((token) => {
|
|
const summary = summarizePermissions(token.permissions)
|
|
return (
|
|
<li
|
|
key={token.id}
|
|
className="flex items-start justify-between gap-3 px-3 py-3"
|
|
>
|
|
<div className="min-w-0 space-y-1">
|
|
<p className="text-sm font-medium">{token.name}</p>
|
|
<p className="font-mono text-xs text-muted-foreground">
|
|
{token.token_prefix}…
|
|
</p>
|
|
{summary.length > 0 && (
|
|
<ul className="text-xs text-muted-foreground">
|
|
{summary.slice(0, 4).map((line) => (
|
|
<li key={line}>{line}</li>
|
|
))}
|
|
{summary.length > 4 && (
|
|
<li>+ {summary.length - 4} autre(s) permission(s)</li>
|
|
)}
|
|
</ul>
|
|
)}
|
|
<p className="text-xs text-muted-foreground">
|
|
Créé {formatDate(token.created_at)}
|
|
{token.last_used_at
|
|
? ` · Dernière utilisation ${formatDate(token.last_used_at)}`
|
|
: ""}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={revokeToken.isPending}
|
|
onClick={() => revokeToken.mutate(token.id)}
|
|
aria-label={`Révoquer ${token.name}`}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="space-y-3">
|
|
<CardTitle className="text-base">Bonnes pratiques</CardTitle>
|
|
<div className="space-y-2 text-sm text-muted-foreground">
|
|
<p>
|
|
Limitez chaque token au périmètre strict nécessaire (principe du moindre privilège).
|
|
</p>
|
|
<p>
|
|
Le secret n'est affiché qu'une fois à la création — stockez-le dans un
|
|
gestionnaire de secrets, jamais dans le code source.
|
|
</p>
|
|
<p>
|
|
La permission « Super admin — Tokens API » permet aussi de gérer d'autres
|
|
tokens via l'API — réservez-la aux agents d'administration de confiance.
|
|
</p>
|
|
<p>
|
|
Préférez des tokens dédiés par agent ou intégration pour faciliter la révocation.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</AutomationTabMasonry>
|
|
|
|
<ApiTokenCreatedDialog
|
|
created={created}
|
|
open={createdOpen}
|
|
onOpenChange={setCreatedOpen}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|