diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3fe5599..5ed8600 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "localmart", "version": "0.1.0", "dependencies": { + "@clerk/nextjs": "^6.12.0", "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@stripe/react-stripe-js": "^3.1.1", @@ -45,6 +46,115 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@clerk/backend": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.24.2.tgz", + "integrity": "sha512-O5W3AlZ/g5dcdadXbBJfq+JqXoTimR2vm8bMTEJ2MEaCDXOyFoGvlv+93XxS3TgcuWFrVPgzdBPfaE/95G5Xxw==", + "dependencies": { + "@clerk/shared": "^2.22.0", + "@clerk/types": "^4.46.1", + "cookie": "1.0.2", + "snakecase-keys": "8.0.1", + "tslib": "2.4.1" + }, + "engines": { + "node": ">=18.17.0" + } + }, + "node_modules/@clerk/backend/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/@clerk/clerk-react": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.23.0.tgz", + "integrity": "sha512-2BMT3+KOWRJsAPDuhFoVdee2solukUcHw8BuKFfiOw6XXbU01H7WBezQCgF8gGFwrygEzZ0P5MBIz3L6MC/LYQ==", + "dependencies": { + "@clerk/shared": "^2.22.0", + "@clerk/types": "^4.46.1", + "tslib": "2.4.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + } + }, + "node_modules/@clerk/clerk-react/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/@clerk/nextjs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.12.0.tgz", + "integrity": "sha512-fA+M09d9KdZd0L0yDpQZJ0LscKwrZkJVa+4Zkk9LVdeanhbrLokQljS9ZbZvAS+jGKTzjX0SPhTeJdoq3NTFHg==", + "dependencies": { + "@clerk/backend": "^1.24.2", + "@clerk/clerk-react": "^5.23.0", + "@clerk/shared": "^2.22.0", + "@clerk/types": "^4.46.1", + "crypto-js": "4.2.0", + "server-only": "0.0.1", + "tslib": "2.4.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "next": "^13.5.4 || ^14.0.3 || ^15.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + } + }, + "node_modules/@clerk/nextjs/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/@clerk/shared": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-2.22.0.tgz", + "integrity": "sha512-VWBeddOJVa3sqUPdvquaaQYw4h5hACSG3EUDOW7eSu2F6W3BXUozyLJQPBJ9C0MuoeHhOe/DeV8x2KqOgxVZaQ==", + "hasInstallScript": true, + "dependencies": { + "@clerk/types": "^4.46.1", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.7.0", + "swr": "^2.2.0" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/types": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.46.1.tgz", + "integrity": "sha512-QwomMjG1v2GqOITU2/rQUC11LFFsqNFUA92VKDo8dS1nN9iQadT6cNk7MZWqEYTxAfAM/IgX/gB00aGsLKaV8g==", + "dependencies": { + "csstype": "3.1.3" + }, + "engines": { + "node": ">=18.17.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -2018,6 +2128,14 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2033,6 +2151,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2174,6 +2297,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2211,6 +2342,15 @@ "node": ">=0.10.0" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3206,6 +3346,11 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3919,6 +4064,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4087,6 +4240,14 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -4094,6 +4255,17 @@ "dev": true, "license": "ISC" }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4287,6 +4459,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5086,6 +5267,11 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5297,6 +5483,28 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/snakecase-keys": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.0.1.tgz", + "integrity": "sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==", + "dependencies": { + "map-obj": "^4.1.0", + "snake-case": "^3.0.4", + "type-fest": "^4.15.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5313,6 +5521,11 @@ "dev": true, "license": "MIT" }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5633,6 +5846,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz", + "integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -5805,6 +6030,17 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz", + "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -5933,6 +6169,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2118c43..719d0ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@clerk/nextjs": "^6.12.0", "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@stripe/react-stripe-js": "^3.1.1", diff --git a/frontend/src/app/contexts/auth.tsx b/frontend/src/app/contexts/auth.tsx index 59c74b8..7eb033f 100644 --- a/frontend/src/app/contexts/auth.tsx +++ b/frontend/src/app/contexts/auth.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { config } from '@/config'; +import { useSignIn } from '@clerk/nextjs'; interface User { id: string; @@ -15,12 +16,16 @@ interface AuthContextType { login: (email: string, password: string) => Promise; signup: (email: string, password: string, name: string) => Promise; logout: () => void; + sendMagicLink: (email: string) => Promise; + isProcessingMagicLink: boolean; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); + const [isProcessingMagicLink, setIsProcessingMagicLink] = useState(false); + const { signIn, isLoaded: isClerkLoaded } = useSignIn(); useEffect(() => { // Check for saved auth data on mount @@ -95,8 +100,33 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { localStorage.removeItem('auth'); }; + const sendMagicLink = async (email: string) => { + if (!isClerkLoaded) return; + + try { + setIsProcessingMagicLink(true); + await signIn.create({ + strategy: 'email_link', + identifier: email, + redirectUrl: `${window.location.origin}/login`, + }); + } catch (error) { + console.error('Error sending magic link:', error); + throw error; + } finally { + setIsProcessingMagicLink(false); + } + }; + return ( - + {children} ); diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 93958b5..ba4e738 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter, DM_Sans } from "next/font/google"; import "./globals.css"; +import { ClerkProvider } from '@clerk/nextjs'; import { AuthProvider } from './contexts/auth'; import { CartProvider } from './contexts/cart'; import { FeatureFlagsProvider } from './contexts/featureFlags'; @@ -36,14 +37,16 @@ export default function RootLayout({ - - - -
- {children} - - - + + + + +
+ {children} + + + + diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index f0c3be7..d0589e1 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -4,13 +4,15 @@ import { useState, useEffect, Suspense } from 'react'; import { useAuth } from '../contexts/auth'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; -import { BuildingStorefrontIcon } from '@heroicons/react/24/outline'; +import { BuildingStorefrontIcon, EnvelopeIcon } from '@heroicons/react/24/outline'; +import toast from 'react-hot-toast'; function LoginForm() { - const { login, signup } = useAuth(); + const { login, signup, sendMagicLink, isProcessingMagicLink } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const [isSignup, setIsSignup] = useState(false); + const [isMagicLink, setIsMagicLink] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [firstName, setFirstName] = useState(''); @@ -29,6 +31,12 @@ function LoginForm() { setIsLoading(true); try { + if (isMagicLink) { + await sendMagicLink(email); + toast.success('Check your email for the magic link!'); + return; + } + if (isSignup) { await signup(email, password, `${firstName} ${lastName}`); } else { @@ -37,16 +45,20 @@ function LoginForm() { router.push('/'); } catch (err) { setError(err instanceof Error ? err.message : 'Authentication failed'); + if (isMagicLink) { + toast.error('Failed to send magic link. Please try again.'); + } } finally { setIsLoading(false); } }; - const toggleMode = () => { + const toggleMode = (mode: 'signup' | 'login' | 'magic-link') => { setError(''); - setIsSignup(!isSignup); + setIsSignup(mode === 'signup'); + setIsMagicLink(mode === 'magic-link'); // Update URL without refreshing the page - const newUrl = !isSignup ? '/login?signup=true' : '/login'; + const newUrl = mode === 'signup' ? '/login?signup=true' : '/login'; window.history.pushState({}, '', newUrl); }; @@ -65,7 +77,7 @@ function LoginForm() {
- {isSignup && ( + {isSignup && !isMagicLink && (
-
- - setPassword(e.target.value)} - /> -
+ {!isMagicLink && ( +
+ + setPassword(e.target.value)} + /> +
+ )}
{error && ( @@ -143,8 +157,10 @@ function LoginForm() { disabled={isLoading} className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-white bg-[#2A9D8F] hover:bg-[#40B4A6] active:bg-[#1E7268] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#2A9D8F] transition-colors duration-200" > - {isLoading ? ( + {isLoading || isProcessingMagicLink ? ( 'Loading...' + ) : isMagicLink ? ( + 'Send Magic Link' ) : isSignup ? ( 'Sign up' ) : ( @@ -153,15 +169,31 @@ function LoginForm() {
-
+
+ {!isMagicLink && ( + + )}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 19394da..7230b34 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -3,4 +3,6 @@ export const config = { pocketbaseUrl: process.env.NEXT_PUBLIC_POCKETBASE_URL || 'http://localhost:8090', searchUrl: process.env.NEXT_PUBLIC_SEARCH_URL || 'http://localhost:4100', meilisearchUrl: process.env.NEXT_PUBLIC_MEILI_URL || 'http://localhost:7700', -}; \ No newline at end of file + next_public_clerk_publishable_key: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '', + clerk_secret_key: process.env.CLERK_SECRET_KEY || 'secret', +}; diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts new file mode 100644 index 0000000..50a1e03 --- /dev/null +++ b/frontend/src/middleware.ts @@ -0,0 +1,14 @@ +import { clerkMiddleware } from "@clerk/nextjs/server"; + +// This middleware will handle Clerk's magic link authentication +// while allowing the existing auth system to work normally +export default clerkMiddleware(); + +export const config = { + matcher: [ + // Skip Next.js internals and all static files, unless found in search params + '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + // Always run for API routes + '/(api|trpc)(.*)', + ], +};