Spaces:
Running
Running
File size: 5,629 Bytes
dd87944 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import React, { useCallback, useEffect, useState } from "react";
import { http, getActiveBase, BACKEND_URL, getSessionToken } from "../lib/api";
import { frontendUrl } from "../lib/paths";
import { Github, Link2, Unlink, RefreshCw } from "lucide-react";
import { toast } from "sonner";
const PROVIDER_ICONS = {
github: Github,
google: () => (
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
),
};
export default function ConnectedAccountsPanel() {
const [accounts, setAccounts] = useState([]);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(null);
const load = useCallback(async () => {
setLoading(true);
try {
const r = await http.get("/connections");
setAccounts(r.data?.accounts || []);
} catch (e) {
toast.error(e?.response?.data?.detail || "Impossible de charger les comptes");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const connect = (provider) => {
const base = (getActiveBase() || BACKEND_URL || "").replace(/\/$/, "");
if (!base) {
toast.error("Backend indisponible");
return;
}
const returnUrl = frontendUrl("/auth/connections/callback");
const qs = new URLSearchParams({ return_url: returnUrl });
const session = getSessionToken();
if (session) qs.set("session", session);
window.location.href = `${base}/api/oauth/${provider}/start?${qs.toString()}`;
};
const disconnect = async (provider) => {
if (!window.confirm(`Déconnecter ${provider} ?`)) return;
setBusy(provider);
try {
await http.delete(`/connections/${provider}`);
toast.success("Compte déconnecté");
await load();
} catch (e) {
toast.error(e?.response?.data?.detail || "Erreur");
} finally {
setBusy(null);
}
};
if (loading) {
return (
<div className="flex justify-center py-8">
<div className="dot-loading"><span /><span /><span /></div>
</div>
);
}
return (
<div className="space-y-3" data-testid="connected-accounts-panel">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-em">
Lie GitHub ou Google pour qu'Émo agisse avec tes comptes (tokens côté serveur uniquement).
</p>
<button type="button" onClick={load} className="p-1.5 rounded em-hover text-muted-em" aria-label="Actualiser">
<RefreshCw size={14} />
</button>
</div>
{accounts.map((acc) => {
const Icon = PROVIDER_ICONS[acc.provider] || Link2;
const connected = acc.connected;
const configured = acc.configured;
const display = acc.profile?.display || acc.profile?.login;
return (
<div key={acc.provider} className="emo-settings-card flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: "var(--emo-subtle-bg)", color: "var(--emo-text-secondary)" }}
>
<Icon size={18} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium" style={{ color: "var(--emo-text)" }}>
{acc.label || acc.provider}
</p>
{connected && display ? (
<p className="text-xs text-muted-em truncate">Connecté en tant que {display}</p>
) : configured ? (
<p className="text-xs text-muted-em">Non connecté</p>
) : (
<p className="text-xs" style={{ color: "var(--emo-warning-text)" }}>
{acc.message || "Configure keys on server"}
</p>
)}
</div>
<div className="flex-shrink-0">
{connected ? (
<button
type="button"
data-testid={`disconnect-${acc.provider}`}
disabled={busy === acc.provider}
onClick={() => disconnect(acc.provider)}
className="emo-btn-ghost px-3 py-2 rounded-lg text-xs flex items-center gap-1.5"
>
<Unlink size={12} /> Déconnecter
</button>
) : (
<button
type="button"
data-testid={`connect-${acc.provider}`}
disabled={!configured || busy === acc.provider}
onClick={() => connect(acc.provider)}
className="emo-btn-primary px-3 py-2 rounded-lg text-xs flex items-center gap-1.5 disabled:opacity-40"
>
<Link2 size={12} /> Connecter
</button>
)}
</div>
</div>
);
})}
</div>
);
}
|