feat: Add OAuth email integration and production deployment config
- Add OAuthCredential model for storing Google/Microsoft OAuth tokens - Add email provider auto-detection endpoint (Gmail, Outlook, Yahoo, etc.) - Add EmailConfigWizard frontend component with step-by-step setup - Add OAuth flow endpoints for Google and Microsoft XOAUTH2 - Update production settings to make AWS, Sentry, Mailgun optional - Update Traefik config for wildcard subdomain routing - Add logo resize utility script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,25 @@ export interface FetchNowResult {
|
||||
processed: number;
|
||||
}
|
||||
|
||||
export interface EmailProviderDetectResult {
|
||||
success: boolean;
|
||||
email: string;
|
||||
domain: string;
|
||||
detected: boolean;
|
||||
detected_via?: 'domain_lookup' | 'mx_record';
|
||||
provider: 'google' | 'microsoft' | 'yahoo' | 'apple' | 'aol' | 'zoho' | 'protonmail' | 'unknown';
|
||||
display_name: string;
|
||||
imap_host?: string;
|
||||
imap_port?: number;
|
||||
smtp_host?: string;
|
||||
smtp_port?: number;
|
||||
oauth_supported: boolean;
|
||||
message?: string;
|
||||
notes?: string;
|
||||
suggested_imap_port?: number;
|
||||
suggested_smtp_port?: number;
|
||||
}
|
||||
|
||||
export interface IncomingTicketEmail {
|
||||
id: number;
|
||||
message_id: string;
|
||||
@@ -167,3 +186,77 @@ export const reprocessIncomingEmail = async (id: number): Promise<{
|
||||
const response = await apiClient.post(`/api/tickets/incoming-emails/${id}/reprocess/`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect email provider from email address
|
||||
* Auto-detects Gmail, Outlook, Yahoo, iCloud, etc. from domain
|
||||
* Also checks MX records for custom domains using Google Workspace or Microsoft 365
|
||||
*/
|
||||
export const detectEmailProvider = async (email: string): Promise<EmailProviderDetectResult> => {
|
||||
const response = await apiClient.post('/api/tickets/email-settings/detect/', { email });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// OAuth types and functions
|
||||
export interface OAuthStatusResult {
|
||||
google: { configured: boolean };
|
||||
microsoft: { configured: boolean };
|
||||
}
|
||||
|
||||
export interface OAuthInitiateResult {
|
||||
success: boolean;
|
||||
authorization_url?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OAuthCredential {
|
||||
id: number;
|
||||
provider: 'google' | 'microsoft';
|
||||
email: string;
|
||||
purpose: string;
|
||||
is_valid: boolean;
|
||||
is_expired: boolean;
|
||||
last_used_at: string | null;
|
||||
last_error: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth configuration status
|
||||
*/
|
||||
export const getOAuthStatus = async (): Promise<OAuthStatusResult> => {
|
||||
const response = await apiClient.get('/api/oauth/status/');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiate Google OAuth flow
|
||||
*/
|
||||
export const initiateGoogleOAuth = async (purpose: string = 'email'): Promise<OAuthInitiateResult> => {
|
||||
const response = await apiClient.post('/api/oauth/google/initiate/', { purpose });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiate Microsoft OAuth flow
|
||||
*/
|
||||
export const initiateMicrosoftOAuth = async (purpose: string = 'email'): Promise<OAuthInitiateResult> => {
|
||||
const response = await apiClient.post('/api/oauth/microsoft/initiate/', { purpose });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* List OAuth credentials
|
||||
*/
|
||||
export const getOAuthCredentials = async (): Promise<OAuthCredential[]> => {
|
||||
const response = await apiClient.get('/api/oauth/credentials/');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete OAuth credential
|
||||
*/
|
||||
export const deleteOAuthCredential = async (id: number): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.delete(`/api/oauth/credentials/${id}/`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
1004
frontend/src/components/EmailConfigWizard.tsx
Normal file
1004
frontend/src/components/EmailConfigWizard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,19 @@ import {
|
||||
fetchEmailsNow,
|
||||
getIncomingEmails,
|
||||
reprocessIncomingEmail,
|
||||
detectEmailProvider,
|
||||
getOAuthStatus,
|
||||
initiateGoogleOAuth,
|
||||
initiateMicrosoftOAuth,
|
||||
getOAuthCredentials,
|
||||
deleteOAuthCredential,
|
||||
TicketEmailSettings,
|
||||
TicketEmailSettingsUpdate,
|
||||
IncomingTicketEmail,
|
||||
EmailProviderDetectResult,
|
||||
OAuthStatusResult,
|
||||
OAuthInitiateResult,
|
||||
OAuthCredential,
|
||||
} from '../api/ticketEmailSettings';
|
||||
|
||||
const QUERY_KEY = 'ticketEmailSettings';
|
||||
@@ -103,4 +113,77 @@ export const useReprocessIncomingEmail = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export type { TicketEmailSettings, TicketEmailSettingsUpdate, IncomingTicketEmail };
|
||||
/**
|
||||
* Hook to detect email provider from email address
|
||||
*/
|
||||
export const useDetectEmailProvider = () => {
|
||||
return useMutation({
|
||||
mutationFn: (email: string) => detectEmailProvider(email),
|
||||
});
|
||||
};
|
||||
|
||||
// OAuth Hooks
|
||||
const OAUTH_STATUS_KEY = 'oauthStatus';
|
||||
const OAUTH_CREDENTIALS_KEY = 'oauthCredentials';
|
||||
|
||||
/**
|
||||
* Hook to get OAuth configuration status
|
||||
*/
|
||||
export const useOAuthStatus = () => {
|
||||
return useQuery<OAuthStatusResult>({
|
||||
queryKey: [OAUTH_STATUS_KEY],
|
||||
queryFn: getOAuthStatus,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to initiate Google OAuth flow
|
||||
*/
|
||||
export const useInitiateGoogleOAuth = () => {
|
||||
return useMutation({
|
||||
mutationFn: (purpose: string = 'email') => initiateGoogleOAuth(purpose),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to initiate Microsoft OAuth flow
|
||||
*/
|
||||
export const useInitiateMicrosoftOAuth = () => {
|
||||
return useMutation({
|
||||
mutationFn: (purpose: string = 'email') => initiateMicrosoftOAuth(purpose),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to list OAuth credentials
|
||||
*/
|
||||
export const useOAuthCredentials = () => {
|
||||
return useQuery<OAuthCredential[]>({
|
||||
queryKey: [OAUTH_CREDENTIALS_KEY],
|
||||
queryFn: getOAuthCredentials,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete OAuth credential
|
||||
*/
|
||||
export const useDeleteOAuthCredential = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteOAuthCredential(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [OAUTH_CREDENTIALS_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type {
|
||||
TicketEmailSettings,
|
||||
TicketEmailSettingsUpdate,
|
||||
IncomingTicketEmail,
|
||||
EmailProviderDetectResult,
|
||||
OAuthStatusResult,
|
||||
OAuthInitiateResult,
|
||||
OAuthCredential,
|
||||
};
|
||||
|
||||
@@ -55,7 +55,8 @@ import {
|
||||
useTestSmtpConnection,
|
||||
useFetchEmailsNow,
|
||||
} from '../../hooks/useTicketEmailSettings';
|
||||
import { Send } from 'lucide-react';
|
||||
import { Send, Wand2 } from 'lucide-react';
|
||||
import EmailConfigWizard from '../../components/EmailConfigWizard';
|
||||
|
||||
type TabType = 'general' | 'stripe' | 'tiers' | 'oauth';
|
||||
|
||||
@@ -119,12 +120,14 @@ const PlatformSettings: React.FC = () => {
|
||||
|
||||
const GeneralSettingsTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: emailSettings, isLoading, error } = useTicketEmailSettings();
|
||||
const { data: emailSettings, isLoading, error, refetch } = useTicketEmailSettings();
|
||||
const updateMutation = useUpdateTicketEmailSettings();
|
||||
const testImapMutation = useTestImapConnection();
|
||||
const testSmtpMutation = useTestSmtpConnection();
|
||||
const fetchNowMutation = useFetchEmailsNow();
|
||||
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
// IMAP settings
|
||||
imap_host: '',
|
||||
@@ -228,14 +231,39 @@ const GeneralSettingsTab: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Show wizard if requested
|
||||
if (showWizard) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<EmailConfigWizard
|
||||
onComplete={() => {
|
||||
setShowWizard(false);
|
||||
refetch();
|
||||
}}
|
||||
onCancel={() => setShowWizard(false)}
|
||||
initialEmail={emailSettings?.imap_username || ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Email Processing Status */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Mail className="w-5 h-5" />
|
||||
{t('platform.settings.emailProcessing', 'Support Email Processing')}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Mail className="w-5 h-5" />
|
||||
{t('platform.settings.emailProcessing', 'Support Email Processing')}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowWizard(true)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
|
||||
>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
{t('platform.settings.setupWizard', 'Setup Wizard')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user