ultisuite-client/components/gmail/settings/automation/api-tokens-panel.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

246 lines
9.3 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 { 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,
defaultMailScope,
emptyPermissionGrants,
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 [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 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)
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(),
})
setCreated(result)
setCreatedOpen(true)
setName("")
setPermissions(emptyPermissionGrants())
setMailScope(defaultMailScope())
setDriveScope(defaultDriveScope())
}
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 et Drive 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}
/>
</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&apos;est affiché qu&apos;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&apos;autres
tokens via l&apos;API réservez-la aux agents d&apos;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>
)
}