Skip to content

Commit 2feb68f

Browse files
author
Matthew Valancy
committed
feat: add OAuth provider configuration UI to admin panel
- Add new 'OAuth Providers' tab in admin panel - Implement OAuthProviderManagement component with: - Configuration for Google, LinkedIn, and GitHub OAuth providers - Enable/disable toggle for each provider - Client ID and Client Secret inputs with show/hide feature - Read-only callback URL display with copy-to-clipboard - Status indicators (configured/not configured) - Save/Reset functionality - Setup instructions for admins - UI follows existing admin panel patterns and styling - TODO: Backend API integration for persisting OAuth config
1 parent 2b516c6 commit 2feb68f

1 file changed

Lines changed: 249 additions & 1 deletion

File tree

packages/web/src/pages/Admin.tsx

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2-
import { Users, Database, Shield, Download, Upload, Settings2, RefreshCw, AlertCircle, Lock, Key, Globe, CheckCircle, XCircle, AlertTriangle, FileText, Calendar, Server, Network } from 'lucide-react';
2+
import { Users, Database, Shield, Download, Upload, Settings2, RefreshCw, AlertCircle, Lock, Key, Globe, CheckCircle, XCircle, AlertTriangle, FileText, Calendar, Server, Network, Copy, Eye, EyeOff } from 'lucide-react';
33
import { useAuth } from '../contexts/AuthContext';
44
import { AdminUserManagement } from '../components/AdminUserManagement';
55
import { CustomDropdown } from '../components/CustomDropdown';
@@ -26,6 +26,7 @@ export function Admin() {
2626
const tabs = [
2727
{ id: 'users', name: 'Users', icon: Users, description: 'Manage user roles and permissions' },
2828
{ id: 'settings', name: 'Registration', icon: Settings2, description: 'User registration policies and defaults' },
29+
{ id: 'oauth', name: 'OAuth Providers', icon: Key, description: 'Configure OAuth authentication providers' },
2930
{ id: 'database', name: 'Database', icon: Database, description: 'Database configuration and maintenance' },
3031
{ id: 'security', name: 'Security', icon: Shield, description: 'Security settings and passwords' },
3132
{ id: 'backup', name: 'Backup & Restore', icon: Download, description: 'System backup and restore' },
@@ -37,6 +38,8 @@ export function Admin() {
3738
return <AdminUserManagement />;
3839
case 'settings':
3940
return <UserManagementSettings />;
41+
case 'oauth':
42+
return <OAuthProviderManagement />;
4043
case 'database':
4144
return <DatabaseManagement />;
4245
case 'security':
@@ -262,6 +265,251 @@ function UserManagementSettings() {
262265
);
263266
}
264267

268+
// OAuth Provider Management Component
269+
function OAuthProviderManagement() {
270+
const [providers, setProviders] = useState({
271+
google: {
272+
enabled: false,
273+
clientId: '',
274+
clientSecret: '',
275+
callbackUrl: 'https://localhost:4128/auth/google/callback',
276+
configured: false,
277+
},
278+
linkedin: {
279+
enabled: false,
280+
clientId: '',
281+
clientSecret: '',
282+
callbackUrl: 'https://localhost:4128/auth/linkedin/callback',
283+
configured: false,
284+
},
285+
github: {
286+
enabled: false,
287+
clientId: '',
288+
clientSecret: '',
289+
callbackUrl: 'https://localhost:4128/auth/github/callback',
290+
configured: false,
291+
},
292+
});
293+
294+
const [showSecrets, setShowSecrets] = useState({
295+
google: false,
296+
linkedin: false,
297+
github: false,
298+
});
299+
300+
const [saved, setSaved] = useState(false);
301+
const [loading, setLoading] = useState(false);
302+
303+
useEffect(() => {
304+
// TODO: Load OAuth provider configuration from backend
305+
console.log('Loading OAuth provider configuration...');
306+
}, []);
307+
308+
const handleSave = async () => {
309+
setLoading(true);
310+
try {
311+
// TODO: Save OAuth provider configuration to backend
312+
console.log('Saving OAuth provider configuration:', providers);
313+
314+
// Simulate API call
315+
await new Promise(resolve => setTimeout(resolve, 1000));
316+
317+
setSaved(true);
318+
setTimeout(() => setSaved(false), 3000);
319+
} catch (error) {
320+
console.error('Failed to save OAuth configuration:', error);
321+
} finally {
322+
setLoading(false);
323+
}
324+
};
325+
326+
const handleCopyCallback = (url: string) => {
327+
navigator.clipboard.writeText(url);
328+
console.log('Copied callback URL:', url);
329+
};
330+
331+
const toggleShowSecret = (provider: 'google' | 'linkedin' | 'github') => {
332+
setShowSecrets(prev => ({ ...prev, [provider]: !prev[provider] }));
333+
};
334+
335+
const updateProvider = (provider: 'google' | 'linkedin' | 'github', field: string, value: any) => {
336+
setProviders(prev => ({
337+
...prev,
338+
[provider]: {
339+
...prev[provider],
340+
[field]: value,
341+
configured: field === 'clientId' || field === 'clientSecret'
342+
? (value && prev[provider].clientId && prev[provider].clientSecret)
343+
: prev[provider].configured,
344+
},
345+
}));
346+
};
347+
348+
const renderProviderConfig = (
349+
providerKey: 'google' | 'linkedin' | 'github',
350+
providerName: string,
351+
providerIcon: React.ReactNode
352+
) => {
353+
const provider = providers[providerKey];
354+
const showSecret = showSecrets[providerKey];
355+
356+
return (
357+
<div key={providerKey} className="bg-gray-800 border border-gray-700 rounded-lg p-6">
358+
<div className="flex items-center justify-between mb-6">
359+
<div className="flex items-center space-x-3">
360+
{providerIcon}
361+
<div>
362+
<h3 className="text-lg font-semibold text-gray-100">{providerName}</h3>
363+
<p className="text-sm text-gray-400">
364+
{provider.configured
365+
? <span className="flex items-center text-green-400"><CheckCircle className="h-4 w-4 mr-1" /> Configured</span>
366+
: <span className="flex items-center text-gray-500"><AlertCircle className="h-4 w-4 mr-1" /> Not configured</span>
367+
}
368+
</p>
369+
</div>
370+
</div>
371+
<div className="flex items-center space-x-2">
372+
<span className="text-sm text-gray-400">Enable</span>
373+
<input
374+
type="checkbox"
375+
checked={provider.enabled}
376+
onChange={(e) => updateProvider(providerKey, 'enabled', e.target.checked)}
377+
className="h-4 w-4 text-green-500 focus:ring-green-500 border-gray-500 bg-gray-700 rounded"
378+
/>
379+
</div>
380+
</div>
381+
382+
<div className="space-y-4">
383+
<div>
384+
<label className="block text-sm font-medium text-gray-300 mb-2">
385+
Client ID
386+
</label>
387+
<input
388+
type="text"
389+
value={provider.clientId}
390+
onChange={(e) => updateProvider(providerKey, 'clientId', e.target.value)}
391+
placeholder={`Enter ${providerName} Client ID`}
392+
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
393+
/>
394+
</div>
395+
396+
<div>
397+
<label className="block text-sm font-medium text-gray-300 mb-2">
398+
Client Secret
399+
</label>
400+
<div className="relative">
401+
<input
402+
type={showSecret ? 'text' : 'password'}
403+
value={provider.clientSecret}
404+
onChange={(e) => updateProvider(providerKey, 'clientSecret', e.target.value)}
405+
placeholder={`Enter ${providerName} Client Secret`}
406+
className="w-full px-3 py-2 pr-10 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
407+
/>
408+
<button
409+
type="button"
410+
onClick={() => toggleShowSecret(providerKey)}
411+
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300"
412+
>
413+
{showSecret ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
414+
</button>
415+
</div>
416+
</div>
417+
418+
<div>
419+
<label className="block text-sm font-medium text-gray-300 mb-2">
420+
Callback URL
421+
<span className="text-gray-500 text-xs ml-2">(Copy this to your OAuth app configuration)</span>
422+
</label>
423+
<div className="flex items-center space-x-2">
424+
<input
425+
type="text"
426+
value={provider.callbackUrl}
427+
readOnly
428+
className="flex-1 px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-300 cursor-not-allowed"
429+
/>
430+
<button
431+
type="button"
432+
onClick={() => handleCopyCallback(provider.callbackUrl)}
433+
className="px-3 py-2 bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded-lg text-gray-300 transition-colors"
434+
title="Copy callback URL"
435+
>
436+
<Copy className="h-4 w-4" />
437+
</button>
438+
</div>
439+
</div>
440+
</div>
441+
</div>
442+
);
443+
};
444+
445+
return (
446+
<div className="max-w-5xl mx-auto px-6 py-8">
447+
<div className="mb-6">
448+
<h2 className="text-2xl font-bold text-gray-100 mb-2">OAuth Provider Configuration</h2>
449+
<p className="text-gray-400">
450+
Configure OAuth authentication providers for user sign-in. Users can login using their existing accounts from these providers.
451+
</p>
452+
</div>
453+
454+
<div className="space-y-6">
455+
{renderProviderConfig('google', 'Google', <Globe className="h-6 w-6 text-red-400" />)}
456+
{renderProviderConfig('linkedin', 'LinkedIn', <Network className="h-6 w-6 text-blue-400" />)}
457+
{renderProviderConfig('github', 'GitHub', <Server className="h-6 w-6 text-purple-400" />)}
458+
</div>
459+
460+
<div className="mt-8 flex items-center justify-between p-4 bg-gray-800/50 border border-gray-700 rounded-lg">
461+
<div className="text-sm text-gray-400">
462+
{saved && (
463+
<span className="flex items-center text-green-400">
464+
<CheckCircle className="h-4 w-4 mr-2" />
465+
OAuth configuration saved successfully
466+
</span>
467+
)}
468+
</div>
469+
<div className="flex space-x-3">
470+
<button
471+
onClick={() => window.location.reload()}
472+
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-lg transition-colors"
473+
disabled={loading}
474+
>
475+
Reset
476+
</button>
477+
<button
478+
onClick={handleSave}
479+
disabled={loading}
480+
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
481+
>
482+
{loading ? (
483+
<>
484+
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
485+
Saving...
486+
</>
487+
) : (
488+
'Save OAuth Configuration'
489+
)}
490+
</button>
491+
</div>
492+
</div>
493+
494+
<div className="mt-6 p-4 bg-yellow-900/20 border border-yellow-600/30 rounded-lg">
495+
<div className="flex items-start space-x-3">
496+
<AlertTriangle className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
497+
<div className="text-sm text-yellow-200/90">
498+
<p className="font-medium mb-1">Setup Instructions:</p>
499+
<ol className="list-decimal list-inside space-y-1 text-yellow-200/70">
500+
<li>Create an OAuth app in the provider's developer console</li>
501+
<li>Copy the Client ID and Client Secret</li>
502+
<li>Add the Callback URL to your OAuth app's allowed redirect URIs</li>
503+
<li>Paste the credentials here and enable the provider</li>
504+
<li>Save the configuration</li>
505+
</ol>
506+
</div>
507+
</div>
508+
</div>
509+
</div>
510+
);
511+
}
512+
265513
// Database Management Component with full admin tools
266514
function DatabaseManagement() {
267515
const [debugInfo, setDebugInfo] = useState<string[]>([]);

0 commit comments

Comments
 (0)