@imksh/create 1.0.25 → 1.0.26
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/_gitignore +0 -24
- 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/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/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/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/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-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
- /package/templates/{react-native → production-fullstack/mobile}/expo-env.d.ts +0 -0
|
@@ -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}`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SocketEvents } from "./events.js";
|
|
2
|
+
|
|
3
|
+
// Basic user info injected into socket.data by the auth middleware
|
|
4
|
+
export interface SocketUser {
|
|
5
|
+
id: string;
|
|
6
|
+
roles?: { role: { name: string } }[] | unknown;
|
|
7
|
+
institutionId?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SocketData {
|
|
11
|
+
user: SocketUser;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ServerToClientEvents {
|
|
15
|
+
[SocketEvents.NOTIFICATION_NEW]: (payload: unknown) => void;
|
|
16
|
+
[SocketEvents.CHAT_NEW_MESSAGE]: (payload: unknown) => void;
|
|
17
|
+
[SocketEvents.LMS_ASSIGNMENT_CREATED]: (payload: unknown) => void;
|
|
18
|
+
[SocketEvents.ATTENDANCE_MARKED]: (payload: unknown) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ClientToServerEvents {
|
|
22
|
+
[SocketEvents.SUBSCRIBE]: (payload: { topic: string }) => void;
|
|
23
|
+
[SocketEvents.UNSUBSCRIBE]: (payload: { topic: string }) => void;
|
|
24
|
+
[SocketEvents.NOTIFICATION_MARK_AS_READ]: (
|
|
25
|
+
payload: { notificationId: string },
|
|
26
|
+
callback: (response: { success: boolean; notificationId?: string; error?: string }) => void
|
|
27
|
+
) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InterServerEvents {
|
|
31
|
+
ping: () => void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "shivAi",
|
|
3
3
|
"main": "expo-router/entry",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"dependencies": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"expo-font": "~57.0.1",
|
|
20
20
|
"expo-glass-effect": "~57.0.1",
|
|
21
21
|
"expo-image": "~57.0.1",
|
|
22
|
+
"expo-image-picker": "^57.0.6",
|
|
22
23
|
"expo-linear-gradient": "^57.0.1",
|
|
23
24
|
"expo-linking": "~57.0.3",
|
|
24
25
|
"expo-notifications": "~57.0.6",
|
|
@@ -3,8 +3,8 @@ import { View, TouchableOpacity } from "react-native";
|
|
|
3
3
|
import { useAuthStore } from "../../store/useAuthStore";
|
|
4
4
|
import { Txt } from "../../components/common/Typography";
|
|
5
5
|
import { router, useFocusEffect } from "expo-router";
|
|
6
|
-
import * as ImagePicker from "expo-image-picker";
|
|
7
6
|
import { Ionicons } from "@expo/vector-icons";
|
|
7
|
+
import ImagePickerModal from "../../components/ui/inputs/ImagePickerModal";
|
|
8
8
|
import Avatar from "../../components/ui/data-display/Avatar";
|
|
9
9
|
|
|
10
10
|
import ScrollContainer from "../../components/ui/layout/ScrollContainer";
|
|
@@ -27,19 +27,7 @@ export default function EditProfileScreen() {
|
|
|
27
27
|
const [name, setName] = useState(user?.name || "");
|
|
28
28
|
const [phone, setPhone] = useState(user?.phone || "");
|
|
29
29
|
const [avatarUri, setAvatarUri] = useState<string | null>(null);
|
|
30
|
-
|
|
31
|
-
const pickImage = async () => {
|
|
32
|
-
const result = await ImagePicker.launchImageLibraryAsync({
|
|
33
|
-
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
34
|
-
allowsEditing: true,
|
|
35
|
-
aspect: [1, 1],
|
|
36
|
-
quality: 0.8,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
if (!result.canceled && result.assets[0]) {
|
|
40
|
-
setAvatarUri(result.assets[0].uri);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
30
|
+
const [isImagePickerOpen, setIsImagePickerOpen] = useState(false);
|
|
43
31
|
|
|
44
32
|
const handleSave = async () => {
|
|
45
33
|
const formData = new FormData();
|
|
@@ -112,7 +100,7 @@ export default function EditProfileScreen() {
|
|
|
112
100
|
)}
|
|
113
101
|
</View>
|
|
114
102
|
<TouchableOpacity
|
|
115
|
-
onPress={
|
|
103
|
+
onPress={() => setIsImagePickerOpen(true)}
|
|
116
104
|
className="absolute bottom-0 right-0 bg-primary p-2 rounded-full border-2 border-base-100"
|
|
117
105
|
>
|
|
118
106
|
<Ionicons name="camera" size={18} color="#fff" />
|
|
@@ -155,6 +143,14 @@ export default function EditProfileScreen() {
|
|
|
155
143
|
</View>
|
|
156
144
|
</View>
|
|
157
145
|
</ScrollContainer>
|
|
146
|
+
<ImagePickerModal
|
|
147
|
+
isOpen={isImagePickerOpen}
|
|
148
|
+
onClose={() => setIsImagePickerOpen(false)}
|
|
149
|
+
onImageSelected={(image) => setAvatarUri(image.uri)}
|
|
150
|
+
allowsEditing
|
|
151
|
+
title="Profile Picture"
|
|
152
|
+
subtitle="Choose a new profile picture"
|
|
153
|
+
/>
|
|
158
154
|
</>
|
|
159
155
|
);
|
|
160
156
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import
|
|
3
|
+
import TabHeader from "@/components/common/TabHeader";
|
|
4
4
|
|
|
5
5
|
const dashboard = () => {
|
|
6
6
|
return (
|
|
7
7
|
<View>
|
|
8
|
-
<
|
|
8
|
+
<TabHeader title="Dashboard" showMenuButton={false} />
|
|
9
9
|
</View>
|
|
10
10
|
);
|
|
11
11
|
};
|