From 700474cd1925952e28ba87c569ea69597f06cf86 Mon Sep 17 00:00:00 2001
From: Hyunkyung
Date: Tue, 9 Jun 2026 09:26:39 +0200
Subject: [PATCH] add sso
---
apps/web/app/(auth)/login/LoginForm.tsx | 32 +++++---
apps/web/app/components/Header.tsx | 82 +++++++++++++++++--
.../web/app/components/SocialLoginButtons.tsx | 26 +++---
apps/web/app/globals.css | 63 ++++++++++++--
apps/web/lib/auth-providers.ts | 4 +-
apps/web/lib/auth.ts | 47 +++++++++--
6 files changed, 209 insertions(+), 45 deletions(-)
diff --git a/apps/web/app/(auth)/login/LoginForm.tsx b/apps/web/app/(auth)/login/LoginForm.tsx
index 5cd30d1..44ddb5b 100644
--- a/apps/web/app/(auth)/login/LoginForm.tsx
+++ b/apps/web/app/(auth)/login/LoginForm.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { signIn, getSession } from 'next-auth/react';
@@ -11,6 +11,16 @@ import { toast } from 'sonner';
import SocialLoginButtons from '@/app/components/SocialLoginButtons';
import type { OAuthProvider } from '@/lib/auth-providers';
+const OAUTH_ERROR_MESSAGES: Record = {
+ OAuthAccountNotLinked: 'This email is already registered. Sign in with email & password, or use the same social login you originally used.',
+ OAuthSignin: 'Could not start social login. Please try again.',
+ OAuthCallback: 'Social login failed during callback. Please try again.',
+ OAuthCreateAccount: 'Could not create account via social login. Please try again.',
+ Configuration: 'A server configuration error occurred. Please try again later.',
+ AccessDenied: 'Access was denied. Please try again.',
+ Verification: 'The sign-in link has expired or already been used.',
+};
+
const loginSchema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string().min(1, 'Password is required'),
@@ -21,8 +31,17 @@ export default function LoginForm({ enabledProviders }: { enabledProviders: OAut
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl');
+ const oauthError = searchParams.get('error');
const [isLoading, setIsLoading] = useState(false);
+ useEffect(() => {
+ if (oauthError) {
+ const message = OAUTH_ERROR_MESSAGES[oauthError] ?? `Sign-in error: ${oauthError}`;
+ toast.error(message, { duration: 6000 });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const {
register,
handleSubmit,
@@ -130,17 +149,6 @@ export default function LoginForm({ enabledProviders }: { enabledProviders: OAut
Don't have an account?{' '}
Create one — it's free
-
- {/* Demo credentials (dev only) */}
- {process.env.NODE_ENV === 'development' && (
- <>
- Demo credentials
-
-
Customer: sarah.miller@gmail.com / Customer2026!
-
Admin: admin@ecowoods.ca / Admin2026!
-
- >
- )}
);
diff --git a/apps/web/app/components/Header.tsx b/apps/web/app/components/Header.tsx
index 7cbb099..71dd923 100644
--- a/apps/web/app/components/Header.tsx
+++ b/apps/web/app/components/Header.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
+import { useSession } from 'next-auth/react';
/* ---------------------- Hooks ---------------------- */
function useScrollState() {
@@ -42,6 +43,24 @@ const navigation = [
{ label: 'FAQ', href: '#faq' },
];
+const MYPAGE_NAV = [
+ { href: '/mypage', label: 'Dashboard' },
+ { href: '/mypage/quotes', label: 'My Quotes' },
+ { href: '/mypage/projects', label: 'My Projects' },
+ { href: '/mypage/invoices', label: 'Invoices & Payments' },
+ { href: '/mypage/inquiries', label: 'Inquiries' },
+];
+
+const ADMIN_NAV = [
+ { href: '/admin', label: 'Dashboard' },
+ { href: '/admin/quotes', label: 'Quotes & Leads' },
+ { href: '/admin/projects', label: 'Projects' },
+ { href: '/admin/invoices', label: 'Invoices' },
+ { href: '/admin/users', label: 'Customers' },
+ { href: '/admin/inquiries', label: 'Inquiries' },
+ { href: '/admin/settings', label: 'Settings' },
+];
+
const PHONE_DISPLAY = '(416) 249-1276';
const PHONE_HREF = 'tel:+14162491276';
@@ -49,8 +68,10 @@ const PHONE_HREF = 'tel:+14162491276';
export default function Header() {
const { direction, scrolled } = useScrollState();
const [mobileOpen, setMobileOpen] = useState(false);
+ const [portalExpanded, setPortalExpanded] = useState(false);
const [activeSection, setActiveSection] = useState('');
const [baseUrl, setBaseUrl] = useState('/');
+ const { data: session, status } = useSession();
useEffect(() => {
setBaseUrl(window.location.origin + '/');
@@ -70,6 +91,11 @@ export default function Header() {
};
}, [mobileOpen]);
+ // Reset portal sub-menu when mobile sheet closes
+ useEffect(() => {
+ if (!mobileOpen) setPortalExpanded(false);
+ }, [mobileOpen]);
+
// Track active section on scroll
useEffect(() => {
const onScroll = () => {
@@ -99,6 +125,12 @@ export default function Header() {
return () => window.removeEventListener('keydown', onKey);
}, []);
+ const isAdmin = session?.user?.role === 'ADMIN';
+ const isLoggedIn = status === 'authenticated';
+ const portalLabel = isAdmin ? 'Admin' : 'My Page';
+ const portalNav = isAdmin ? ADMIN_NAV : MYPAGE_NAV;
+ const portalIndex = navigation.length + 2;
+
return (
<>
-
+
- Login
+ {isLoggedIn ? portalLabel : 'Login'}
- setMobileOpen(false)}>
- Login
- 0{navigation.length + 2}
-
+
+ {/* Portal section — Login or My Page / Admin */}
+ {!isLoggedIn && status !== 'loading' ? (
+ setMobileOpen(false)}>
+ Login
+ 0{portalIndex}
+
+ ) : isLoggedIn ? (
+
+
+ {portalExpanded && (
+
+ )}
+
+ ) : null}
diff --git a/apps/web/app/components/SocialLoginButtons.tsx b/apps/web/app/components/SocialLoginButtons.tsx
index bc57446..04b3c91 100644
--- a/apps/web/app/components/SocialLoginButtons.tsx
+++ b/apps/web/app/components/SocialLoginButtons.tsx
@@ -1,7 +1,7 @@
'use client';
/**
- * SocialLoginButtons — Google, Facebook, Apple OAuth 로그인 버튼
+ * SocialLoginButtons — Google, Facebook, X(Twitter) OAuth 로그인 버튼
*
* 어떤 provider가 환경변수에 설정되어 있는지 서버에서 prop으로 받아
* 활성화된 버튼만 렌더링합니다.
@@ -9,7 +9,7 @@
* 사용 예시:
*
*/
@@ -17,7 +17,7 @@ import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { toast } from 'sonner';
-type Provider = 'google' | 'facebook' | 'apple';
+type Provider = 'google' | 'facebook' | 'twitter';
const PROVIDER_CONFIG: Record
= {
google: {
@@ -44,22 +44,22 @@ const PROVIDER_CONFIG: Record
),
style: {
- background: '#1877F2',
- color: '#ffffff',
- border: '1px solid #1877F2',
+ background: '#ffffff',
+ color: '#3c4043',
+ border: '1px solid #dadce0',
},
},
- apple: {
- label: 'Continue with Apple',
+ twitter: {
+ label: 'Continue with X',
icon: (
-