diff --git a/apps/web-dashboard/src/components/Settings/IntegrationsSettings.jsx b/apps/web-dashboard/src/components/Settings/IntegrationsSettings.jsx
index 3b5508b3b..1366b1a36 100644
--- a/apps/web-dashboard/src/components/Settings/IntegrationsSettings.jsx
+++ b/apps/web-dashboard/src/components/Settings/IntegrationsSettings.jsx
@@ -1,6 +1,7 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
- Database, Shield, Mail, HardDrive, Check, Copy, Server
+ Shield, Database, Mail, HardDrive, Check, Copy, Search, X, Eye, EyeOff,
+ CheckCircle, Info, Server
} from 'lucide-react';
import api from '../../utils/api';
import toast from 'react-hot-toast';
@@ -11,31 +12,62 @@ import DatabaseConfigForm from './DatabaseConfigForm';
import StorageConfigForm from './StorageConfigForm';
import MailTemplatesForm from './MailTemplatesForm';
-function IntegrationSectionCard({ title, description, icon: Icon, iconColor = 'var(--color-primary)', accentColor, children, style = {} }) {
- return (
-
- {accentColor && (
-
- )}
-
- {title && (
-
-
- {Icon && }
-
{title}
-
- {description && (
-
- {description}
-
- )}
-
- )}
- {children}
-
-
- );
-}
+/* ─── SVG Brand Icons ─── */
+const Icons = {
+ GitHub: () => (
+
+
+
+ ),
+ Google: () => (
+
+
+
+
+
+
+ ),
+ Apple: () => (
+
+
+
+ ),
+ Discord: () => (
+
+
+
+ ),
+ MongoDB: () => (
+
+
+
+ ),
+ Redis: () => (
+
+
+
+ ),
+ Resend: () => (
+
+
+
+ )
+};
+
+const normalizeAuthProviders = (raw) => ({
+ github: {
+ enabled: !!raw?.github?.enabled,
+ clientId: raw?.github?.clientId || '',
+ clientSecret: raw?.github?.clientSecret || '',
+ hasClientSecret: !!raw?.github?.hasClientSecret
+ },
+ google: {
+ enabled: !!raw?.google?.enabled,
+ clientId: raw?.google?.clientId || '',
+ clientSecret: raw?.google?.clientSecret || '',
+ hasClientSecret: !!raw?.google?.hasClientSecret
+ }
+});
export default function IntegrationsSettings({
project,
@@ -51,365 +83,251 @@ export default function IntegrationsSettings({
handleResendKeySave,
}) {
const isViewer = role === 'viewer';
+ const siteUrl = project?.siteUrl || '';
// Auth Providers State
- const [authProviders, setAuthProviders] = useState({
- github: { enabled: false, clientId: '', clientSecret: '', hasClientSecret: false },
- google: { enabled: false, clientId: '', clientSecret: '', hasClientSecret: false }
- });
+ const [authProviders, setAuthProviders] = useState(() => normalizeAuthProviders(project?.authProviders));
const [isSavingProviders, setIsSavingProviders] = useState(false);
- const [activeAuthTab, setActiveAuthTab] = useState('github');
- const [copiedUrl, setCopiedUrl] = useState(null);
+ const [selectedProviderModal, setSelectedProviderModal] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+
+ // Modal Form States
+ const [modalForm, setModalForm] = useState({ enabled: false, clientId: '', clientSecret: '' });
+ const [showSecret, setShowSecret] = useState(false);
+ const [copiedUrl, setCopiedUrl] = useState(false);
+
+ const closeButtonRef = useRef(null);
- const siteUrl = project?.siteUrl || '';
const githubCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/github/callback`;
const googleCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/google/callback`;
useEffect(() => {
if (project?.authProviders) {
- const providers = project.authProviders;
- Promise.resolve().then(() => setAuthProviders(providers));
+ const normalized = normalizeAuthProviders(project.authProviders);
+ Promise.resolve().then(() => setAuthProviders(normalized));
}
}, [project?.authProviders]);
- const handleProviderFieldChange = (provider, field, value) => {
- setAuthProviders((prev) => ({
- ...prev,
- [provider]: { ...prev[provider], [field]: value }
- }));
+ useEffect(() => {
+ if (!selectedProviderModal) return;
+ closeButtonRef.current?.focus();
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Escape') setSelectedProviderModal(null);
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [selectedProviderModal]);
+
+ const openOAuthModal = (providerKey) => {
+ const prov = authProviders[providerKey] || { enabled: false, clientId: '', clientSecret: '' };
+ setModalForm({
+ enabled: !!prov.enabled,
+ clientId: prov.clientId || '',
+ clientSecret: ''
+ });
+ setShowSecret(false);
+ setCopiedUrl(false);
+ setSelectedProviderModal(providerKey);
};
- const handleSaveProviders = async () => {
+ const handleSaveOAuthModal = async () => {
+ if (!selectedProviderModal) return;
+
+ // Validation: when enabling, clientId and clientSecret (new or existing) are required
+ if (modalForm.enabled) {
+ if (!modalForm.clientId.trim()) {
+ toast.error("Client ID is required when enabling a provider.");
+ return;
+ }
+ const existingHasSecret = authProviders[selectedProviderModal]?.hasClientSecret;
+ if (!modalForm.clientSecret.trim() && !existingHasSecret) {
+ toast.error("Client Secret is required when enabling a provider.");
+ return;
+ }
+ }
+
setIsSavingProviders(true);
try {
+ const updatedProviderData = {
+ enabled: !!modalForm.enabled,
+ clientId: modalForm.clientId,
+ ...(modalForm.clientSecret ? { clientSecret: modalForm.clientSecret } : {})
+ };
+
const payload = {
- github: {
+ github: selectedProviderModal === 'github' ? updatedProviderData : {
enabled: !!authProviders.github.enabled,
clientId: authProviders.github.clientId,
- ...(authProviders.github.clientSecret ? { clientSecret: authProviders.github.clientSecret } : {})
},
- google: {
+ google: selectedProviderModal === 'google' ? updatedProviderData : {
enabled: !!authProviders.google.enabled,
clientId: authProviders.google.clientId,
- ...(authProviders.google.clientSecret ? { clientSecret: authProviders.google.clientSecret } : {})
}
};
+
const res = await api.patch(`/api/projects/${projectId}/auth/providers`, payload);
- setAuthProviders(res.data.authProviders);
+ const normalized = normalizeAuthProviders(res.data.authProviders);
+ setAuthProviders(normalized);
if (onProjectUpdate) {
- onProjectUpdate(prev => ({ ...prev, authProviders: res.data.authProviders }));
+ onProjectUpdate(prev => ({ ...prev, authProviders: normalized }));
}
- toast.success('Auth provider settings saved!');
+ toast.success(`${selectedProviderModal === 'github' ? 'GitHub' : 'Google'} OAuth settings updated!`);
+ setSelectedProviderModal(null);
} catch (err) {
- toast.error(err.response?.data?.message || 'Failed to save auth provider settings');
+ toast.error(err.response?.data?.message || 'Failed to update provider settings');
} finally {
setIsSavingProviders(false);
}
};
- const copyToClipboard = async (text, type) => {
+ const copyToClipboard = async (text) => {
if (!navigator?.clipboard) {
toast.error('Clipboard access is not available.');
return;
}
try {
await navigator.clipboard.writeText(text);
- setCopiedUrl(type);
- toast.success('Callback URL copied!');
- setTimeout(() => setCopiedUrl(null), 2000);
+ setCopiedUrl(true);
+ toast.success('OAuth Callback URL copied!');
+ setTimeout(() => setCopiedUrl(false), 2000);
} catch {
toast.error('Failed to copy to clipboard.');
}
};
+ const oauthProvidersList = useMemo(() => {
+ const list = [
+ { id: 'github', name: 'GitHub', icon: Icons.GitHub, enabled: authProviders.github.enabled, configured: authProviders.github.hasClientSecret },
+ { id: 'google', name: 'Google', icon: Icons.Google, enabled: authProviders.google.enabled, configured: authProviders.google.hasClientSecret },
+ { id: 'apple', name: 'Apple', icon: Icons.Apple, disabled: true, statusText: 'Coming Soon' },
+ { id: 'discord', name: 'Discord', icon: Icons.Discord, disabled: true, statusText: 'Coming Soon' }
+ ];
+
+ if (!searchTerm.trim()) return list;
+ return list.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()));
+ }, [authProviders, searchTerm]);
+
+ const activeCallbackUrl = selectedProviderModal === 'google' ? googleCallbackUrl : githubCallbackUrl;
+
return (
-
+
- {/* 1. DATABASES SECTION */}
-
-
- {/* Primary MongoDB */}
-
-
- {/* Redis (Coming Soon) */}
-
-
-
-
-
-
Redis Cache
-
- High performance memory cache & pub/sub engine.
-
-
-
-
- Coming Soon
-
-
+ {/* 1. OAUTH2 PROVIDERS GRID */}
+
+
+
+
OAuth2 Providers
+
+ Enable third-party social logins for your application users.
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ style={{ ...inputStyle, paddingLeft: '30px', height: '32px', fontSize: '0.75rem' }}
+ />
-
-
- {/* 2. AUTH INTEGRATIONS SECTION */}
-
-
-
- {/* Site URL warning check */}
- {!siteUrl && (
-
- ⚠️ Site URL is not set: Please set your app's frontend URL in the General Settings so OAuth redirects to /auth/callback correctly.
-
- )}
-
- {/* Auth Provider Selector Tabs */}
-
- {[
- { id: 'github', label: 'GitHub OAuth', enabled: authProviders.github.enabled, configured: authProviders.github.hasClientSecret },
- { id: 'google', label: 'Google OAuth', enabled: authProviders.google.enabled, configured: authProviders.google.hasClientSecret }
- ].map((prov) => (
-
+ ⚠️ Site URL is not set: Please set your app's site URL in the General Settings tab so OAuth redirects to /auth/callback correctly.
+
+ )}
+
+
+ {oauthProvidersList.map((prov) => {
+ const ProviderIcon = prov.icon;
+ const isConfigured = prov.configured || prov.enabled;
+ return (
+
setActiveAuthTab(prov.id)}
+ role="button"
+ tabIndex={prov.disabled ? -1 : 0}
+ aria-disabled={prov.disabled}
+ onClick={() => !prov.disabled && openOAuthModal(prov.id)}
+ onKeyDown={(e) => {
+ if (!prov.disabled && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ openOAuthModal(prov.id);
+ }
+ }}
style={{
- padding: '8px 14px',
- background: 'transparent',
- border: 'none',
- borderBottom: activeAuthTab === prov.id ? '2px solid var(--color-primary)' : '2px solid transparent',
- color: activeAuthTab === prov.id ? '#fff' : 'var(--color-text-muted)',
- fontSize: '0.8rem',
- fontWeight: activeAuthTab === prov.id ? 600 : 400,
- cursor: 'pointer',
+ background: 'rgba(255,255,255,0.02)',
+ border: '1px solid var(--color-border)',
+ borderRadius: '12px',
+ padding: '1.25rem',
+ cursor: prov.disabled ? 'not-allowed' : 'pointer',
display: 'flex',
- alignItems: 'center',
- gap: '6px'
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ minHeight: '110px',
+ opacity: prov.disabled ? 0.6 : 1,
+ transition: 'all 0.2s ease',
}}
+ className={!prov.disabled ? "provider-card-hover" : ""}
>
- {prov.label}
- {prov.enabled && (
-
- )}
-
- ))}
-
-
- {/* Active Provider Form */}
- {activeAuthTab === 'github' && (
-
-
-
-
GitHub OAuth Integration
-
- Allow users to log in with their GitHub account.
-
+
-
- handleProviderFieldChange('github', 'enabled', e.target.checked)}
- disabled={isViewer}
- />
-
- {authProviders.github.enabled ? 'Enabled' : 'Disabled'}
-
-
-
-
-
-
-
copyToClipboard(githubCallbackUrl, 'github')}
- style={{ height: '32px', fontSize: '0.75rem', padding: '0 10px', display: 'flex', alignItems: 'center', gap: '4px' }}
- >
- {copiedUrl === 'github' ? : }
-
+
+
+ {prov.statusText || (prov.enabled ? 'enabled' : isConfigured ? 'configured' : 'disabled')}
+
-
-
-
-
- handleProviderFieldChange('github', 'clientId', e.target.value)}
- style={{ ...inputStyle, fontFamily: 'monospace' }}
- disabled={isViewer}
- />
-
-
-
- Client Secret{' '}
-
- • {authProviders.github.hasClientSecret ? 'Configured (encrypted)' : 'Not set'}
-
-
- }
- >
- handleProviderFieldChange('github', 'clientSecret', e.target.value)}
- style={{ ...inputStyle, fontFamily: 'monospace' }}
- disabled={isViewer}
- />
-
-
- )}
+ );
+ })}
+
+
- {activeAuthTab === 'google' && (
-
-
-
-
Google OAuth Integration
-
- Allow users to log in with Google accounts.
-
+ {/* 2. DATABASES SECTION */}
+
+
+
+
+ {/* 3. MAIL SECTION */}
+
+
Mail Delivery & Templates
+
+ Transactional email providers and dynamic template engine.
+
+
+
+
+
+
+
-
- handleProviderFieldChange('google', 'enabled', e.target.checked)}
- disabled={isViewer}
- />
-
- {authProviders.google.enabled ? 'Enabled' : 'Disabled'}
+
+
Resend.com (BYOK API Key)
+
+ {hasResendKey ? '• Configured (Encrypted at rest)' : '• Not configured'}
-
-
-
-
-
-
- copyToClipboard(googleCallbackUrl, 'google')}
- style={{ height: '32px', fontSize: '0.75rem', padding: '0 10px', display: 'flex', alignItems: 'center', gap: '4px' }}
- >
- {copiedUrl === 'google' ? : }
-
-
-
-
-
- handleProviderFieldChange('google', 'clientId', e.target.value)}
- style={{ ...inputStyle, fontFamily: 'monospace' }}
- disabled={isViewer}
- />
-
-
-
- Client Secret{' '}
-
- • {authProviders.google.hasClientSecret ? 'Configured (encrypted)' : 'Not set'}
-
-
- }
- >
- handleProviderFieldChange('google', 'clientSecret', e.target.value)}
- style={{ ...inputStyle, fontFamily: 'monospace' }}
- disabled={isViewer}
- />
-
- )}
-
- {!isViewer && (
-
-
- {isSavingProviders ? 'Saving Auth Settings...' : 'Save Auth Settings'}
-
-
- )}
-
-
-
- {/* 3. MAIL INTEGRATIONS SECTION */}
-
-
- {/* Provider Selection Info */}
-
-
- Active Provider: Resend.com (BYOK)
-
- SendGrid, AWS SES & urMail Engine support are in development.
-
-
-
- {/* Resend BYOK Card */}
-
-
Resend.com API Key
-
- Upload a per-project Resend API key to send mail from your custom domain.
-
-
- Resend API Key{' '}
-
- · {hasResendKey ? 'Configured' : 'Not configured'}
-
-
- }>
+
- Blank defaults to onboarding@resend.dev>}>
+ Defaults to onboarding@resend.dev>}>
{!isViewer && (
-
+
{resendKeyLoading ? "Saving..." : "Save Mail Settings"}
@@ -446,22 +364,165 @@ export default function IntegrationsSettings({
)}
- {/* Mail Templates Sub-form */}
-
-
- {/* 4. STORAGE INTEGRATIONS SECTION */}
-
+
+
+ {/* 4. STORAGE ENGINES */}
+
-
+
+
+ {/* ─── APPWRITE STYLE OAUTH2 PROVIDER SETTINGS MODAL ─── */}
+ {selectedProviderModal && (
+
setSelectedProviderModal(null)}
+ >
+
e.stopPropagation()}
+ >
+ {/* Close button */}
+
setSelectedProviderModal(null)}
+ aria-label="Close modal"
+ >
+
+
+
+ {/* Modal Header */}
+
+
+ {selectedProviderModal === 'github' ? : }
+
+
+
+ {selectedProviderModal === 'github' ? 'GitHub' : 'Google'} OAuth2 settings
+
+
+
+
+
+ To use {selectedProviderModal === 'github' ? 'GitHub' : 'Google'} authentication in your application, fill in this form.
+
+
+
+
+ {/* Enable Toggle Switch */}
+
+ Enable Provider
+
+ setModalForm(p => ({ ...p, enabled: e.target.checked }))}
+ disabled={isViewer}
+ />
+
+ {modalForm.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+ {/* Client ID */}
+
+ setModalForm(p => ({ ...p, clientId: e.target.value }))}
+ style={{ ...inputStyle, fontFamily: 'monospace' }}
+ disabled={isViewer}
+ />
+
+
+ {/* Client Secret */}
+
+
+ setModalForm(p => ({ ...p, clientSecret: e.target.value }))}
+ style={{ ...inputStyle, fontFamily: 'monospace', paddingRight: '36px' }}
+ disabled={isViewer}
+ />
+ setShowSecret(!showSecret)}
+ style={{ position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', padding: 0 }}
+ >
+ {showSecret ? : }
+
+
+
+
+ {/* Callout Box for Redirect URI */}
+
+
+
+ To complete set up, add this OAuth2 redirect URI to your {selectedProviderModal === 'github' ? 'GitHub app' : 'Google Cloud Console'} configuration.
+
+
+
+
+ copyToClipboard(activeCallbackUrl)}
+ style={{ height: '32px', padding: '0 10px', display: 'flex', alignItems: 'center', gap: '4px', flexShrink: 0 }}
+ >
+ {copiedUrl ? : }
+
+
+
+
+
+ {/* Modal Action Buttons */}
+
+ setSelectedProviderModal(null)}
+ style={{ height: '34px', fontSize: '0.78rem', padding: '0 14px' }}
+ >
+ Cancel
+
+
+ {isSavingProviders ? 'Updating...' : 'Update Settings'}
+
+
+
+
+ )}
+
);
}
diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx
index d1555a3dc..0827c1d11 100644
--- a/apps/web-dashboard/src/pages/Auth.jsx
+++ b/apps/web-dashboard/src/pages/Auth.jsx
@@ -2,12 +2,10 @@ import { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import api from '../utils/api';
import toast from 'react-hot-toast';
-import { Shield, X, AlertCircle, Monitor, LogOut, Key } from 'lucide-react';
+import { Shield, X, AlertCircle, Monitor, LogOut, Key, ExternalLink } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import AuthHeader from '../components/Auth/AuthHeader';
-import SocialAuthConfig from '../components/Auth/SocialAuthConfig';
-import SocialAuthModal from '../components/Auth/SocialAuthModal';
import UserTable from '../components/Auth/UserTable';
import Pagination from '../components/Database/Pagination';
import SectionHeader from '../components/Dashboard/SectionHeader';
@@ -52,17 +50,10 @@ export default function Auth() {
const [project, setProject] = useState(null);
const [isEnabling, setIsEnabling] = useState(false);
const [isTogglingSignup, setIsTogglingSignup] = useState(false);
- const [isSavingProviders, setIsSavingProviders] = useState(false);
- const [isSocialAuthModalOpen, setIsSocialAuthModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isEditUsersSchemaModalOpen, setIsEditUsersSchemaModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState(null); // user being edited
const latestUsersRequestId = useRef(0);
- const [selectedProvider, setSelectedProvider] = useState('github');
- const [authProviders, setAuthProviders] = useState({
- github: { enabled: false, clientId: '', clientSecret: '', hasClientSecret: false },
- google: { enabled: false, clientId: '', clientSecret: '', hasClientSecret: false }
- });
const [isSessionsModalOpen, setIsSessionsModalOpen] = useState(false);
const [sessionsTargetUser, setSessionsTargetUser] = useState(null);
@@ -74,11 +65,6 @@ export default function Auth() {
const [newPassword, setNewPassword] = useState('');
const [isResetLoading, setIsResetLoading] = useState(false);
- // Config
- const siteUrl = project?.siteUrl || '';
- const githubCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/github/callback`;
- const googleCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/google/callback`;
-
const myMember = project?.members?.find(m => {
const memberId = typeof m.user === 'object' ? m.user?._id : m.user;
return memberId?.toString() === user?._id?.toString() || m.email === user?.email;
@@ -113,7 +99,6 @@ export default function Auth() {
if (isMounted) {
setProject(projectResult.data);
- if (projectResult.data?.authProviders) setAuthProviders(projectResult.data.authProviders);
if (projectResult.data?.isAuthEnabled) {
const requestId = ++latestUsersRequestId.current;
const usersRes = await api.get(
@@ -158,36 +143,6 @@ export default function Auth() {
} finally { setIsEnabling(false); }
};
- const handleProviderFieldChange = (provider, field, value) => {
- setAuthProviders((prev) => ({
- ...prev,
- [provider]: { ...prev[provider], [field]: value }
- }));
- };
-
- const handleSaveProviders = async () => {
- setIsSavingProviders(true);
- try {
- const payload = {
- github: {
- enabled: !!authProviders.github.enabled,
- clientId: authProviders.github.clientId,
- ...(authProviders.github.clientSecret ? { clientSecret: authProviders.github.clientSecret } : {})
- },
- google: {
- enabled: !!authProviders.google.enabled,
- clientId: authProviders.google.clientId,
- ...(authProviders.google.clientSecret ? { clientSecret: authProviders.google.clientSecret } : {})
- }
- };
- const res = await api.patch(`/api/projects/${projectId}/auth/providers`, payload);
- setAuthProviders(res.data.authProviders);
- toast.success('Social auth saved');
- setIsSocialAuthModalOpen(false);
- } catch { toast.error('Failed to save settings'); }
- finally { setIsSavingProviders(false); }
- };
-
const handleTogglePublicSignup = async (enable) => {
if (isTogglingSignup || isViewer) return;
setIsTogglingSignup(true);
@@ -306,21 +261,6 @@ export default function Auth() {
onAddUser={isViewer ? null : () => setIsAddModalOpen(true)}
/>
- {/* Social Auth Modal */}
- setIsSocialAuthModalOpen(false)}
- selectedProvider={selectedProvider}
- setSelectedProvider={setSelectedProvider}
- authProviders={authProviders}
- handleProviderFieldChange={handleProviderFieldChange}
- handleSaveProviders={handleSaveProviders}
- isSavingProviders={isSavingProviders}
- siteUrl={siteUrl}
- githubCallbackUrl={githubCallbackUrl}
- googleCallbackUrl={googleCallbackUrl}
- />
-
{/* Add / Edit User Drawer */}
-
setIsSocialAuthModalOpen(true)}
- setSelectedProvider={setSelectedProvider}
- />
+
+
+
+
+ OAuth2 & Social Providers
+
+
+ Configure GitHub, Google, and third-party login integrations in Settings → Integrations & Services .
+
+
+
navigate(`/project/${projectId}/settings?tab=integrations`)}
+ style={{ height: '32px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' }}
+ >
+
+ Integrations
+
+
+
Security Policy