diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 55d06ee..f29d4f9 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -60,7 +60,7 @@ app.use( callback(new Error('Not allowed by CORS')); } }, - allowedHeaders: ['Authorization', 'Content-Type'], + allowedHeaders: ['Authorization', 'Content-Type', 'X-Auth-Actor', 'X-Auth-Role', 'X-Client-Role'], credentials: true, }), ); diff --git a/apps/auth-service/src/controllers/auth.controller.ts b/apps/auth-service/src/controllers/auth.controller.ts index 241cbf0..c658739 100644 --- a/apps/auth-service/src/controllers/auth.controller.ts +++ b/apps/auth-service/src/controllers/auth.controller.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from 'express'; import { AuthError, ValidationError } from '@packages/error-handler'; import prisma from '@packages/libs/prisma'; +import imageKit from '@packages/libs/imageKit'; import { checkOTPRestrictions, handleForgotPassword, @@ -114,9 +115,6 @@ export const UserLogin = async (request: Request, response: Response, next: Next setCookie(response, 'access_token', accessToken); setCookie(response, 'refresh_token', refreshToken); - response.clearCookie('seller_access_token'); - response.clearCookie('seller_refresh_token'); - response.status(200).json({ success: true, message: 'User logged in successfully!', @@ -134,10 +132,7 @@ export const UserLogin = async (request: Request, response: Response, next: Next //Refresh Token User export const RefreshToken = async (request: any, response: Response, next: NextFunction) => { try { - const refreshToken = - request.cookies['refresh_token'] || - request.cookies['seller_refresh_token'] || - request.headers.authorization?.split(' ')[1]; + const refreshToken = request.cookies['refresh_token'] || request.headers.authorization?.split(' ')[1]; if (!refreshToken) { throw new ValidationError('Unauthorized! No refresh token'); @@ -148,16 +143,11 @@ export const RefreshToken = async (request: any, response: Response, next: NextF role: string; }; - if (!decoded || !decoded.id || !decoded.role) { + if (!decoded || !decoded.id || !decoded.role || decoded.role !== 'user') { return new JsonWebTokenError('Forbidden! Invalid refresh token'); } - let account; - if (decoded.role === 'user') { - account = await prisma.users.findUnique({ where: { id: decoded.id } }); - } else if (decoded.role === 'seller') { - account = await prisma.sellers.findUnique({ where: { id: decoded.id }, include: { shop: true } }); - } + const account = await prisma.users.findUnique({ where: { id: decoded.id } }); if (!account) { return new AuthError('User not found!'); @@ -174,15 +164,55 @@ export const RefreshToken = async (request: any, response: Response, next: NextF }, ); - if (decoded.role === 'user') { - setCookie(response, 'access_token', accessToken); - } else if (decoded.role === 'seller') { - setCookie(response, 'seller_access_token', accessToken); + setCookie(response, 'access_token', accessToken); + setCookie(response, 'refresh_token', refreshToken); + + request.role = decoded.role; + response.status(201).json({ success: true }); + } catch (error) { + return next(error); + } +}; + +//Refresh Token Seller +export const RefreshSellerToken = async (request: any, response: Response, next: NextFunction) => { + try { + const refreshToken = request.cookies['seller_refresh_token'] || request.headers.authorization?.split(' ')[1]; + + if (!refreshToken) { + throw new ValidationError('Unauthorized! No refresh token'); } + const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET as string) as { + id: string; + role: string; + }; + + if (!decoded || !decoded.id || !decoded.role || decoded.role !== 'seller') { + return new JsonWebTokenError('Forbidden! Invalid refresh token'); + } + + const account = await prisma.sellers.findUnique({ where: { id: decoded.id }, include: { shop: true } }); + + if (!account) { + return new AuthError('Seller not found!'); + } + + const accessToken = jwt.sign( + { + id: decoded.id, + role: decoded.role, + }, + process.env.ACCESS_TOKEN_SECRET as string, + { + expiresIn: '72h', + }, + ); + + setCookie(response, 'seller_access_token', accessToken); + setCookie(response, 'seller_refresh_token', refreshToken); + request.role = decoded.role; - // store the tokens in httpOnly secure cookie - setCookie(response, 'refresh_token', refreshToken); response.status(201).json({ success: true }); } catch (error) { return next(error); @@ -199,8 +229,6 @@ export const getUser = async (request: any, response: Response, next: NextFuncti return next(new AuthError('User or seller not found!')); } - response.clearCookie('seller_access_token'); - response.clearCookie('seller_refresh_token'); return next(new AuthError('User not Signed In!')); } response.status(200).json({ user }); @@ -291,7 +319,14 @@ export const verifySeller = async (request: Request, response: Response, next: N await verifyOTP(email, otp, next); const hashedPassword = await bcrypt.hash(password, 10); const seller = await prisma.sellers.create({ - data: { name, email, password: hashedPassword, phone_number, country }, + data: { + name, + email, + password: hashedPassword, + phone_number, + country, + shopId: null, // Explicitly set to null initially + }, }); response.status(200).json({ @@ -334,7 +369,7 @@ export const sellerLogin = async (request: Request, response: Response, next: Ne role: 'seller', }, process.env.ACCESS_TOKEN_SECRET as string, - { expiresIn: '24h' }, + { expiresIn: '72h' }, ); const refreshToken = jwt.sign( @@ -343,7 +378,7 @@ export const sellerLogin = async (request: Request, response: Response, next: Ne role: 'seller', }, process.env.REFRESH_TOKEN_SECRET as string, - { expiresIn: '7d' }, + { expiresIn: '30d' }, ); //Because we set same cookies for user and seller, only one type of account can be logged in at a time. @@ -351,9 +386,6 @@ export const sellerLogin = async (request: Request, response: Response, next: Ne setCookie(response, 'seller_access_token', accessToken); setCookie(response, 'seller_refresh_token', refreshToken); - response.clearCookie('access_token'); - response.clearCookie('refresh_token'); - response.status(200).json({ success: true, message: 'Seller logged in successfully!', @@ -402,6 +434,12 @@ export const createNewShop = async (request: Request, response: Response, next: data: shopData, }); + // Update the seller's shopId to link them + await prisma.sellers.update({ + where: { id: sellerId }, + data: { shopId: shop.id }, + }); + response.status(200).json({ success: true, shop, @@ -417,12 +455,26 @@ export const logoutUser = async (request: any, response: Response, next: NextFun // Clear all authentication cookies response.clearCookie('access_token'); response.clearCookie('refresh_token'); + + response.status(200).json({ + success: true, + message: 'Logged out successfully!', + }); + } catch (error) { + return next(error); + } +}; + +//Logout seller +export const logoutSeller = async (request: any, response: Response, next: NextFunction) => { + try { + // Clear all authentication cookies response.clearCookie('seller_access_token'); response.clearCookie('seller_refresh_token'); response.status(200).json({ success: true, - message: 'Logged out successfully!', + message: 'Seller logged out successfully!', }); } catch (error) { return next(error); @@ -659,6 +711,211 @@ export const changeUserPassword = async (request: any, response: Response, next: } }; +//Get shop information +export const getShopInfo = async (request: any, response: Response, next: NextFunction) => { + try { + const sellerId = request.seller.id; + + const shop = await prisma.shops.findUnique({ + where: { sellerId }, + include: { + avatar: true, + productAnalytics: true, + }, + }); + + if (!shop) { + return next(new ValidationError('Shop not found!')); + } + + response.status(200).json({ + success: true, + shop, + }); + } catch (error) { + return next(error); + } +}; + +//Get shop information by id for public access +export const getShopInfoById = async (request: Request, response: Response, next: NextFunction) => { + try { + const { shopId } = request.params; + + if (!shopId) { + return next(new ValidationError('Shop ID is required!')); + } + + const shop = await prisma.shops.findUnique({ + where: { id: shopId }, + include: { + avatar: true, + productAnalytics: true, + products: { + where: { + isDeleted: false, + status: 'ACTIVE', + }, + include: { + images: true, + shop: { + select: { + id: true, + name: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }, + reviews: { + include: { + user: { + select: { + id: true, + name: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }, + sellers: { + select: { + id: true, + name: true, + email: true, + country: true, + phone_number: true, + }, + }, + }, + }); + + if (!shop) { + return next(new ValidationError('Shop not found!')); + } + + const totalProducts = shop.products?.length || 0; + const totalReviews = shop.reviews?.length || 0; + const averageRating = + totalReviews > 0 + ? shop.reviews.reduce((sum, review) => sum + (review.rating || 0), 0) / totalReviews + : shop.ratings || 0; + + return response.status(200).json({ + success: true, + shop: { + ...shop, + totalProducts, + totalReviews, + averageRating, + }, + }); + } catch (error) { + return next(error); + } +}; + +//Update shop information +export const updateShopInfo = async (request: any, response: Response, next: NextFunction) => { + try { + const sellerId = request.seller.id; + const { name, bio, address, opening_hours, website, category, social_links } = request.body; + + // Validate required fields + if (!name || !bio || !address || !opening_hours || !category) { + return next(new ValidationError('Missing required fields!')); + } + + const shop = await prisma.shops.findUnique({ + where: { sellerId }, + }); + + if (!shop) { + return next(new ValidationError('Shop not found!')); + } + + const updateData: any = { + name, + bio, + address, + opening_hours, + category, + }; + + if (website && website.trim()) { + updateData.website = website; + } + + if (social_links && Array.isArray(social_links)) { + updateData.social_links = social_links; + } + + const updatedShop = await prisma.shops.update({ + where: { sellerId }, + data: updateData, + include: { + avatar: true, + productAnalytics: true, + }, + }); + + response.status(200).json({ + success: true, + message: 'Shop information updated successfully!', + shop: updatedShop, + }); + } catch (error) { + return next(error); + } +}; + +//Update shop cover banner +export const updateShopCoverBanner = async (request: any, response: Response, next: NextFunction) => { + try { + const sellerId = request.seller.id; + const { coverBanner } = request.body; + + if (!coverBanner || typeof coverBanner !== 'string') { + return next(new ValidationError('Cover banner file is required!')); + } + + const shop = await prisma.shops.findUnique({ + where: { sellerId }, + }); + + if (!shop) { + return next(new ValidationError('Shop not found!')); + } + + const uploadResponse = await imageKit.upload({ + file: coverBanner, + fileName: `shop-cover-${sellerId}-${Date.now()}.jpg`, + folder: '/shops/cover-banners', + }); + + const updatedShop = await prisma.shops.update({ + where: { sellerId }, + data: { coverBanner: uploadResponse.url }, + include: { + avatar: true, + productAnalytics: true, + }, + }); + + response.status(200).json({ + success: true, + message: 'Cover banner updated successfully!', + shop: updatedShop, + }); + } catch (error) { + return next(error); + } +}; + //Create stripe connect account link export const createStripeConnectLink = async (request: Request, response: Response, next: NextFunction) => { try { @@ -708,8 +965,8 @@ export const createStripeConnectLink = async (request: Request, response: Respon const accountLink = await stripe.accountLinks.create({ account: account.id, - refresh_url: `http://localhost:3000/seller/success`, - return_url: `http://localhost:3000/seller/success`, + refresh_url: `https://nextcart-seller.vercel.app/seller/success`, + return_url: `https://nextcart-seller.vercel.app/seller/success`, type: 'account_onboarding', }); diff --git a/apps/auth-service/src/main.ts b/apps/auth-service/src/main.ts index e99afea..93f3fbf 100644 --- a/apps/auth-service/src/main.ts +++ b/apps/auth-service/src/main.ts @@ -24,7 +24,7 @@ app.use( callback(new Error('Not allowed by CORS')); } }, - allowedHeaders: ['Authorization', 'Content-Type'], + allowedHeaders: ['Authorization', 'Content-Type', 'X-Auth-Actor', 'X-Auth-Role', 'X-Client-Role'], credentials: true, }), ); diff --git a/apps/auth-service/src/routes/auth.router.ts b/apps/auth-service/src/routes/auth.router.ts index 79232cb..dfc3c82 100644 --- a/apps/auth-service/src/routes/auth.router.ts +++ b/apps/auth-service/src/routes/auth.router.ts @@ -7,16 +7,22 @@ import { createStripeConnectLink, deleteUserAddress, getSeller, + getShopInfo, + getShopInfoById, getUser, getUserAddresses, logoutUser, + logoutSeller, sellerLogin, SellerRegistration, setDefaultAddress, + updateShopCoverBanner, + updateShopInfo, updateUserAddress, UserForgotPassword, UserLogin, RefreshToken, + RefreshSellerToken, UserRegistration, UserResetPassword, UserVerification, @@ -32,6 +38,7 @@ router.post('/user-registration', UserRegistration); router.post('/verify-user', UserVerification); router.post('/login-user', UserLogin); router.post('/refresh-token', RefreshToken); +router.post('/refresh-seller-token', RefreshSellerToken); router.get('/logged-in-user', isAuthenticated, getUser); router.post('/forgot-password-user', UserForgotPassword); router.post('/verify-forgot-password-otp', UserVerifyForgotPasswordOTP); @@ -43,6 +50,7 @@ router.post('/login-seller', sellerLogin); router.get('/logged-in-seller', isAuthenticated, isSeller, getSeller); router.post('/create-stripe-link', createStripeConnectLink); router.post('/logout', logoutUser); +router.post('/logout-seller', logoutSeller); // User Address Management router.get('/user-addresses', isAuthenticated, getUserAddresses); @@ -54,4 +62,10 @@ router.patch('/user-addresses/:id/set-default', isAuthenticated, setDefaultAddre // User Password Management router.put('/change-password', isAuthenticated, changeUserPassword); +// Shop Management +router.get('/shop-info/:shopId', getShopInfoById); +router.get('/shop-info', isAuthenticated, isSeller, getShopInfo); +router.put('/shop-info', isAuthenticated, isSeller, updateShopInfo); +router.put('/shop-cover-banner', isAuthenticated, isSeller, updateShopCoverBanner); + export default router; diff --git a/apps/auth-service/src/utils/cookies/setCookie.ts b/apps/auth-service/src/utils/cookies/setCookie.ts index fbf027c..0fb1d71 100644 --- a/apps/auth-service/src/utils/cookies/setCookie.ts +++ b/apps/auth-service/src/utils/cookies/setCookie.ts @@ -5,6 +5,6 @@ export const setCookie = (response: Response, name: string, value: string) => { httpOnly: true, secure: true, sameSite: 'none', - maxAge: 7 * 24 * 60 * 60 * 1000, //7 days + maxAge: 30 * 24 * 60 * 60 * 1000, //30 days }); }; diff --git a/apps/chat-service/src/main.ts b/apps/chat-service/src/main.ts index a26dcbf..4c2215e 100644 --- a/apps/chat-service/src/main.ts +++ b/apps/chat-service/src/main.ts @@ -20,7 +20,7 @@ app.use( callback(new Error('Not allowed by CORS')); } }, - allowedHeaders: ['Authorization', 'Content-Type'], + allowedHeaders: ['Authorization', 'Content-Type', 'X-Auth-Actor', 'X-Auth-Role', 'X-Client-Role'], credentials: true, }), ); diff --git a/apps/order-service/src/main.ts b/apps/order-service/src/main.ts index 3cb5ea9..d7c5c0a 100644 --- a/apps/order-service/src/main.ts +++ b/apps/order-service/src/main.ts @@ -23,7 +23,7 @@ app.use( callback(new Error('Not allowed by CORS')); } }, - allowedHeaders: ['Authorization', 'Content-Type'], + allowedHeaders: ['Authorization', 'Content-Type', 'X-Auth-Actor', 'X-Auth-Role', 'X-Client-Role'], credentials: true, }), ); diff --git a/apps/order-service/src/utils/order.utils.ts b/apps/order-service/src/utils/order.utils.ts index 72790db..4cc724e 100644 --- a/apps/order-service/src/utils/order.utils.ts +++ b/apps/order-service/src/utils/order.utils.ts @@ -149,6 +149,7 @@ export const sendOrderConfirmationEmail = async ( name, cart, totalAmount, + sessionId, trackingUrl: `https://nextcart.com/order/${sessionId}`, }); }; diff --git a/apps/product-service/src/main.ts b/apps/product-service/src/main.ts index 28071cc..1e47794 100644 --- a/apps/product-service/src/main.ts +++ b/apps/product-service/src/main.ts @@ -24,7 +24,7 @@ app.use( callback(new Error('Not allowed by CORS')); } }, - allowedHeaders: ['Authorization', 'Content-Type'], + allowedHeaders: ['Authorization', 'Content-Type', 'X-Auth-Actor', 'X-Auth-Role', 'X-Client-Role'], credentials: true, }), ); diff --git a/apps/seller-ui/src/app/(routes)/dashboard/inbox/page.tsx b/apps/seller-ui/src/app/(routes)/dashboard/inbox/page.tsx index d86a437..91a1091 100644 --- a/apps/seller-ui/src/app/(routes)/dashboard/inbox/page.tsx +++ b/apps/seller-ui/src/app/(routes)/dashboard/inbox/page.tsx @@ -3,7 +3,7 @@ import React, { Suspense, useState, useEffect } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { useWebSocket } from 'apps/seller-ui/src/context/websocket-context'; -import { useConversations } from 'apps/seller-ui/src/hooks/chat'; +import { useConversations, useMarkAsSeen } from 'apps/seller-ui/src/hooks/chat'; import { ChatHeader, ChatWindow, ConversationList, EmptyChatState } from 'apps/seller-ui/src/shared/organisms'; const InboxPageInner: React.FC = () => { @@ -12,7 +12,8 @@ const InboxPageInner: React.FC = () => { const searchParams = useSearchParams(); const router = useRouter(); - const { isConnected, isConnecting, error, ws } = useWebSocket(); + const { isConnected, isConnecting, error } = useWebSocket(); + const { mutate: markConversationAsSeen } = useMarkAsSeen(); const conversationsQuery = useConversations(); const conversations = conversationsQuery.data?.conversations || []; @@ -45,13 +46,7 @@ const InboxPageInner: React.FC = () => { router.replace('/dashboard/inbox'); } - // Also mark it as seen using ws - ws?.send( - JSON.stringify({ - type: 'MARK_AS_SEEN', - conversationId: conversationId, - }), - ); + markConversationAsSeen({ conversationId }); } }; diff --git a/apps/seller-ui/src/app/(routes)/dashboard/logout/page.tsx b/apps/seller-ui/src/app/(routes)/dashboard/logout/page.tsx new file mode 100644 index 0000000..09e585c --- /dev/null +++ b/apps/seller-ui/src/app/(routes)/dashboard/logout/page.tsx @@ -0,0 +1,78 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import useLogout from '../../../../hooks/useLogout'; +import { LogOut, AlertTriangle } from 'lucide-react'; + +const LogoutPage = () => { + const [showConfirmation, setShowConfirmation] = useState(false); + const [isLoggingOut, setIsLoggingOut] = useState(false); + const { logout } = useLogout(); + const router = useRouter(); + + useEffect(() => { + // Show confirmation dialog immediately when page loads + setShowConfirmation(true); + }, []); + + const handleLogoutConfirm = async () => { + setIsLoggingOut(true); + await logout(); + }; + + const handleCancel = () => { + // Go back to dashboard + router.push('/'); + }; + + if (!showConfirmation) { + return null; + } + + return ( +
+ Are you sure you want to logout? You will need to login again to access your seller account. +
+ +Stay updated with your shop activity and customer interactions
++ We're building a comprehensive notification system to keep you informed about orders, customer messages, and + important shop updates. +
+ +Manage your account and shop preferences
+Home . Login
-- Don't have an account?{' '} - - Sign up - -
- -