Skip to content

Commit cbaffd3

Browse files
lane711claude
andauthored
fix(oauth-providers): show Settings tab and render provider fields (#987)
- hasUserSettings now returns true when a custom renderer is registered, so Settings tab is visible even with empty/null plugin DB settings - Adds renderOAuthProvidersSettingsContent with GitHub + Google credential fields (clientId, clientSecret, enabled toggle) - Custom saveSettings reconstructs nested providers structure before POST - Registers oauth-providers in pluginSettingsComponents Fixes: Settings tab hidden until user manually saves a dummy entry Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 41348e9 commit cbaffd3

2 files changed

Lines changed: 149 additions & 3 deletions

File tree

packages/core/src/templates/pages/admin-plugin-settings.template.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export interface PluginSettingsPageData {
6060

6161
export function renderPluginSettingsPage(data: PluginSettingsPageData): string {
6262
const { plugin, activity = [], user } = data
63-
// true only when the plugin has at least one user-configurable setting key
64-
// (_-prefixed keys are internal metadata, not settings the user can edit)
65-
const hasUserSettings = Object.keys(plugin.settings || {}).some(k => !k.startsWith('_'))
63+
// true when plugin has configurable settings or a custom renderer (which handles empty state itself)
64+
const hasUserSettings = plugin.id in pluginSettingsComponents ||
65+
Object.keys(plugin.settings || {}).some(k => !k.startsWith('_'))
6666
const defaultTab = hasUserSettings ? 'settings' : 'info'
6767

6868
const pageContent = `
@@ -768,6 +768,7 @@ type PluginSettingsRenderer = (plugin: any, settings: PluginSettings) => string
768768
const pluginSettingsComponents: Record<string, PluginSettingsRenderer> = {
769769
'otp-login': renderOTPLoginSettingsContent,
770770
'email': renderEmailSettingsContent,
771+
'oauth-providers': renderOAuthProvidersSettingsContent,
771772
}
772773

773774
/**
@@ -1521,6 +1522,108 @@ defineUserProfile({
15211522
`
15221523
}
15231524

1525+
/**
1526+
* OAuth Providers plugin settings content
1527+
*/
1528+
function renderOAuthProvidersSettingsContent(_plugin: any, settings: PluginSettings): string {
1529+
const providers = (settings.providers as any) || {}
1530+
const github = providers.github || {}
1531+
const google = providers.google || {}
1532+
1533+
const inputClass = 'w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none text-white'
1534+
1535+
function renderProviderCard(name: string, label: string, icon: string, data: any): string {
1536+
const clientId = escapeHtmlAttr(data.clientId || '')
1537+
const clientSecret = escapeHtmlAttr(data.clientSecret || '')
1538+
const enabled = data.enabled === true
1539+
1540+
return `
1541+
<div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6">
1542+
<div class="flex items-center justify-between mb-4">
1543+
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
1544+
<span>${icon}</span> ${label}
1545+
</h3>
1546+
<label class="relative inline-flex items-center cursor-pointer">
1547+
<input type="checkbox" id="oauth_${name}_enabled" ${enabled ? 'checked' : ''} class="sr-only peer">
1548+
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
1549+
<span class="ml-2 text-sm text-gray-300">Enabled</span>
1550+
</label>
1551+
</div>
1552+
<div class="space-y-4">
1553+
<div>
1554+
<label for="oauth_${name}_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
1555+
<input type="text" id="oauth_${name}_clientId" value="${clientId}" placeholder="${label} OAuth Client ID" class="${inputClass}">
1556+
</div>
1557+
<div>
1558+
<label for="oauth_${name}_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
1559+
<input type="password" id="oauth_${name}_clientSecret" value="${clientSecret}" placeholder="${label} OAuth Client Secret" class="${inputClass}">
1560+
</div>
1561+
</div>
1562+
</div>
1563+
`
1564+
}
1565+
1566+
return `
1567+
<div class="space-y-6">
1568+
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
1569+
<p class="text-blue-200 text-sm">
1570+
Configure OAuth provider credentials below. Credentials are stored securely and used server-side only.
1571+
Make sure to add the callback URL <code class="text-blue-300">/auth/oauth/[provider]/callback</code> to your OAuth app's allowed redirect URIs.
1572+
</p>
1573+
</div>
1574+
1575+
${renderProviderCard('github', 'GitHub', '🐙', github)}
1576+
${renderProviderCard('google', 'Google', '🔵', google)}
1577+
</div>
1578+
1579+
<script>
1580+
(function () {
1581+
const origSave = window.saveSettings;
1582+
window.saveSettings = async function () {
1583+
const settings = {
1584+
providers: {
1585+
github: {
1586+
clientId: document.getElementById('oauth_github_clientId').value,
1587+
clientSecret: document.getElementById('oauth_github_clientSecret').value,
1588+
enabled: document.getElementById('oauth_github_enabled').checked,
1589+
},
1590+
google: {
1591+
clientId: document.getElementById('oauth_google_clientId').value,
1592+
clientSecret: document.getElementById('oauth_google_clientSecret').value,
1593+
enabled: document.getElementById('oauth_google_enabled').checked,
1594+
},
1595+
}
1596+
};
1597+
1598+
const saveButton = document.getElementById('save-button');
1599+
saveButton.disabled = true;
1600+
saveButton.textContent = 'Saving...';
1601+
1602+
try {
1603+
const response = await fetch('/admin/plugins/oauth-providers/settings', {
1604+
method: 'POST',
1605+
headers: { 'Content-Type': 'application/json' },
1606+
body: JSON.stringify(settings),
1607+
});
1608+
const result = await response.json();
1609+
if (result.success) {
1610+
showNotification('Settings saved successfully', 'success');
1611+
setTimeout(() => location.reload(), 1000);
1612+
} else {
1613+
throw new Error(result.error || 'Failed to save settings');
1614+
}
1615+
} catch (error) {
1616+
showNotification(error.message, 'error');
1617+
} finally {
1618+
saveButton.disabled = false;
1619+
saveButton.textContent = 'Save Settings';
1620+
}
1621+
};
1622+
})();
1623+
</script>
1624+
`
1625+
}
1626+
15241627
/**
15251628
* Check if a plugin has a custom settings component
15261629
*/
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { test, expect } from '@playwright/test'
2+
import { loginAsAdmin } from './utils/test-helpers'
3+
4+
test.describe('OAuth Providers - settings page', () => {
5+
test.beforeEach(async ({ page }) => {
6+
await loginAsAdmin(page)
7+
})
8+
9+
test('shows Settings tab without needing prior saved settings', async ({ page }) => {
10+
await page.goto('/admin/plugins/oauth-providers')
11+
await expect(page.locator('#settings-tab')).toBeVisible()
12+
})
13+
14+
test('renders GitHub and Google credential fields', async ({ page }) => {
15+
await page.goto('/admin/plugins/oauth-providers#settings')
16+
await expect(page.locator('#oauth_github_clientId')).toBeVisible()
17+
await expect(page.locator('#oauth_github_clientSecret')).toBeVisible()
18+
await expect(page.locator('#oauth_github_enabled')).toBeAttached()
19+
await expect(page.locator('#oauth_google_clientId')).toBeVisible()
20+
await expect(page.locator('#oauth_google_clientSecret')).toBeVisible()
21+
await expect(page.locator('#oauth_google_enabled')).toBeAttached()
22+
})
23+
24+
test('saves and reloads nested provider settings correctly', async ({ page }) => {
25+
await page.goto('/admin/plugins/oauth-providers#settings')
26+
27+
await page.fill('#oauth_github_clientId', 'test-gh-client-id')
28+
await page.fill('#oauth_github_clientSecret', 'test-gh-secret')
29+
30+
const [response] = await Promise.all([
31+
page.waitForResponse(r => r.url().includes('/admin/plugins/oauth-providers/settings') && r.request().method() === 'POST'),
32+
page.click('#save-button'),
33+
])
34+
35+
expect(response.status()).toBe(200)
36+
const body = await response.json()
37+
expect(body.success).toBe(true)
38+
39+
// After reload, values persist
40+
await page.goto('/admin/plugins/oauth-providers#settings')
41+
await expect(page.locator('#oauth_github_clientId')).toHaveValue('test-gh-client-id')
42+
})
43+
})

0 commit comments

Comments
 (0)