Add Odoo connection scoping UI to API Tokens page

When creating a key, pick "All connections (unscoped)" or lock it to one
specific Odoo connection — matches the new per-key instance_name scoping
on the backend. The keys table now shows each key's scope (or "All
connections" for unscoped keys).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
MOHAN 2026-07-02 02:28:48 +05:30
parent 98de0a4675
commit df82c51754
2 changed files with 39 additions and 5 deletions

View File

@ -87,6 +87,7 @@ export interface ApiKeyInfo {
last_used_at: string | null;
revoked_at: string | null;
is_active: boolean;
instance_name: string | null;
}
export interface AnalyticsSummary {
@ -213,10 +214,10 @@ export const api = {
listApiKeys: (apiKey: string) =>
req<{ keys: ApiKeyInfo[]; total: number }>('/api/keys', { headers: authHeader(apiKey) }),
createApiKey: (apiKey: string, name: string) =>
req<{ id: number; name: string; api_key: string; mcp_connection: { url: string; header: string } }>(
createApiKey: (apiKey: string, name: string, instanceName?: string) =>
req<{ id: number; name: string; instance_name: string | null; api_key: string; mcp_connection: { url: string; header: string } }>(
'/api/keys',
{ method: 'POST', headers: authHeader(apiKey), body: JSON.stringify({ name }) },
{ method: 'POST', headers: authHeader(apiKey), body: JSON.stringify({ name, instance_name: instanceName || null }) },
),
revokeApiKey: (apiKey: string, id: number) =>

View File

@ -4,8 +4,10 @@ import { Plus, Trash2, Key, Copy, Check, AlertTriangle } from 'lucide-react';
export default function Tokens() {
const [keys, setKeys] = useState<ApiKeyInfo[]>([]);
const [instances, setInstances] = useState<string[]>([]);
const [showForm, setShowForm] = useState(false);
const [name, setName] = useState('');
const [scopedInstance, setScopedInstance] = useState('');
const [loading, setLoading] = useState(false);
const [newKey, setNewKey] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
@ -14,6 +16,7 @@ export default function Tokens() {
const key = session.getKey();
if (!key) return;
api.listApiKeys(key).then((r) => setKeys(r.keys)).catch(console.error);
api.listCredentials(key).then((r) => setInstances(r.instances.map((i) => i.instance_name))).catch(console.error);
}
useEffect(load, []);
@ -23,9 +26,10 @@ export default function Tokens() {
if (!key) return;
setLoading(true);
try {
const result = await api.createApiKey(key, name || 'Untitled key');
const result = await api.createApiKey(key, name || 'Untitled key', scopedInstance || undefined);
setNewKey(result.api_key);
setName('');
setScopedInstance('');
setShowForm(false);
load();
} catch (err) {
@ -60,7 +64,8 @@ export default function Tokens() {
<div>
<h1 className="text-xl font-bold text-gray-900">API Tokens</h1>
<p className="text-sm text-gray-500 mt-0.5">
Create and revoke API keys used to connect MCP clients.
Create and revoke API keys used to connect MCP clients. Each key can be locked
to a single Odoo connection, or left unscoped to access all of them.
</p>
</div>
<button
@ -116,6 +121,24 @@ export default function Tokens() {
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1.5">Odoo Connection</label>
<select
value={scopedInstance}
onChange={(e) => setScopedInstance(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
>
<option value="">All connections (unscoped)</option>
{instances.map((i) => (
<option key={i} value={i}>{i}</option>
))}
</select>
<p className="text-xs text-gray-400 mt-1.5">
{scopedInstance
? `This key will only work with the "${scopedInstance}" connection.`
: 'This key can access any of your Odoo connections.'}
</p>
</div>
<div className="flex gap-3 pt-2">
<button
type="submit"
@ -151,6 +174,7 @@ export default function Tokens() {
<tr className="text-left text-xs text-gray-500">
<th className="px-5 py-3 font-medium">Name</th>
<th className="px-5 py-3 font-medium">Key</th>
<th className="px-5 py-3 font-medium">Odoo Connection</th>
<th className="px-5 py-3 font-medium">Created</th>
<th className="px-5 py-3 font-medium">Last Used</th>
<th className="px-5 py-3 font-medium">Status</th>
@ -162,6 +186,15 @@ export default function Tokens() {
<tr key={k.id}>
<td className="px-5 py-3.5 font-medium text-gray-900">{k.name}</td>
<td className="px-5 py-3.5 font-mono text-xs text-gray-500">{k.key_prefix}...</td>
<td className="px-5 py-3.5">
{k.instance_name ? (
<span className="inline-flex items-center gap-1 text-xs text-brand-700 bg-brand-50 px-2 py-0.5 rounded-full">
{k.instance_name}
</span>
) : (
<span className="text-xs text-gray-400">All connections</span>
)}
</td>
<td className="px-5 py-3.5 text-xs text-gray-500">{k.created_at}</td>
<td className="px-5 py-3.5 text-xs text-gray-500">{k.last_used_at ?? 'Never'}</td>
<td className="px-5 py-3.5">