mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 15:51:13 -05:00
feat: Implement OAuth profile enhancement
This commit is contained in:
230
supabase/functions/process-oauth-profile/index.ts
Normal file
230
supabase/functions/process-oauth-profile/index.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
const CLOUDFLARE_ACCOUNT_ID = Deno.env.get('CLOUDFLARE_ACCOUNT_ID');
|
||||
const CLOUDFLARE_API_TOKEN = Deno.env.get('CLOUDFLARE_IMAGES_API_TOKEN');
|
||||
|
||||
interface GoogleUserMetadata {
|
||||
email?: string;
|
||||
name?: string;
|
||||
picture?: string;
|
||||
email_verified?: boolean;
|
||||
}
|
||||
|
||||
interface DiscordUserMetadata {
|
||||
email?: string;
|
||||
username?: string;
|
||||
global_name?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Verify JWT and get user
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser(token);
|
||||
|
||||
if (authError || !user) {
|
||||
console.error('[OAuth Profile] Authentication failed:', authError);
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[OAuth Profile] Processing profile for user:', user.id);
|
||||
|
||||
const provider = user.app_metadata?.provider;
|
||||
const userMetadata = user.user_metadata;
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
let displayName: string | null = null;
|
||||
let usernameBase: string | null = null;
|
||||
|
||||
// Extract provider-specific data
|
||||
if (provider === 'google') {
|
||||
const googleData = userMetadata as GoogleUserMetadata;
|
||||
avatarUrl = googleData.picture || null;
|
||||
displayName = googleData.name || null;
|
||||
usernameBase = googleData.email?.split('@')[0] || null;
|
||||
console.log('[OAuth Profile] Google user:', { avatarUrl, displayName, usernameBase });
|
||||
} else if (provider === 'discord') {
|
||||
const discordData = userMetadata as DiscordUserMetadata;
|
||||
displayName = discordData.global_name || discordData.username || null;
|
||||
usernameBase = discordData.username || null;
|
||||
|
||||
// Construct Discord avatar URL
|
||||
if (discordData.avatar && discordData.id) {
|
||||
avatarUrl = `https://cdn.discordapp.com/avatars/${discordData.id}/${discordData.avatar}.png?size=512`;
|
||||
}
|
||||
console.log('[OAuth Profile] Discord user:', { avatarUrl, displayName, usernameBase });
|
||||
} else {
|
||||
console.log('[OAuth Profile] Unsupported provider:', provider);
|
||||
return new Response(JSON.stringify({ success: true, message: 'Provider not supported' }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Check if profile already has avatar
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('avatar_image_id, username')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (profile?.avatar_image_id) {
|
||||
console.log('[OAuth Profile] Avatar already exists, skipping');
|
||||
return new Response(JSON.stringify({ success: true, message: 'Avatar already exists' }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
let cloudflareImageId: string | null = null;
|
||||
let cloudflareImageUrl: string | null = null;
|
||||
|
||||
// Download and upload avatar to Cloudflare
|
||||
if (avatarUrl) {
|
||||
try {
|
||||
console.log('[OAuth Profile] Downloading avatar from:', avatarUrl);
|
||||
|
||||
// Download image with timeout
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
|
||||
const imageResponse = await fetch(avatarUrl, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error(`Failed to download avatar: ${imageResponse.statusText}`);
|
||||
}
|
||||
|
||||
const imageBlob = await imageResponse.blob();
|
||||
|
||||
// Validate image size (max 10MB)
|
||||
if (imageBlob.size > 10 * 1024 * 1024) {
|
||||
throw new Error('Image too large (max 10MB)');
|
||||
}
|
||||
|
||||
console.log('[OAuth Profile] Downloaded image:', {
|
||||
size: imageBlob.size,
|
||||
type: imageBlob.type,
|
||||
});
|
||||
|
||||
// Get upload URL from Cloudflare
|
||||
const uploadUrlResponse = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadUrlResponse.ok) {
|
||||
throw new Error('Failed to get Cloudflare upload URL');
|
||||
}
|
||||
|
||||
const uploadData = await uploadUrlResponse.json();
|
||||
const uploadURL = uploadData.result.uploadURL;
|
||||
|
||||
console.log('[OAuth Profile] Got Cloudflare upload URL');
|
||||
|
||||
// Upload to Cloudflare
|
||||
const formData = new FormData();
|
||||
formData.append('file', imageBlob, 'avatar.png');
|
||||
|
||||
const uploadResponse = await fetch(uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error('Failed to upload to Cloudflare');
|
||||
}
|
||||
|
||||
const result = await uploadResponse.json();
|
||||
|
||||
if (result.success) {
|
||||
cloudflareImageId = result.result.id;
|
||||
cloudflareImageUrl = `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_ID}/${cloudflareImageId}/avatar`;
|
||||
console.log('[OAuth Profile] Uploaded to Cloudflare:', { cloudflareImageId, cloudflareImageUrl });
|
||||
} else {
|
||||
throw new Error('Cloudflare upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OAuth Profile] Avatar upload failed:', error);
|
||||
// Continue without avatar - don't block profile creation
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile with enhanced data
|
||||
const updateData: any = {};
|
||||
|
||||
if (cloudflareImageId) {
|
||||
updateData.avatar_image_id = cloudflareImageId;
|
||||
updateData.avatar_url = cloudflareImageUrl;
|
||||
}
|
||||
|
||||
if (displayName) {
|
||||
updateData.display_name = displayName;
|
||||
}
|
||||
|
||||
// Only update if we have data to update
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update(updateData)
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (updateError) {
|
||||
console.error('[OAuth Profile] Failed to update profile:', updateError);
|
||||
return new Response(JSON.stringify({ error: 'Failed to update profile' }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[OAuth Profile] Profile updated successfully');
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
avatar_uploaded: !!cloudflareImageId,
|
||||
profile_updated: Object.keys(updateData).length > 0,
|
||||
}), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[OAuth Profile] Error:', error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user