@imksh/create 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/create.js +2 -1
- package/src/index.js +1 -1
- package/src/utils/askTemplate.js +1 -0
- package/templates/mern-react-native/mobile/_gitignore +43 -0
- package/templates/mern-react-native/mobile/package.json +2 -1
- package/templates/mern-react-native/mobile/src/app/(auth)/edit-profile.tsx +11 -15
- package/templates/mern-react-native/mobile/src/app/(tabs)/dashboard.tsx +2 -2
- package/templates/mern-react-native/mobile/src/app/(tabs)/search.tsx +2 -2
- package/templates/mern-react-native/mobile/src/config/sidebar.config.ts +0 -60
- package/templates/mern-react-native/mobile/src/config/tabNavigation.ts +6 -36
- package/templates/mern-react-native/mobile/src/services/notification.service.ts +22 -0
- package/templates/mern-react-native/mobile/src/store/useNotificationStore.ts +158 -33
- package/templates/next/next.config.mjs +1 -0
- package/templates/next/package.json +10 -9
- package/templates/next/postcss.config.js +5 -0
- package/templates/next/src/app/layout.js +1 -0
- package/templates/next/src/app/page.js +1 -1
- package/templates/next/src/components/layout/ProtectedRoute.jsx +3 -3
- package/templates/next/src/index.css +1 -0
- package/templates/next/src/store/useUiStore.js +13 -7
- package/templates/next-ts/.env.example +0 -0
- package/templates/next-ts/AGENTS.md +37 -0
- package/templates/next-ts/CLAUDE.md +1 -0
- package/templates/next-ts/next-env.d.ts +3 -1
- package/templates/next-ts/next.config.mjs +1 -0
- package/templates/next-ts/package.json +13 -12
- package/templates/next-ts/postcss.config.js +5 -0
- package/templates/next-ts/public/logo.png +0 -0
- package/templates/next-ts/src/app/error.tsx +41 -0
- package/templates/next-ts/src/app/layout.tsx +2 -1
- package/templates/next-ts/src/app/not-found.tsx +5 -0
- package/templates/next-ts/src/app/page.tsx +1 -1
- package/templates/next-ts/src/components/layout/ProtectedRoute.tsx +3 -3
- package/templates/next-ts/src/components/ui/Button.tsx +63 -0
- package/templates/next-ts/src/components/ui/Input.tsx +79 -0
- package/templates/next-ts/src/index.css +1 -0
- package/templates/next-ts/src/store/useUiStore.ts +13 -7
- package/templates/next-ts/tsconfig.json +19 -5
- package/templates/production-fullstack/client/_gitignore +24 -0
- package/templates/production-fullstack/client/index.html +14 -0
- package/templates/production-fullstack/client/package.json +47 -0
- package/templates/production-fullstack/client/public/_redirects +1 -0
- package/templates/production-fullstack/client/public/favicon.ico +0 -0
- package/templates/production-fullstack/client/public/favicon.svg +1 -0
- package/templates/production-fullstack/client/public/logo.png +0 -0
- package/templates/production-fullstack/client/public/manifest.json +24 -0
- package/templates/production-fullstack/client/public/sw.js +56 -0
- package/templates/production-fullstack/client/src/App.tsx +68 -0
- package/templates/production-fullstack/client/src/assets/animations/loading.json +1 -0
- package/templates/production-fullstack/client/src/assets/data/navigation.ts +29 -0
- package/templates/production-fullstack/client/src/assets/images/logo.png +0 -0
- package/templates/production-fullstack/client/src/components/layout/ProtectedRoute.tsx +23 -0
- package/templates/production-fullstack/client/src/components/layout/PublicLayout.tsx +13 -0
- package/templates/production-fullstack/client/src/components/layout/SidebarLayout.tsx +37 -0
- package/templates/production-fullstack/client/src/components/shared/Breadcrumbs.tsx +27 -0
- package/templates/production-fullstack/client/src/components/shared/LottieIcon.tsx +36 -0
- package/templates/production-fullstack/client/src/components/shared/NotFound.tsx +78 -0
- package/templates/production-fullstack/client/src/components/shared/PwaInstallPrompt.tsx +72 -0
- package/templates/production-fullstack/client/src/components/shared/SEO.tsx +67 -0
- package/templates/production-fullstack/client/src/components/shared/Scroll.tsx +25 -0
- package/templates/production-fullstack/client/src/components/shared/Unauthorized.tsx +76 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/CardSkeleton.tsx +23 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/DashboardSkeleton.tsx +19 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/ListSkeleton.tsx +24 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/PageSkeleton.tsx +26 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/ProfileSkeleton.tsx +23 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/Skeleton.tsx +18 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/TableSkeleton.tsx +33 -0
- package/templates/production-fullstack/client/src/components/ui/ConfirmModal.tsx +152 -0
- package/templates/production-fullstack/client/src/components/ui/Header.tsx +230 -0
- package/templates/production-fullstack/client/src/components/ui/Loading.tsx +19 -0
- package/templates/production-fullstack/client/src/components/ui/Modal.tsx +106 -0
- package/templates/production-fullstack/client/src/components/ui/SidebarHeader.tsx +34 -0
- package/templates/production-fullstack/client/src/components/ui/Table.tsx +118 -0
- package/templates/production-fullstack/client/src/components/ui/ToggleSwitch.tsx +38 -0
- package/templates/production-fullstack/client/src/components/ui/sidebar.tsx +152 -0
- package/templates/production-fullstack/client/src/config/index.ts +44 -0
- package/templates/production-fullstack/client/src/constants/navigation.ts +14 -0
- package/templates/production-fullstack/client/src/features/auth/api/authService.ts +19 -0
- package/templates/production-fullstack/client/src/features/auth/pages/Login.tsx +233 -0
- package/templates/production-fullstack/client/src/features/auth/store/useAuthStore.ts +143 -0
- package/templates/production-fullstack/client/src/features/core/pages/Home.tsx +150 -0
- package/templates/production-fullstack/client/src/features/core/pages/Profile.tsx +9 -0
- package/templates/production-fullstack/client/src/hooks/useLenis.ts +28 -0
- package/templates/production-fullstack/client/src/hooks/usePwaInstall.ts +45 -0
- package/templates/production-fullstack/client/src/hooks/useWindowSize.ts +25 -0
- package/templates/production-fullstack/client/src/index.css +158 -0
- package/templates/production-fullstack/client/src/lib/axios.ts +22 -0
- package/templates/production-fullstack/client/src/lib/dayjs.ts +12 -0
- package/templates/production-fullstack/client/src/lib/query-client.ts +11 -0
- package/templates/production-fullstack/client/src/lib/socket.ts +6 -0
- package/templates/production-fullstack/client/src/main.tsx +28 -0
- package/templates/production-fullstack/client/src/store/useUiStore.ts +74 -0
- package/templates/production-fullstack/client/src/utils/formatDate.ts +19 -0
- package/templates/production-fullstack/client/src/utils/getInitials.ts +10 -0
- package/templates/production-fullstack/client/src/vite-env.d.ts +1 -0
- package/templates/production-fullstack/client/tsconfig.json +25 -0
- package/templates/production-fullstack/client/tsconfig.node.json +10 -0
- package/templates/production-fullstack/client/vite.config.ts +8 -0
- package/templates/production-fullstack/mobile/LICENSE +21 -0
- package/templates/production-fullstack/mobile/_gitignore +43 -0
- package/templates/production-fullstack/mobile/app.json +84 -0
- package/templates/production-fullstack/mobile/assets/animations/celebrate.json +1 -0
- package/templates/production-fullstack/mobile/assets/animations/infinity.json +1 -0
- package/templates/production-fullstack/mobile/assets/animations/loading.json +1 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Bold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_ExtraBold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Medium.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Regular.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_SemiBold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logo.png +0 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logo.svg +1 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logoWithBg.png +0 -0
- package/templates/production-fullstack/mobile/babel.config.js +13 -0
- package/templates/production-fullstack/mobile/drizzle.config.ts +8 -0
- package/templates/production-fullstack/mobile/eas.json +24 -0
- package/templates/production-fullstack/mobile/eslint.config.js +10 -0
- package/templates/production-fullstack/mobile/expo-env.d.ts +3 -0
- package/templates/production-fullstack/mobile/metro.config.js +6 -0
- package/templates/production-fullstack/mobile/nativewind-env.d.ts +3 -0
- package/templates/production-fullstack/mobile/package.json +89 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/_layout.tsx +12 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/change-password.tsx +112 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/edit-profile.tsx +160 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/forgot-password.tsx +191 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/login.tsx +253 -0
- package/templates/production-fullstack/mobile/src/app/_layout.tsx +102 -0
- package/templates/production-fullstack/mobile/src/app/global.css +82 -0
- package/templates/production-fullstack/mobile/src/components/common/AnimatedHeader.tsx +328 -0
- package/templates/production-fullstack/mobile/src/components/common/ConfirmationToast.tsx +67 -0
- package/templates/production-fullstack/mobile/src/components/common/Loading.tsx +26 -0
- package/templates/production-fullstack/mobile/src/components/common/LocationLoader.tsx +591 -0
- package/templates/production-fullstack/mobile/src/components/common/ScreenHeader.tsx +68 -0
- package/templates/production-fullstack/mobile/src/components/common/TabHeader.tsx +58 -0
- package/templates/production-fullstack/mobile/src/components/common/Typography.tsx +113 -0
- package/templates/production-fullstack/mobile/src/components/common/Welcome.tsx +55 -0
- package/templates/production-fullstack/mobile/src/components/ui/BottomModal.tsx +159 -0
- package/templates/production-fullstack/mobile/src/components/ui/ClassicTabBar.tsx +129 -0
- package/templates/production-fullstack/mobile/src/components/ui/CustomTabBar.tsx +238 -0
- package/templates/production-fullstack/mobile/src/components/ui/Sidebar.tsx +258 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/Button.tsx +180 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/ButtonGroup.tsx +32 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/FloatingActionButton.tsx +72 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/IconButton.tsx +84 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Accordion.tsx +82 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Avatar.tsx +47 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Badge.tsx +104 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Card.tsx +36 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Image.tsx +43 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/ImageViewer.tsx +33 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/SwipeableRow.tsx +94 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/CircularProgress.tsx +38 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/GestureWrapper.tsx +114 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/ProgressBar.tsx +51 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/Skeleton.tsx +84 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/Spinner.tsx +50 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Checkbox.tsx +54 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/DatePicker.tsx +186 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/ImagePickerModal.tsx +129 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Input.tsx +158 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/LocationPicker.tsx +211 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/OTPInput.tsx +83 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/PasswordInput.tsx +16 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Radio.tsx +54 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/SearchBar.tsx +53 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Slider.tsx +49 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Switch.tsx +75 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/TextArea.tsx +15 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/WebLocationPicker.tsx +321 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Container.tsx +23 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Divider.tsx +20 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Grid.tsx +62 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/KeyboardAvoidingViewWrapper.tsx +16 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/ScrollContainer.tsx +21 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Section.tsx +51 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Spacer.tsx +6 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Stack.tsx +34 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/Pagination.tsx +61 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/ProgressStepper.tsx +66 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/Tabs.tsx +173 -0
- package/templates/production-fullstack/mobile/src/config/sidebar.config.ts +72 -0
- package/templates/production-fullstack/mobile/src/config/tabNavigation.ts +64 -0
- package/templates/production-fullstack/mobile/src/constants/Colors.ts +71 -0
- package/templates/production-fullstack/mobile/src/db/index.ts +9 -0
- package/templates/production-fullstack/mobile/src/db/schema.ts +14 -0
- package/templates/production-fullstack/mobile/src/features/auth/api/auth.service.ts +35 -0
- package/templates/production-fullstack/mobile/src/features/auth/store/useAuthStore.ts +273 -0
- package/templates/production-fullstack/mobile/src/features/auth/types/auth.type.ts +14 -0
- package/templates/production-fullstack/mobile/src/hooks/useDebounce.ts +17 -0
- package/templates/production-fullstack/mobile/src/lib/axios.ts +27 -0
- package/templates/production-fullstack/mobile/src/lib/query-client.ts +11 -0
- package/templates/production-fullstack/mobile/src/lib/socket.ts +36 -0
- package/templates/production-fullstack/mobile/src/lib/storage.ts +55 -0
- package/templates/production-fullstack/mobile/src/services/permission.service.ts +214 -0
- package/templates/production-fullstack/mobile/src/store/localStore.ts +7 -0
- package/templates/production-fullstack/mobile/src/store/useNotificationStore.ts +53 -0
- package/templates/production-fullstack/mobile/src/types/common.type.ts +6 -0
- package/templates/production-fullstack/mobile/src/types/images.d.ts +24 -0
- package/templates/production-fullstack/mobile/src/utils/toast.ts +26 -0
- package/templates/production-fullstack/mobile/tailwind.config.js +71 -0
- package/templates/production-fullstack/mobile/tsconfig.json +23 -0
- package/templates/production-fullstack/server/.env.example +63 -0
- package/templates/production-fullstack/server/README.md +75 -0
- package/templates/production-fullstack/server/_gitignore +24 -0
- package/templates/production-fullstack/server/eslint.config.js +13 -0
- package/templates/production-fullstack/server/package.json +58 -0
- package/templates/production-fullstack/server/prisma/schema/core/attachment.prisma +14 -0
- package/templates/production-fullstack/server/prisma/schema/core/notification.prisma +18 -0
- package/templates/production-fullstack/server/prisma/schema/core/permission.prisma +26 -0
- package/templates/production-fullstack/server/prisma/schema/core/role.prisma +25 -0
- package/templates/production-fullstack/server/prisma/schema/core/user.prisma +56 -0
- package/templates/production-fullstack/server/prisma/schema/enums/common.prisma +13 -0
- package/templates/production-fullstack/server/prisma/schema/enums/notification.prisma +6 -0
- package/templates/production-fullstack/server/prisma/schema/enums/user.prisma +11 -0
- package/templates/production-fullstack/server/prisma/schema/schema.prisma +7 -0
- package/templates/production-fullstack/server/prisma.config.ts +14 -0
- package/templates/production-fullstack/server/src/app.ts +36 -0
- package/templates/production-fullstack/server/src/common/middlewares/auth.middleware.ts +45 -0
- package/templates/production-fullstack/server/src/common/middlewares/errorHandler.ts +16 -0
- package/templates/production-fullstack/server/src/common/middlewares/multer.ts +20 -0
- package/templates/production-fullstack/server/src/common/middlewares/rateLimiter.ts +53 -0
- package/templates/production-fullstack/server/src/common/services/email.service.ts +28 -0
- package/templates/production-fullstack/server/src/common/services/push.service.ts +65 -0
- package/templates/production-fullstack/server/src/common/services/storage.service.ts +9 -0
- package/templates/production-fullstack/server/src/common/types/express.d.ts +10 -0
- package/templates/production-fullstack/server/src/common/utils/cloudinaryUpload.ts +44 -0
- package/templates/production-fullstack/server/src/common/utils/emailOtp.ts +30 -0
- package/templates/production-fullstack/server/src/common/utils/genOtpToken.ts +22 -0
- package/templates/production-fullstack/server/src/common/utils/generateVapidKeys.ts +12 -0
- package/templates/production-fullstack/server/src/common/utils/jwt.ts +27 -0
- package/templates/production-fullstack/server/src/common/utils/pushHelper.ts +22 -0
- package/templates/production-fullstack/server/src/config/env.ts +2 -0
- package/templates/production-fullstack/server/src/config/logger.ts +58 -0
- package/templates/production-fullstack/server/src/config/prisma.ts +26 -0
- package/templates/production-fullstack/server/src/features/attachments/upload.controller.ts +22 -0
- package/templates/production-fullstack/server/src/features/attachments/upload.routes.ts +11 -0
- package/templates/production-fullstack/server/src/features/auth/auth.controller.ts +124 -0
- package/templates/production-fullstack/server/src/features/auth/auth.routes.ts +22 -0
- package/templates/production-fullstack/server/src/features/auth/auth.service.ts +155 -0
- package/templates/production-fullstack/server/src/features/notifications/push.controller.ts +55 -0
- package/templates/production-fullstack/server/src/features/notifications/push.routes.ts +11 -0
- package/templates/production-fullstack/server/src/index.ts +17 -0
- package/templates/production-fullstack/server/src/routes/index.ts +12 -0
- package/templates/production-fullstack/server/src/sockets/auth.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/connection.ts +31 -0
- package/templates/production-fullstack/server/src/sockets/emitters/assignment.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/attendance.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/exam.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/notification.emitter.ts +17 -0
- package/templates/production-fullstack/server/src/sockets/events.ts +27 -0
- package/templates/production-fullstack/server/src/sockets/handlers/attendance.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/chat.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/index.ts +15 -0
- package/templates/production-fullstack/server/src/sockets/handlers/lms.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/notification.handler.ts +39 -0
- package/templates/production-fullstack/server/src/sockets/index.ts +39 -0
- package/templates/production-fullstack/server/src/sockets/middleware.ts +54 -0
- package/templates/production-fullstack/server/src/sockets/rooms.ts +11 -0
- package/templates/production-fullstack/server/src/sockets/types.ts +32 -0
- package/templates/production-fullstack/server/tsconfig.json +14 -0
- package/templates/react/src/assets/animations/loading.json +1 -0
- package/templates/react-native/_gitignore +43 -0
- package/templates/react-native/package.json +2 -1
- package/templates/react-native/src/app/(auth)/edit-profile.tsx +11 -15
- package/templates/react-native/src/app/(tabs)/dashboard.tsx +2 -2
- package/templates/react-native/src/app/(tabs)/search.tsx +2 -2
- package/templates/react-native/src/config/sidebar.config.ts +0 -60
- package/templates/react-native/src/config/tabNavigation.ts +6 -36
- package/templates/react-native/src/services/notification.service.ts +22 -0
- package/templates/react-native/src/store/useNotificationStore.ts +158 -33
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import * as authService from "./auth.service.js";
|
|
3
|
+
import { generateToken } from "../../common/utils/jwt.js";
|
|
4
|
+
import genOtpToken from "../../common/utils/genOtpToken.js";
|
|
5
|
+
import logger from "../../config/logger.js";
|
|
6
|
+
|
|
7
|
+
export const signup = async (req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
try {
|
|
9
|
+
const { name, email, phone, password } = req.body;
|
|
10
|
+
if (!email || !password || !name) {
|
|
11
|
+
return next({ status: 400, message: "All fields are required." });
|
|
12
|
+
}
|
|
13
|
+
const newUser = await authService.signup({ name, email, phone, password });
|
|
14
|
+
const token = await generateToken(newUser, res);
|
|
15
|
+
const safeUser: any = { ...newUser };
|
|
16
|
+
delete safeUser.password;
|
|
17
|
+
res.status(201).json({ message: "Registration Successful", data: safeUser, token });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.error("Error in signup controller", error);
|
|
20
|
+
next(error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const login = async (req: Request, res: Response, next: NextFunction) => {
|
|
25
|
+
try {
|
|
26
|
+
const { email, password } = req.body;
|
|
27
|
+
if (!email || !password) {
|
|
28
|
+
return next({ status: 400, message: "All fields are required." });
|
|
29
|
+
}
|
|
30
|
+
const user = await authService.login({ email, password });
|
|
31
|
+
const token = await generateToken(user, res);
|
|
32
|
+
const safeUser: any = { ...user };
|
|
33
|
+
delete safeUser.password;
|
|
34
|
+
res.status(200).json({ message: "Login successfully", data: safeUser, token });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error("Error in login controller", error);
|
|
37
|
+
next(error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const logout = async (req: Request, res: Response, next: NextFunction) => {
|
|
42
|
+
try {
|
|
43
|
+
res.clearCookie("token", {
|
|
44
|
+
httpOnly: true,
|
|
45
|
+
secure: process.env.NODE_ENV === "production",
|
|
46
|
+
sameSite: "strict",
|
|
47
|
+
path: "/",
|
|
48
|
+
});
|
|
49
|
+
res.status(200).json({ message: "Logout Successfully" });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error("Error in logout controller", error);
|
|
52
|
+
next(error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getMe = async (req: Request, res: Response, next: NextFunction) => {
|
|
57
|
+
try {
|
|
58
|
+
res.status(200).json({ message: "User Authenticated", data: req.user });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error("Error in getMe controller", error);
|
|
61
|
+
next(error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const genOtp = async (req: Request, res: Response, next: NextFunction) => {
|
|
66
|
+
try {
|
|
67
|
+
const { email } = req.body;
|
|
68
|
+
if (!email) {
|
|
69
|
+
return next({ status: 400, message: "Email is required." });
|
|
70
|
+
}
|
|
71
|
+
await authService.generateOtp(email);
|
|
72
|
+
res.status(200).json({ message: "Otp sent successfully" });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error("Error in genOtp", error);
|
|
75
|
+
next(error);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const verifyOtp = async (req: Request, res: Response, next: NextFunction) => {
|
|
80
|
+
try {
|
|
81
|
+
const { email, otp } = req.body;
|
|
82
|
+
if (!email || !otp) {
|
|
83
|
+
return next({ status: 400, message: "All fields are required." });
|
|
84
|
+
}
|
|
85
|
+
const user = await authService.verifyOtp({ email, otp });
|
|
86
|
+
await genOtpToken(user, res);
|
|
87
|
+
res.status(200).json({ message: "Otp verified successfully" });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error("Error in verifyOtp", error);
|
|
90
|
+
next(error);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const resetPassword = async (req: Request, res: Response, next: NextFunction) => {
|
|
95
|
+
try {
|
|
96
|
+
const { newPassword } = req.body;
|
|
97
|
+
const currentUser = req.user;
|
|
98
|
+
if (!newPassword) {
|
|
99
|
+
return next({ status: 400, message: "All fields are required." });
|
|
100
|
+
}
|
|
101
|
+
await authService.resetPassword(currentUser, newPassword);
|
|
102
|
+
res.status(200).json({ message: "Password updated successfully" });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error("Error in resetPassword", error);
|
|
105
|
+
next(error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const updateMe = async (req: Request, res: Response, next: NextFunction) => {
|
|
110
|
+
try {
|
|
111
|
+
const { name, phone } = req.body;
|
|
112
|
+
const user = await authService.updateProfile(req.user.id, {
|
|
113
|
+
name,
|
|
114
|
+
phone,
|
|
115
|
+
file: req.file,
|
|
116
|
+
});
|
|
117
|
+
const safeUser: any = { ...user };
|
|
118
|
+
delete safeUser.password;
|
|
119
|
+
res.status(200).json({ success: true, data: safeUser });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.error("Error updating profile", error);
|
|
122
|
+
next(error);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import {
|
|
3
|
+
signup,
|
|
4
|
+
login,
|
|
5
|
+
getMe,
|
|
6
|
+
logout,
|
|
7
|
+
updateMe,
|
|
8
|
+
} from "./auth.controller.js";
|
|
9
|
+
import { protect } from "../../common/middlewares/auth.middleware.js";
|
|
10
|
+
import { apiLimiter } from "../../common/middlewares/rateLimiter.js";
|
|
11
|
+
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
|
|
14
|
+
router.use(apiLimiter);
|
|
15
|
+
|
|
16
|
+
router.post("/signup", signup);
|
|
17
|
+
router.post("/login", login);
|
|
18
|
+
router.post("/logout", logout);
|
|
19
|
+
router.get("/me", protect, getMe);
|
|
20
|
+
router.put("/me", protect, updateMe);
|
|
21
|
+
|
|
22
|
+
export default router;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import bcrypt from "bcrypt";
|
|
2
|
+
import { prisma } from "../../config/prisma.js";
|
|
3
|
+
import { sendOtpEmail } from "../../common/utils/emailOtp.js";
|
|
4
|
+
import { uploadSingleToCloudinary, deleteFromCloudinary } from "../../common/utils/cloudinaryUpload.js";
|
|
5
|
+
import logger from "../../config/logger.js";
|
|
6
|
+
export const signup = async ({ name, email, phone, password }: any) => {
|
|
7
|
+
const existingUser = await prisma.user.findUnique({
|
|
8
|
+
where: { email },
|
|
9
|
+
});
|
|
10
|
+
if (existingUser) {
|
|
11
|
+
const err: any = new Error("User already exists");
|
|
12
|
+
err.status = 409;
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const salt = await bcrypt.genSalt(10);
|
|
17
|
+
const hashedPassword = await bcrypt.hash(password, salt);
|
|
18
|
+
const photoURL = `https://placehold.co/600x400?text=${name.charAt(0).toUpperCase()}`;
|
|
19
|
+
|
|
20
|
+
const newUser = await prisma.user.create({
|
|
21
|
+
data: {
|
|
22
|
+
name,
|
|
23
|
+
email: email.toLowerCase(),
|
|
24
|
+
phone,
|
|
25
|
+
password: hashedPassword,
|
|
26
|
+
avatarUrl: photoURL,
|
|
27
|
+
avatarPublicId: "",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return newUser;
|
|
32
|
+
}
|
|
33
|
+
export const login = async ({ email, password }: any) => {
|
|
34
|
+
const user = await prisma.user.findUnique({
|
|
35
|
+
where: { email },
|
|
36
|
+
});
|
|
37
|
+
if (!user) {
|
|
38
|
+
const err: any = new Error("User not found");
|
|
39
|
+
err.status = 404;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isValid = await bcrypt.compare(password, user.password);
|
|
44
|
+
if (!isValid) {
|
|
45
|
+
const err: any = new Error("Invalid Credentials");
|
|
46
|
+
err.status = 401;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return user;
|
|
51
|
+
}
|
|
52
|
+
export const generateOtp = async (email: string) => {
|
|
53
|
+
const user = await prisma.user.findUnique({
|
|
54
|
+
where: { email },
|
|
55
|
+
});
|
|
56
|
+
if (!user) {
|
|
57
|
+
const err: any = new Error("User not found");
|
|
58
|
+
err.status = 404;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const existingOtp = await prisma.otp.findUnique({
|
|
63
|
+
where: { email },
|
|
64
|
+
});
|
|
65
|
+
if (existingOtp) {
|
|
66
|
+
await prisma.otp.delete({
|
|
67
|
+
where: { email },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const otp = Math.floor(100000 + Math.random() * 900000).toString();
|
|
72
|
+
const hashedOtp = await bcrypt.hash(otp, 10);
|
|
73
|
+
|
|
74
|
+
await prisma.otp.create({
|
|
75
|
+
data: {
|
|
76
|
+
email,
|
|
77
|
+
otp: hashedOtp,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await sendOtpEmail(email, otp);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
export const verifyOtp = async ({ email, otp }: any) => {
|
|
85
|
+
const user = await prisma.user.findUnique({
|
|
86
|
+
where: { email },
|
|
87
|
+
});
|
|
88
|
+
if (!user) {
|
|
89
|
+
const err: any = new Error("User not found");
|
|
90
|
+
err.status = 404;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const existingOtp = await prisma.otp.findUnique({
|
|
95
|
+
where: { email },
|
|
96
|
+
});
|
|
97
|
+
if (!existingOtp) {
|
|
98
|
+
const err: any = new Error("OTP match failed");
|
|
99
|
+
err.status = 400;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isMatched = await bcrypt.compare(otp, existingOtp.otp);
|
|
104
|
+
if (!isMatched) {
|
|
105
|
+
const err: any = new Error("OTP match failed");
|
|
106
|
+
err.status = 400;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return user;
|
|
111
|
+
}
|
|
112
|
+
export const resetPassword = async (user: any, newPassword: any) => {
|
|
113
|
+
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
114
|
+
await prisma.user.update({
|
|
115
|
+
where: { id: user.id },
|
|
116
|
+
data: { password: hashedPassword },
|
|
117
|
+
});
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
export const updateProfile = async (userId: any, { name, phone, file }: any) => {
|
|
121
|
+
const user = await prisma.user.findUnique({
|
|
122
|
+
where: { id: userId },
|
|
123
|
+
});
|
|
124
|
+
if (!user) {
|
|
125
|
+
const err: any = new Error("User not found");
|
|
126
|
+
err.status = 404;
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const updateData: any = {};
|
|
131
|
+
if (name) updateData.name = name;
|
|
132
|
+
if (phone) updateData.phone = phone;
|
|
133
|
+
|
|
134
|
+
if (file) {
|
|
135
|
+
const uploadResult = await uploadSingleToCloudinary(file, {
|
|
136
|
+
folder: "codelab/avatars",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (user.avatarPublicId) {
|
|
140
|
+
await deleteFromCloudinary(user.avatarPublicId).catch((err) =>
|
|
141
|
+
logger.error("Old avatar delete error", err)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
updateData.avatarUrl = uploadResult.url;
|
|
146
|
+
updateData.avatarPublicId = uploadResult.publicId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const updatedUser = await prisma.user.update({
|
|
150
|
+
where: { id: userId },
|
|
151
|
+
data: updateData,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return updatedUser;
|
|
155
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { prisma } from "../../config/prisma.js";
|
|
3
|
+
import logger from "../../config/logger.js";
|
|
4
|
+
|
|
5
|
+
export const subscribe = async (req: Request, res: Response, next: NextFunction) => {
|
|
6
|
+
try {
|
|
7
|
+
const subscription = req.body;
|
|
8
|
+
const userId = req.user?._id || req.user?.id; // supports both mongoose and prisma IDs
|
|
9
|
+
|
|
10
|
+
if (!subscription || !subscription.endpoint) {
|
|
11
|
+
return next({ status: 400, message: "Invalid subscription payload" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const existingSub = await prisma.pushSubscription.findUnique({ where: { endpoint: subscription.endpoint } });
|
|
15
|
+
|
|
16
|
+
if (existingSub) {
|
|
17
|
+
if (userId) {
|
|
18
|
+
await prisma.pushSubscription.update({ where: { endpoint: subscription.endpoint }, data: { userId } })
|
|
19
|
+
}
|
|
20
|
+
return res.status(200).json({ success: true, message: "Already subscribed." });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
await prisma.pushSubscription.create({
|
|
25
|
+
data: {
|
|
26
|
+
endpoint: subscription.endpoint,
|
|
27
|
+
p256dh: subscription.keys.p256dh,
|
|
28
|
+
auth: subscription.keys.auth,
|
|
29
|
+
userId: userId || null
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
res.status(201).json({ success: true, message: "Subscription created." });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error("Error in push subscribe controller", error);
|
|
37
|
+
next(error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const unsubscribe = async (req: Request, res: Response, next: NextFunction) => {
|
|
42
|
+
try {
|
|
43
|
+
const { endpoint } = req.body;
|
|
44
|
+
if (!endpoint) {
|
|
45
|
+
return next({ status: 400, message: "Endpoint required" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await prisma.pushSubscription.delete({ where: { endpoint } }).catch(() => {})
|
|
49
|
+
|
|
50
|
+
res.status(200).json({ success: true, message: "Unsubscribed." });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logger.error("Error in push unsubscribe controller", error);
|
|
53
|
+
next(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { subscribe, unsubscribe } from "./push.controller.js";
|
|
3
|
+
import { protect } from "../../common/middlewares/auth.middleware.js";
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
// You can remove protect if you want anonymous users to be able to subscribe
|
|
8
|
+
router.post("/subscribe", protect, subscribe);
|
|
9
|
+
router.post("/unsubscribe", unsubscribe);
|
|
10
|
+
|
|
11
|
+
export default router;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import app from "./app.js";
|
|
4
|
+
import connectDB from "./config/prisma.js";
|
|
5
|
+
import logger from "./config/logger.js";
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
const PORT = process.env.PORT || 5000;
|
|
10
|
+
|
|
11
|
+
connectDB();
|
|
12
|
+
|
|
13
|
+
const server = http.createServer(app);
|
|
14
|
+
|
|
15
|
+
server.listen(PORT, () => {
|
|
16
|
+
logger.success(`Server is running on port ${PORT}`);
|
|
17
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import authRoutes from "../features/auth/auth.routes.js";
|
|
3
|
+
import uploadRoutes from "../features/attachments/upload.routes.js";
|
|
4
|
+
import pushRoutes from "../features/notifications/push.routes.js";
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
router.use("/auth", authRoutes);
|
|
9
|
+
router.use("/upload", uploadRoutes);
|
|
10
|
+
router.use("/push", pushRoutes);
|
|
11
|
+
|
|
12
|
+
export default router;
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Socket, Server } from "socket.io";
|
|
2
|
+
import logger from "../config/logger.js";
|
|
3
|
+
import { registerHandlers } from "./handlers/index.js";
|
|
4
|
+
import { getUserRoom, getInstitutionRoom } from "./rooms.js";
|
|
5
|
+
import { SocketEvents } from "./events.js";
|
|
6
|
+
import { SocketData, ServerToClientEvents, ClientToServerEvents, InterServerEvents } from "./types.js";
|
|
7
|
+
|
|
8
|
+
type AppSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
9
|
+
type AppServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
10
|
+
|
|
11
|
+
export const onConnection = (io: AppServer, socket: AppSocket) => {
|
|
12
|
+
const user = socket.data.user;
|
|
13
|
+
|
|
14
|
+
logger.info(`Socket connected: ${socket.id} (User: ${user.id})`);
|
|
15
|
+
|
|
16
|
+
// Auto-join user-specific room
|
|
17
|
+
socket.join(getUserRoom(user.id));
|
|
18
|
+
|
|
19
|
+
// Auto-join institution room if applicable
|
|
20
|
+
if (user.institutionId) {
|
|
21
|
+
socket.join(getInstitutionRoom(user.institutionId));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Register all event handlers
|
|
25
|
+
registerHandlers(io, socket);
|
|
26
|
+
|
|
27
|
+
// Handle disconnect
|
|
28
|
+
socket.on(SocketEvents.DISCONNECT, (reason) => {
|
|
29
|
+
logger.info(`Socket disconnected: ${socket.id} (User: ${user.id}, Reason: ${reason})`);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getIO } from "../index.js";
|
|
2
|
+
import { getUserRoom } from "../rooms.js";
|
|
3
|
+
import { SocketEvents } from "../events.js";
|
|
4
|
+
import logger from "../../config/logger.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emits a new notification to a specific user.
|
|
8
|
+
* This is meant to be called from business logic services.
|
|
9
|
+
*/
|
|
10
|
+
export const emitNewNotification = (userId: string, payload: unknown) => {
|
|
11
|
+
try {
|
|
12
|
+
const io = getIO();
|
|
13
|
+
io.to(getUserRoom(userId)).emit(SocketEvents.NOTIFICATION_NEW, payload);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
logger.error(`Failed to emit notification to user ${userId}`, error);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const SocketEvents = {
|
|
2
|
+
// Connection Events
|
|
3
|
+
DISCONNECT: "disconnect",
|
|
4
|
+
CONNECT_ERROR: "connect_error",
|
|
5
|
+
|
|
6
|
+
// Room Events
|
|
7
|
+
SUBSCRIBE: "room:subscribe",
|
|
8
|
+
UNSUBSCRIBE: "room:unsubscribe",
|
|
9
|
+
|
|
10
|
+
// Notification Events
|
|
11
|
+
NOTIFICATION_NEW: "notification:new",
|
|
12
|
+
NOTIFICATION_MARK_AS_READ: "notification:markAsRead",
|
|
13
|
+
|
|
14
|
+
// Chat Events
|
|
15
|
+
CHAT_NEW_MESSAGE: "chat:newMessage",
|
|
16
|
+
CHAT_TYPING: "chat:typing",
|
|
17
|
+
CHAT_STOP_TYPING: "chat:stopTyping",
|
|
18
|
+
|
|
19
|
+
// LMS Events
|
|
20
|
+
LMS_ASSIGNMENT_CREATED: "lms:assignmentCreated",
|
|
21
|
+
LMS_GRADE_PUBLISHED: "lms:gradePublished",
|
|
22
|
+
|
|
23
|
+
// Attendance Events
|
|
24
|
+
ATTENDANCE_MARKED: "attendance:marked",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type SocketEvent = (typeof SocketEvents)[keyof typeof SocketEvents];
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Socket, Server } from "socket.io";
|
|
2
|
+
import { registerNotificationHandlers } from "./notification.handler.js";
|
|
3
|
+
import { SocketData, ServerToClientEvents, ClientToServerEvents, InterServerEvents } from "../types.js";
|
|
4
|
+
|
|
5
|
+
type AppSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
6
|
+
type AppServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
7
|
+
|
|
8
|
+
export const registerHandlers = (io: AppServer, socket: AppSocket) => {
|
|
9
|
+
// Register domain specific handlers
|
|
10
|
+
registerNotificationHandlers(io, socket);
|
|
11
|
+
|
|
12
|
+
// Future handlers:
|
|
13
|
+
// registerChatHandlers(io, socket);
|
|
14
|
+
// registerLmsHandlers(io, socket);
|
|
15
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Socket, Server } from "socket.io";
|
|
2
|
+
import logger from "../../config/logger.js";
|
|
3
|
+
import { SocketEvents } from "../events.js";
|
|
4
|
+
import { SocketData, ServerToClientEvents, ClientToServerEvents, InterServerEvents } from "../types.js";
|
|
5
|
+
|
|
6
|
+
type AppSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
7
|
+
type AppServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
8
|
+
|
|
9
|
+
export const registerNotificationHandlers = (_io: AppServer, socket: AppSocket) => {
|
|
10
|
+
socket.on(SocketEvents.NOTIFICATION_MARK_AS_READ, (payload, callback) => {
|
|
11
|
+
try {
|
|
12
|
+
const { notificationId } = payload;
|
|
13
|
+
logger.info(`Notification marked as read: ${notificationId} by user ${socket.data.user.id}`);
|
|
14
|
+
|
|
15
|
+
if (typeof callback === "function") {
|
|
16
|
+
callback({ success: true, notificationId });
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.error("Error in notification:markAsRead handler", error);
|
|
20
|
+
if (typeof callback === "function") {
|
|
21
|
+
callback({ success: false, error: "Failed to mark as read" });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
socket.on(SocketEvents.SUBSCRIBE, (payload) => {
|
|
27
|
+
if (payload?.topic) {
|
|
28
|
+
socket.join(`topic:${payload.topic}`);
|
|
29
|
+
logger.info(`User ${socket.data.user.id} subscribed to topic: ${payload.topic}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
socket.on(SocketEvents.UNSUBSCRIBE, (payload) => {
|
|
34
|
+
if (payload?.topic) {
|
|
35
|
+
socket.leave(`topic:${payload.topic}`);
|
|
36
|
+
logger.info(`User ${socket.data.user.id} unsubscribed from topic: ${payload.topic}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Server } from "socket.io";
|
|
2
|
+
import { socketAuthMiddleware } from "./middleware.js";
|
|
3
|
+
import { onConnection } from "./connection.js";
|
|
4
|
+
import { SocketData, ServerToClientEvents, ClientToServerEvents, InterServerEvents } from "./types.js";
|
|
5
|
+
|
|
6
|
+
type AppServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
7
|
+
|
|
8
|
+
let io: AppServer;
|
|
9
|
+
|
|
10
|
+
const initializeSocket = (server: import("http").Server) => {
|
|
11
|
+
io = new Server(server, {
|
|
12
|
+
cors: {
|
|
13
|
+
origin: ["http://localhost:5173"],
|
|
14
|
+
credentials: true,
|
|
15
|
+
},
|
|
16
|
+
// To support Redis in the future, you would add the adapter here:
|
|
17
|
+
// adapter: createAdapter(pubClient, subClient)
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Apply Authentication Middleware
|
|
21
|
+
io.use(socketAuthMiddleware);
|
|
22
|
+
|
|
23
|
+
// Handle Connections
|
|
24
|
+
io.on("connection", (socket) => {
|
|
25
|
+
onConnection(io, socket);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return io;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getIO = (): AppServer => {
|
|
32
|
+
if (!io) {
|
|
33
|
+
throw new Error("Socket.IO has not been initialized yet");
|
|
34
|
+
}
|
|
35
|
+
return io;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { io };
|
|
39
|
+
export default initializeSocket;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Socket } from "socket.io";
|
|
2
|
+
import { verifyToken } from "../common/utils/jwt.js";
|
|
3
|
+
import { SocketData, ServerToClientEvents, ClientToServerEvents, InterServerEvents } from "./types.js";
|
|
4
|
+
import { prisma } from "../config/prisma.js";
|
|
5
|
+
|
|
6
|
+
type AppSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
|
7
|
+
|
|
8
|
+
const parseCookie = (cookieString: string, name: string) => {
|
|
9
|
+
const match = cookieString.match(new RegExp("(^| )" + name + "=([^;]+)"));
|
|
10
|
+
if (match) return match[2];
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const socketAuthMiddleware = async (socket: AppSocket, next: (err?: Error) => void) => {
|
|
15
|
+
try {
|
|
16
|
+
let token = socket.handshake.auth.token;
|
|
17
|
+
|
|
18
|
+
// Fallback to cookie if token is not explicitly passed in handshake
|
|
19
|
+
if (!token && socket.handshake.headers.cookie) {
|
|
20
|
+
token = parseCookie(socket.handshake.headers.cookie, "token");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!token) {
|
|
24
|
+
return next(new Error("Authentication error: No token provided"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const decoded: any = verifyToken(token);
|
|
29
|
+
|
|
30
|
+
if (!decoded || !decoded.id) {
|
|
31
|
+
return next(new Error("Authentication error: Invalid token"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const user = await prisma.user.findUnique({
|
|
35
|
+
where: { id: decoded.id },
|
|
36
|
+
select: { id: true, roles: true, institutionId: true },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!user) {
|
|
40
|
+
return next(new Error("Authentication error: User not found"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Attach user to socket data
|
|
44
|
+
socket.data.user = {
|
|
45
|
+
id: user.id,
|
|
46
|
+
roles: user.roles,
|
|
47
|
+
institutionId: user.institutionId,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
next();
|
|
51
|
+
} catch {
|
|
52
|
+
next(new Error("Authentication error: Token verification failed"));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room generator functions.
|
|
3
|
+
* Using a consistent namespace pattern (e.g. user:<id>) ensures
|
|
4
|
+
* that rooms don't overlap and can be easily managed or targeted.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const getUserRoom = (userId: string) => `user:${userId}`;
|
|
8
|
+
export const getInstitutionRoom = (institutionId: string) => `institution:${institutionId}`;
|
|
9
|
+
export const getClassRoom = (classId: string) => `class:${classId}`;
|
|
10
|
+
export const getSectionRoom = (sectionId: string) => `section:${sectionId}`;
|
|
11
|
+
export const getTopicRoom = (topic: string) => `topic:${topic}`;
|