diff --git a/backend/.env.example b/backend/.env.example index 0f9603f5..81cc84bc 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,19 @@ - +# Environment NODE_ENV=development -MONGODB_URI= -JWT_SECRET= -STARKNET_MAINNET_RPC_API_URL= -STARKNET_SEPOLIA_RPC_API_URL= \ No newline at end of file + +# Database +MONGODB_URI=mongodb://localhost:27017/musicstrk + +# JWT Secret (generate a secure random string) +JWT_SECRET=your_super_secret_jwt_key_here_make_it_long_and_random + +# Starknet RPC URLs +STARKNET_MAINNET_RPC_API_URL=https://starknet-mainnet.public.blastapi.io +STARKNET_SEPOLIA_RPC_API_URL=https://starknet-sepolia.public.blastapi.io + +# TikTok OAuth +TIKTOK_CLIENT_ID=your_tiktok_client_id_here +TIKTOK_CLIENT_SECRET=your_tiktok_client_secret_here + +# Frontend URL +FRONTEND_URL=http://localhost:3000 \ No newline at end of file diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 5aa273b8..a6a555bf 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,5 +1,6 @@ import { Router, Request } from "express"; import { BigNumberish, constants, ec, Provider } from "starknet"; +import TikTokAuthRoutes from "./auth/tiktok"; import UserModel, { createUser, findUserByaddress } from "models/UserModel"; import { AUTHENTICATION_SNIP12_MESSAGE } from "constants/index"; @@ -24,6 +25,8 @@ type ReqBody_Authenticate = { signature: string[]; }; +AuthRoutes.use("/tiktok", TikTokAuthRoutes) + AuthRoutes.post( "/authenticate", async (req: Request<{}, {}, ReqBody_Authenticate>, res: any) => { diff --git a/backend/src/routes/v1/auth/tiktok.ts b/backend/src/routes/v1/auth/tiktok.ts new file mode 100644 index 00000000..34827c20 --- /dev/null +++ b/backend/src/routes/v1/auth/tiktok.ts @@ -0,0 +1,108 @@ +import { Router, Request, Response } from "express"; + +const TikTokAuthRoutes = Router(); + +interface TikTokTokenResponse { + access_token: string; + open_id: string; + scope: string; + expires_in: number; +} + +interface TikTokUserInfo { + open_id: string; + username: string; + display_name: string; + avatar_url: string; +} + +// Exchange authorization code for access token +TikTokAuthRoutes.post("/token", async (req: Request, res: Response): Promise => { + try { + const { code } = req.body; + + if (!code) { + res.status(400).json({ success: false, error: "Authorization code is required" }); + return; + } + + const clientId = process.env.TIKTOK_CLIENT_ID; + const clientSecret = process.env.TIKTOK_CLIENT_SECRET; + const redirectUri = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/auth/tiktok/callback`; + + if (!clientId || !clientSecret) { + res.status(500).json({ success: false, error: "TikTok configuration missing" }); + return; + } + + console.log("Exchanging TikTok authorization code for token..."); + + // Exchange code for access token + const tokenResponse = await fetch("https://open-api.tiktok.com/oauth/access_token/", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_key: clientId, + client_secret: clientSecret, + code: code, + grant_type: "authorization_code", + redirect_uri: redirectUri, + }), + }); + + const tokenData = await tokenResponse.json() as any; + + if (!tokenResponse.ok || tokenData.error) { + console.error("Token exchange failed:", tokenData); + res.status(400).json({ + success: false, + error: tokenData.error_description || "Failed to exchange authorization code" + }); + return; + } + + const { access_token, open_id } = tokenData.data as TikTokTokenResponse; + + console.log("Token exchange successful, fetching user info..."); + + // Get user information + const userResponse = await fetch(`https://open-api.tiktok.com/user/info/?access_token=${access_token}&open_id=${open_id}`); + const userData = await userResponse.json() as any; + + if (!userResponse.ok || userData.error) { + console.error("User info fetch failed:", userData); + res.status(400).json({ + success: false, + error: "Failed to fetch user information" + }); + return; + } + + const userInfo = userData.data.user; + + const authResult = { + accessToken: access_token, + openId: open_id, + userInfo: { + openId: open_id, + username: userInfo.username, + displayName: userInfo.display_name, + avatarUrl: userInfo.avatar_url, + }, + }; + + console.log("TikTok authentication successful for user:", userInfo.username); + + res.json({ success: true, authResult }); + } catch (error) { + console.error("TikTok token exchange error:", error); + res.status(500).json({ + success: false, + error: "Internal server error during token exchange" + }); + } +}); + +export default TikTokAuthRoutes; \ No newline at end of file diff --git a/dashboard/src/components/layout/data/sidebar-data.ts b/dashboard/src/components/layout/data/sidebar-data.ts index 3c91f19d..8345a90f 100644 --- a/dashboard/src/components/layout/data/sidebar-data.ts +++ b/dashboard/src/components/layout/data/sidebar-data.ts @@ -1,24 +1,13 @@ import { - IconBarrierBlock, IconBrowserCheck, - IconBug, - IconChecklist, - IconError404, IconHelp, IconLayoutDashboard, - IconLock, - IconLockAccess, IconMessages, IconNotification, - IconPackages, IconPalette, - IconServerOff, IconSettings, IconTool, IconUserCog, - IconUserOff, - IconUsers, - IconUser, } from '@tabler/icons-react' import { AudioWaveform, diff --git a/dashboard/src/components/layout/nav-group.tsx b/dashboard/src/components/layout/nav-group.tsx index ae98a3e8..df489d73 100644 --- a/dashboard/src/components/layout/nav-group.tsx +++ b/dashboard/src/components/layout/nav-group.tsx @@ -8,7 +8,6 @@ import { } from '@/components/ui/collapsible' import { SidebarGroup, - SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, diff --git a/dashboard/src/components/layout/types.ts b/dashboard/src/components/layout/types.ts index bb078336..e19cae08 100644 --- a/dashboard/src/components/layout/types.ts +++ b/dashboard/src/components/layout/types.ts @@ -1,5 +1,3 @@ -import { LinkProps } from '@tanstack/react-router' - interface User { name: string email: string @@ -19,12 +17,12 @@ interface BaseNavItem { } type NavLink = BaseNavItem & { - url: LinkProps['to'] + url: string items?: never } type NavCollapsible = BaseNavItem & { - items: (BaseNavItem & { url: LinkProps['to'] })[] + items: (BaseNavItem & { url: string })[] url?: never } diff --git a/dashboard/src/context/auth-context.tsx b/dashboard/src/context/auth-context.tsx index 917323e9..10b84a17 100644 --- a/dashboard/src/context/auth-context.tsx +++ b/dashboard/src/context/auth-context.tsx @@ -3,6 +3,7 @@ import { useContext, useEffect, useState, + useCallback, ReactNode, } from 'react' import { useAccount, useConnect, useDisconnect } from '@starknet-react/core' @@ -43,23 +44,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [isConnected, status]) - // Handle the completion or failure of a wallet connection - useEffect(() => { - if (connectingWallet) { - if (status === 'connected' && account && address) { - // Proceed with authentication - completeAuthentication() - } else if (status === 'disconnected' && !isLoading) { - // Connection failed or cancelled - console.log('Connection failed or cancelled') - setConnectingWallet(false) - setError('Wallet connection was cancelled or failed') - setIsLoading(false) - } - } - }, [status, connectingWallet, account, address]) - - const completeAuthentication = async () => { + const completeAuthentication = useCallback(async () => { try { setConnectingWallet(false) if (isConnected && account && address) { @@ -72,14 +57,29 @@ export function AuthProvider({ children }: { children: ReactNode }) { setError(null) } } catch (err) { - console.error('Authentication error:', err) + // Removed console.error for lint compliance const errorMessage = err instanceof Error ? err.message : 'Authentication failed' setError(errorMessage) } finally { setIsLoading(false) } - } + }, [isConnected, account, address]) + + // Handle the completion or failure of a wallet connection + useEffect(() => { + if (connectingWallet) { + if (status === 'connected' && account && address) { + // Proceed with authentication + completeAuthentication() + } else if (status === 'disconnected' && !isLoading) { + // Connection failed or cancelled + setConnectingWallet(false) + setError('Wallet connection was cancelled or failed') + setIsLoading(false) + } + } + }, [status, connectingWallet, account, address, completeAuthentication, isLoading]) const signIn = async (connectorId: string) => { try { @@ -91,7 +91,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { throw new Error('Wallet not found') } - if (typeof window === 'undefined' || !(window as any).starknet) { + if (typeof window === 'undefined' || !(window as { starknet?: unknown }).starknet) { throw new Error( 'Wallet provider not detected. Please install Argent X or Braavos.' ) @@ -101,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setConnectingWallet(true) connect({ connector }) } catch (err) { - console.error('Connection error:', err) + // Removed console.error for lint compliance setConnectingWallet(false) setIsLoading(false) const errorMessage = @@ -118,7 +118,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsAuthenticated(false) setError(null) } catch (err) { - console.error('Sign out error:', err) + // Removed console.error for lint compliance const errorMessage = err instanceof Error ? err.message : 'Failed to sign out' setError(errorMessage) diff --git a/dashboard/src/features/dashboard/components/wallet-modal.tsx b/dashboard/src/features/dashboard/components/wallet-modal.tsx index 72bdcad3..5933a983 100644 --- a/dashboard/src/features/dashboard/components/wallet-modal.tsx +++ b/dashboard/src/features/dashboard/components/wallet-modal.tsx @@ -52,7 +52,6 @@ export function WalletModal({ isOpen, onClose }: WalletModalProps) { setError(null); await signIn(connectorId); } catch (err) { - console.error('Sign in error caught in modal:', err); const errorMessage = err instanceof Error && err.message === 'Wallet connection was cancelled' ? 'Connection cancelled. Please try again.' diff --git a/dashboard/src/features/dashboard/index.tsx b/dashboard/src/features/dashboard/index.tsx index 61f4e432..e5b516a7 100644 --- a/dashboard/src/features/dashboard/index.tsx +++ b/dashboard/src/features/dashboard/index.tsx @@ -7,7 +7,7 @@ import { ProfileDropdown } from '@/components/profile-dropdown' import { ThemeSwitch } from '@/components/theme-switch' import { ConnectWalletButton } from './components/connect-wallet-button' import { WalletModal } from './components/wallet-modal' -import { Check, ChevronLeft, Copy, RotateCcw } from "lucide-react"; +import { Check, Copy } from "lucide-react"; import { useAccount } from '@starknet-react/core' function CopyAddressButton() { diff --git a/webapp/.env.example b/webapp/.env.example new file mode 100644 index 00000000..d7405df9 --- /dev/null +++ b/webapp/.env.example @@ -0,0 +1,11 @@ +# TikTok OAuth Configuration +NEXT_PUBLIC_TIKTOK_CLIENT_ID=your_tiktok_client_id_here +NEXT_PUBLIC_TIKTOK_REDIRECT_URI=http://localhost:3000/auth/tiktok/callback + +# Server-side TikTok Config (for API routes) +TIKTOK_CLIENT_ID=your_tiktok_client_id_here +TIKTOK_CLIENT_SECRET=your_tiktok_client_secret_here + +# Backend URLs +BACKEND_URL=http://localhost:8080 +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 \ No newline at end of file diff --git a/webapp/.gitignore b/webapp/.gitignore index fd3dbb57..a84107af 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -26,6 +26,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env*.local # vercel diff --git a/webapp/app/auth/tiktok/callback/page.tsx b/webapp/app/auth/tiktok/callback/page.tsx new file mode 100644 index 00000000..69589d96 --- /dev/null +++ b/webapp/app/auth/tiktok/callback/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useState, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; + +function TikTokCallbackInner() { + const searchParams = useSearchParams(); + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const [message, setMessage] = useState('Completing TikTok authentication...'); + + useEffect(() => { + const handleCallback = async () => { + try { + const code = searchParams?.get('code'); + const state = searchParams?.get('state'); + const error = searchParams?.get('error'); + const storedState = localStorage.getItem('tiktok_auth_state'); + + if (error) { + throw new Error(`TikTok authentication error: ${error}`); + } + + if (!code || !state || state !== storedState) { + throw new Error('Invalid authentication response'); + } + + setMessage('Exchanging code for access token...'); + + // Exchange code for access token using your backend (FIXED URL) + const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'}/api/v1/auth/tiktok/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }), + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || 'Token exchange failed'); + } + + // Store result and close popup + localStorage.setItem('tiktok_auth_result', JSON.stringify(data.authResult)); + localStorage.removeItem('tiktok_auth_state'); + + setStatus('success'); + setMessage('Authentication successful! Closing window...'); + + setTimeout(() => { + window.close(); + }, 2000); + + } catch (error) { + console.error('TikTok callback error:', error); + const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; + + localStorage.setItem('tiktok_auth_error', errorMessage); + localStorage.removeItem('tiktok_auth_state'); + + setStatus('error'); + setMessage(errorMessage); + + setTimeout(() => { + window.close(); + }, 3000); + } + }; + + handleCallback(); + }, [searchParams]); + + return ( +
+
+ {status === 'loading' && ( +
+ )} + + {status === 'success' && ( +
+ + + +
+ )} + + {status === 'error' && ( +
+ + + +
+ )} + +

+ {message} +

+
+
+ ); +} + +export default function TikTokCallback() { + return ( + + + + ); +} \ No newline at end of file diff --git a/webapp/components/Hero.tsx b/webapp/components/Hero.tsx index a43c612c..9365bb9b 100644 --- a/webapp/components/Hero.tsx +++ b/webapp/components/Hero.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import Image from "next/image" import Link from "next/link" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, useMemo } from "react" import { createRoot } from "react-dom/client" import GitHubButton from "react-github-btn" import { FileText, Mail, Zap, Music, Headphones, Disc, Guitar, Mic, type LucideIcon } from "lucide-react" @@ -28,13 +28,13 @@ export default function HeroSection() { const particlesRef = useRef(null) // Musical icons configuration with synthwave colors - const musicalIcons: MusicalIconEntry[] = [ + const musicalIcons: MusicalIconEntry[] = useMemo(() => [ { Icon: Music, color: "text-[#00f5d4]" }, { Icon: Headphones, color: "text-[#ff6b6b]" }, { Icon: Disc, color: "text-[#00f5d4]" }, { Icon: Guitar, color: "text-[#ff6b6b]" }, { Icon: Mic, color: "text-[#00f5d4]" }, - ] + ], []) // Enhanced email validation const validateEmail = (email: string): boolean => { @@ -43,47 +43,47 @@ export default function HeroSection() { } // Dynamic musical icon animation effect - useEffect(() => { - if (typeof window !== "undefined" && particlesRef.current) { - const createMusicalIcon = () => { - const iconContainer = document.createElement("div") - const { Icon, color } = musicalIcons[Math.floor(Math.random() * musicalIcons.length)] + useEffect(() => { + if (typeof window !== "undefined" && particlesRef.current) { + const createMusicalIcon = () => { + const iconContainer = document.createElement("div") + const { Icon, color } = musicalIcons[Math.floor(Math.random() * musicalIcons.length)] - // Configure icon container with dynamic properties - iconContainer.classList.add( - "absolute", - "musical-icon", - "animate-musical-fall", - "opacity-50", - "hover:opacity-100", - "transition-opacity", - "pointer-events-none", - color, - ) + // Configure icon container with dynamic properties + iconContainer.classList.add( + "absolute", + "musical-icon", + "animate-musical-fall", + "opacity-50", + "hover:opacity-100", + "transition-opacity", + "pointer-events-none", + color, + ) - // Randomize position and animation - iconContainer.style.left = `${Math.random() * 100}%` - iconContainer.style.fontSize = `${Math.random() * 2 + 1}rem` - iconContainer.style.animationDuration = `${Math.random() * 10 + 5}s` + // Randomize position and animation + iconContainer.style.left = `${Math.random() * 100}%` + iconContainer.style.fontSize = `${Math.random() * 2 + 1}rem` + iconContainer.style.animationDuration = `${Math.random() * 10 + 5}s` - // Create React wrapper for icon - const iconWrapper = document.createElement("div") - const root = createRoot(iconWrapper) - root.render() + // Create React wrapper for icon + const iconWrapper = document.createElement("div") + const root = createRoot(iconWrapper) + root.render() - iconContainer.appendChild(iconWrapper) - particlesRef.current?.appendChild(iconContainer) + iconContainer.appendChild(iconWrapper) + particlesRef.current?.appendChild(iconContainer) - // Clean up after animation - setTimeout(() => { - iconContainer.remove() - }, 15000) - } + // Clean up after animation + setTimeout(() => { + iconContainer.remove() + }, 15000) + } - const iconInterval = setInterval(createMusicalIcon, 1000) - return () => clearInterval(iconInterval) - } - }, []) + const iconInterval = setInterval(createMusicalIcon, 1000) + return () => clearInterval(iconInterval) + } + }, [musicalIcons]) // Handle waitlist form submission const handleSubmit = async () => { diff --git a/webapp/components/PerformerRegistration/FormSteps.tsx b/webapp/components/PerformerRegistration/FormSteps.tsx index c812c445..fe749111 100644 --- a/webapp/components/PerformerRegistration/FormSteps.tsx +++ b/webapp/components/PerformerRegistration/FormSteps.tsx @@ -6,19 +6,22 @@ import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { FormData, FormErrors } from "@/hooks/usePerformerForm" import { STEPS, MUSIC_GENRES } from "@/constants/formConstants" +import { TikTokAuthStep } from "./TikTokAuthStep" +import type { TikTokAuthResult } from "@/hooks/useTikTokAuth" interface FormStepProps { - currentStep: number - formData: FormData - errors: FormErrors - handleChange: (e: React.ChangeEvent) => void - handleSelectChange: (name: string, value: string) => void - handleNext: () => void - handlePrevious: () => void - handleSubmit: () => void - isSubmitting: boolean - isConnecting: boolean - connectWallet: () => Promise + currentStep: number; + formData: FormData; + errors: FormErrors; + handleChange: (e: React.ChangeEvent) => void; + handleSelectChange: (name: string, value: string) => void; + handleTikTokAuthSuccess: (authData: TikTokAuthResult) => void; + handleNext: () => void; + handlePrevious: () => void; + handleSubmit: () => void; + isSubmitting: boolean; + isConnecting: boolean; + connectWallet: () => Promise; } export function FormStepContent({ @@ -27,6 +30,7 @@ export function FormStepContent({ errors, handleChange, handleSelectChange, + handleTikTokAuthSuccess, handleNext, handlePrevious, handleSubmit, @@ -43,6 +47,11 @@ export function FormStepContent({ handleChange={handleChange} handleSelectChange={handleSelectChange} /> + case STEPS.TIKTOK_AUTH: // Added + return case STEPS.SOCIAL_MEDIA: return { - if (currentStep === STEPS.SUCCESS) return null + if (currentStep === STEPS.SUCCESS || currentStep === STEPS.TIKTOK_AUTH) return null return (
@@ -148,6 +157,25 @@ function BasicInfoStep({ {errors.stageName &&

{errors.stageName}

}
+
+ + + {errors.email &&

{errors.email}

} +

We'll use this to contact you about your audition

+
+
diff --git a/webapp/components/PerformerRegistration/PerformerRegistrationForm.tsx b/webapp/components/PerformerRegistration/PerformerRegistrationForm.tsx index c66b6dcf..c2678fe1 100644 --- a/webapp/components/PerformerRegistration/PerformerRegistrationForm.tsx +++ b/webapp/components/PerformerRegistration/PerformerRegistrationForm.tsx @@ -23,6 +23,7 @@ export default function PerformerRegistrationForm() { isSubmitting, handleChange, handleSelectChange, + handleTikTokAuthSuccess, handleNext, handlePrevious, handleSubmit, @@ -45,7 +46,7 @@ export default function PerformerRegistrationForm() { - + @@ -56,6 +57,7 @@ export default function PerformerRegistrationForm() { errors={errors} handleChange={handleChange} handleSelectChange={handleSelectChange} + handleTikTokAuthSuccess={handleTikTokAuthSuccess} // Added handleNext={handleNext} handlePrevious={handlePrevious} handleSubmit={handleSubmit} diff --git a/webapp/components/PerformerRegistration/TikTokAuthStep.tsx b/webapp/components/PerformerRegistration/TikTokAuthStep.tsx new file mode 100644 index 00000000..d97d4583 --- /dev/null +++ b/webapp/components/PerformerRegistration/TikTokAuthStep.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Music, CheckCircle, AlertCircle, ExternalLink } from "lucide-react"; +import { useTikTokAuth } from "@/hooks/useTikTokAuth"; +import type { TikTokAuthResult } from "@/hooks/useTikTokAuth"; +import Image from "next/image"; + +interface TikTokAuthStepProps { + onAuthSuccess: (authData: TikTokAuthResult) => void; + onNext: () => void; + // Removed unused isAuthenticated prop +} + +export function TikTokAuthStep({ onAuthSuccess, onNext }: Omit) { + const { isAuthenticating, authResult, error, authenticateWithTikTok, reset } = useTikTokAuth(); + + const handleTikTokAuth = async () => { + const result = await authenticateWithTikTok(); + if (result) { + onAuthSuccess(result); + } + }; + + return ( +
+
+
+ +
+ +
+
+ +

Verify Your TikTok Identity

+

+ Connect your TikTok account to prevent impersonation and verify your audition video. + This ensures authenticity in our audition process. +

+ + {error && ( +
+ +

{error}

+ +
+ )} + + {!authResult && !error && ( +
+ + +

+ A secure popup will open to authenticate with TikTok +

+
+ )} + + {authResult && ( +
+
+ +

TikTok Account Verified!

+ +
+
+ TikTok Avatar +
+

{authResult.userInfo.displayName}

+

@{authResult.userInfo.username}

+
+
+
+ +
+

+ ✓ Identity verified • ✓ Ready for registration +

+
+
+ + +
+ )} + +
+

+ + Why TikTok Verification? +

+
    +
  • • Prevents fake registrations and impersonation
  • +
  • • Ensures audition videos belong to the registrant
  • +
  • • Maintains fair competition for all artists
  • +
  • • Protects the integrity of our platform
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/webapp/constants/formConstants.ts b/webapp/constants/formConstants.ts index 05668c2b..85913af7 100644 --- a/webapp/constants/formConstants.ts +++ b/webapp/constants/formConstants.ts @@ -25,10 +25,11 @@ export const MUSIC_GENRES = [ ] export const STEPS = { - BASIC_INFO: 0, - SOCIAL_MEDIA: 1, - WALLET_CONNECTION: 2, - SUCCESS: 3, + BASIC_INFO: 1, + TIKTOK_AUTH: 2, + SOCIAL_MEDIA: 3, + WALLET_CONNECTION: 4, + SUCCESS: 5, } export const STEP_CONFIGS = { @@ -38,6 +39,12 @@ export const MUSIC_GENRES = [ icon: "Mic", iconColor: "#00f5d4", }, + [STEPS.TIKTOK_AUTH]: { + title: "TikTok Verification", + description: "Verify your TikTok identity to prevent impersonation", + icon: "Music", + iconColor: "#ff6b6b", + }, [STEPS.SOCIAL_MEDIA]: { title: "Social Media Links", description: "Share your social media presence", diff --git a/webapp/hooks/usePerformerForm.ts b/webapp/hooks/usePerformerForm.ts index 9c26d300..c0661a9b 100644 --- a/webapp/hooks/usePerformerForm.ts +++ b/webapp/hooks/usePerformerForm.ts @@ -5,15 +5,18 @@ import { STEPS } from "@/constants/formConstants" export interface FormData { stageName: string genre: string + email: string tiktokAuditionUrl: string tiktokProfileUrl: string socialX: string walletAddress: string + tiktokAuthData?: any } export interface FormErrors { stageName: string genre: string + email: string tiktokAuditionUrl: string tiktokProfileUrl: string socialX: string @@ -28,16 +31,19 @@ export function usePerformerForm() { const [formData, setFormData] = useState({ stageName: "", genre: "", + email: "", tiktokAuditionUrl: "", tiktokProfileUrl: "", socialX: "", walletAddress: "", + tiktokAuthData: null, }) // Form validation state const [errors, setErrors] = useState({ stageName: "", genre: "", + email: "", tiktokAuditionUrl: "", tiktokProfileUrl: "", socialX: "", @@ -64,6 +70,15 @@ export function usePerformerForm() { } } + // Handle TikTok auth success + const handleTikTokAuthSuccess = (authData: any) => { + setFormData((prev) => ({ + ...prev, + tiktokAuthData: authData, + tiktokProfileUrl: `https://www.tiktok.com/@${authData.userInfo.username}` + })) + } + // Validate current step const validateStep = () => { let isValid = true @@ -79,6 +94,27 @@ export function usePerformerForm() { newErrors.genre = "Please select a genre" isValid = false } + + // Email validation + if (!formData.email.trim()) { + newErrors.email = "Email is required" + isValid = false + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "Please enter a valid email address" + isValid = false + } + } + + if (currentStep === STEPS.TIKTOK_AUTH) { + // TikTok auth validation + if (!formData.tiktokAuthData) { + toast({ + variant: "destructive", + title: "TikTok Authentication Required", + description: "Please authenticate with your TikTok account to continue", + }) + isValid = false + } } if (currentStep === STEPS.SOCIAL_MEDIA) { @@ -98,6 +134,15 @@ export function usePerformerForm() { isValid = false } + // Verify TikTok profile matches authenticated account + if (formData.tiktokAuthData && formData.tiktokProfileUrl) { + const expectedProfileUrl = `https://www.tiktok.com/@${formData.tiktokAuthData.userInfo.username}` + if (formData.tiktokProfileUrl !== expectedProfileUrl) { + newErrors.tiktokProfileUrl = "Profile URL must match your authenticated TikTok account" + isValid = false + } + } + if (formData.socialX && !formData.socialX.includes("twitter.com/") && !formData.socialX.includes("x.com/")) { newErrors.socialX = "Please enter a valid Twitter/X profile URL" isValid = false @@ -132,16 +177,53 @@ export function usePerformerForm() { setIsSubmitting(true) - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 2000)) + try { + // Prepare submission data + const submissionData = { + stageName: formData.stageName, + genre: formData.genre, + email: formData.email, + tiktokAuditionUrl: formData.tiktokAuditionUrl, + tiktokProfileUrl: formData.tiktokProfileUrl, + socialX: formData.socialX, + walletAddress: formData.walletAddress, + tiktokAuthData: formData.tiktokAuthData, + } - setIsSubmitting(false) - setCurrentStep(STEPS.SUCCESS) + // Get auth token (you'll need to implement this based on your auth system) + const token = localStorage.getItem('auth_token') || 'dummy_token_for_testing' - toast({ - title: "Registration Complete! 🚀", - description: "Your audition has been submitted successfully.", - }) + const response = await fetch('/api/performers/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(submissionData), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || 'Registration failed') + } + + setCurrentStep(STEPS.SUCCESS) + + toast({ + title: "Registration Complete! 🚀", + description: "Your audition has been submitted successfully.", + }) + } catch (error) { + console.error('Registration error:', error) + toast({ + variant: "destructive", + title: "Registration Failed", + description: error instanceof Error ? error.message : "There was an error submitting your registration. Please try again.", + }) + } finally { + setIsSubmitting(false) + } } return { @@ -153,6 +235,7 @@ export function usePerformerForm() { isSubmitting, handleChange, handleSelectChange, + handleTikTokAuthSuccess, handleNext, handlePrevious, handleSubmit, diff --git a/webapp/hooks/useTikTokAuth.ts b/webapp/hooks/useTikTokAuth.ts new file mode 100644 index 00000000..c9c4563d --- /dev/null +++ b/webapp/hooks/useTikTokAuth.ts @@ -0,0 +1,114 @@ +import { useState, useCallback } from 'react'; + +export interface TikTokAuthResult { + accessToken: string; + openId: string; + userInfo: { + openId: string; + username: string; + displayName: string; + avatarUrl: string; + }; +} + +export const useTikTokAuth = () => { + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [authResult, setAuthResult] = useState(null); + const [error, setError] = useState(null); + + const authenticateWithTikTok = useCallback(async (): Promise => { + setIsAuthenticating(true); + setError(null); + + try { + const clientId = process.env.NEXT_PUBLIC_TIKTOK_CLIENT_ID; + const redirectUri = process.env.NEXT_PUBLIC_TIKTOK_REDIRECT_URI; + + if (!clientId || !redirectUri) { + throw new Error('TikTok configuration missing'); + } + + const state = Math.random().toString(36).substring(7); + const scope = 'user.info.basic'; + + // Store state for verification + localStorage.setItem('tiktok_auth_state', state); + + const authUrl = `https://www.tiktok.com/auth/authorize/?client_key=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&scope=${scope}`; + + // Open popup for OAuth + const popup = window.open( + authUrl, + 'tiktok-auth', + 'width=500,height=600,scrollbars=yes,resizable=yes' + ); + + if (!popup) { + throw new Error('Popup blocked. Please allow popups for this site.'); + } + + return new Promise((resolve, reject) => { + const checkClosed = setInterval(() => { + if (popup.closed) { + clearInterval(checkClosed); + const result = localStorage.getItem('tiktok_auth_result'); + const authError = localStorage.getItem('tiktok_auth_error'); + + if (result) { + const parsedResult = JSON.parse(result); + localStorage.removeItem('tiktok_auth_result'); + localStorage.removeItem('tiktok_auth_state'); + setAuthResult(parsedResult); + resolve(parsedResult); + } else if (authError) { + localStorage.removeItem('tiktok_auth_error'); + localStorage.removeItem('tiktok_auth_state'); + reject(new Error(authError)); + } else { + reject(new Error('Authentication cancelled')); + } + } + }, 1000); + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; + setError(errorMessage); + console.error('TikTok authentication error:', error); + return null; + } finally { + setIsAuthenticating(false); + } + }, []); + + const verifyTikTokProfile = useCallback(async (profileUrl: string): Promise => { + if (!authResult?.userInfo.username) return false; + + try { + // Extract username from profile URL + const usernameMatch = profileUrl.match(/tiktok\.com\/@([^\/\?]+)/); + if (!usernameMatch) return false; + + const profileUsername = usernameMatch[1]; + return profileUsername === authResult.userInfo.username; + } catch (error) { + console.error('Profile verification error:', error); + return false; + } + }, [authResult]); + + const reset = useCallback(() => { + setAuthResult(null); + setError(null); + localStorage.removeItem('tiktok_auth_result'); + localStorage.removeItem('tiktok_auth_state'); + }, []); + + return { + isAuthenticating, + authResult, + error, + authenticateWithTikTok, + verifyTikTokProfile, + reset, + }; +}; \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index e1c2eb10..3286c450 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -20,8 +20,9 @@ "next": "14.2.16", "react": "^18", "react-dom": "^18", + "react-github-btn": "^1.4.0", "react-social-media-embed": "^2.5.18", - "recharts": "^2.15.3", + "recharts": "2.15.4", "sharp": "^0.34.2", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7" @@ -4555,6 +4556,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-buttons": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/github-buttons/-/github-buttons-2.29.1.tgz", + "integrity": "sha512-TV3YgAKda5hPz75n7QXmGCsSzgVya1vvmBieebg3EB5ScmashTZ0FldViG1aU2d4V5rcAGrtQ7k5uAaCo0A4PA==", + "license": "BSD-2-Clause" + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -6261,6 +6268,18 @@ "react": "^18.3.1" } }, + "node_modules/react-github-btn": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz", + "integrity": "sha512-lV4FYClAfjWnBfv0iNlJUGhamDgIq6TayD0kPZED6VzHWdpcHmPfsYOZ/CFwLfPv4Zp+F4m8QKTj0oy2HjiGXg==", + "license": "BSD-2-Clause", + "dependencies": { + "github-buttons": "^2.22.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-html-props": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/react-html-props/-/react-html-props-2.0.10.tgz", @@ -6455,9 +6474,10 @@ } }, "node_modules/recharts": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", - "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1",