From eefae6104e3d89322ec24669c84de115e45fa535 Mon Sep 17 00:00:00 2001 From: yznahmad Date: Thu, 3 Jul 2025 01:40:14 +0300 Subject: [PATCH] Add v1 12d20 2 --- docker-compose.yml | 28 ++- webapp/-.gitignore | 2 +- webapp/.env.local | 12 +- webapp/API_DOCUMENTATION.md | 155 +++++++++++++ webapp/Dockerfile | 56 ++--- webapp/next.config.js | 1 - webapp/package-lock.json | 218 ++++++++++++++++++ webapp/package.json | 2 + webapp/src/app/api/member-lookup/route.ts | 109 +++++++++ .../app/api/user/actions/statistics/route.ts | 4 +- .../dashboard/members/parts/detailsPopUp.tsx | 80 ++++++- webapp/src/messages/ar.json | 6 +- webapp/src/messages/en.json | 6 +- webapp/src/middleware/validateAuthToken.ts | 2 - webapp/src/utils/encryption.ts | 41 ++++ webapp/test-api.js | 64 +++++ 16 files changed, 735 insertions(+), 51 deletions(-) create mode 100644 webapp/API_DOCUMENTATION.md create mode 100644 webapp/src/app/api/member-lookup/route.ts create mode 100644 webapp/src/utils/encryption.ts create mode 100644 webapp/test-api.js diff --git a/docker-compose.yml b/docker-compose.yml index 48276cd..aeee7d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ +version: '3.8' + services: # MongoDB Service mongodb: image: mongo:6.0 container_name: infinity-mongodb - restart: always + restart: unless-stopped environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} @@ -19,26 +21,34 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s # Web Application (Next.js) webapp: build: context: . dockerfile: webapp/Dockerfile + target: runner container_name: infinity-webapp - restart: always + restart: unless-stopped depends_on: mongodb: condition: service_healthy environment: - - NODE_ENV=${NODE_ENV} + - NODE_ENV=${NODE_ENV:-production} - DB_URI=${DB_URI} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} env_file: .env ports: - - "8081:3000" # Let Docker assign a dynamic host port + - "${APP_PORT:-8081}:3000" networks: - infinity-network + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s # Worker Service worker: @@ -46,11 +56,12 @@ services: context: . dockerfile: worker/Dockerfile container_name: infinity-worker - restart: always + restart: unless-stopped depends_on: mongodb: condition: service_healthy environment: + - NODE_ENV=${NODE_ENV:-production} - DB_URI=${DB_URI} env_file: .env networks: @@ -69,16 +80,19 @@ services: - DB_URI=${DB_URI} - ADMIN_USERNAME=${ADMIN_USERNAME} - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - NODE_ENV=${NODE_ENV:-production} env_file: .env networks: - infinity-network - # This ensures the container exits after creating the admin account command: sh -c "python create_account.py" + profiles: ["create-admin"] networks: infinity-network: driver: bridge + name: infinity-network volumes: mongodb_data: - driver: local \ No newline at end of file + driver: local + name: infinity-mongodb-data \ No newline at end of file diff --git a/webapp/-.gitignore b/webapp/-.gitignore index 754c712..8f322f0 100644 --- a/webapp/-.gitignore +++ b/webapp/-.gitignore @@ -25,7 +25,7 @@ yarn-debug.log* yarn-error.log* # local env files -#.env*.local +.env*.local # vercel .vercel diff --git a/webapp/.env.local b/webapp/.env.local index dafbc9e..492c608 100644 --- a/webapp/.env.local +++ b/webapp/.env.local @@ -1,8 +1,8 @@ - -MONGO_INITDB_ROOT_USERNAME=your_username -MONGO_INITDB_ROOT_PASSWORD=your_secure_password +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=mongodb_str_p%40ss MONGO_INITDB_DATABASE=Infinity -# NEXT_PUBLIC_API_URL=https://irongym.yznapps.com:3000 +DB_URI=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@localhost:27017/Infinity?authSource=admin NEXT_PUBLIC_API_BASE=http://localhost:3000 -NODE_ENV=production -DB_URI=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017/Infinity?authSource=admin + +# ADMIN_USERNAME=admin +# ADMIN_PASSWORD=your_secure_admin_password \ No newline at end of file diff --git a/webapp/API_DOCUMENTATION.md b/webapp/API_DOCUMENTATION.md new file mode 100644 index 0000000..3bfba97 --- /dev/null +++ b/webapp/API_DOCUMENTATION.md @@ -0,0 +1,155 @@ +# Member Lookup API Documentation + +## Overview +This API endpoint allows secure lookup of member information using encrypted member IDs. It's designed to be used with an Expo Android app that scans QR codes containing encrypted member IDs. + +## Endpoint +``` +GET /api/member-lookup?encryptedId={encrypted_member_id} +``` + +## Security +- Member IDs are encrypted using AES-256-CBC encryption +- QR codes contain encrypted member IDs, not plain text IDs +- The API validates and decrypts the member ID before database lookup + +## Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| encryptedId | string | Yes | The encrypted member ID obtained from QR code | + +## Response Format + +### Success Response (200) +```json +{ + "success": true, + "message": "Member information retrieved successfully", + "data": { + "name": "John Doe", + "gender": "m", + "planDelay": 1, + "planStart": "Mon, 01 Jan 2024 00:00:00 GMT", + "planStatus": "active", + "planExpAt": "Thu, 01 Feb 2024 00:00:00 GMT" + } +} +``` + +### Error Responses + +#### Missing Parameter (400) +```json +{ + "success": false, + "message": "Missing encrypted member ID" +} +``` + +#### Invalid Encryption (400) +```json +{ + "success": false, + "message": "Invalid encrypted member ID" +} +``` + +#### Member Not Found (404) +```json +{ + "success": false, + "message": "Member not found" +} +``` + +#### Server Error (500) +```json +{ + "success": false, + "message": "Server error" +} +``` + +## Response Data Fields + +| Field | Type | Description | +|-------|------|-------------| +| name | string | Full name (firstName + lastName) | +| gender | string | Gender ("m" for male, "f" for female) | +| planDelay | number | Plan duration in months | +| planStart | string | Plan start date (UTC string) | +| planStatus | string | "active" or "expired" | +| planExpAt | string | Plan expiration date (UTC string) | + +## Usage with Android App + +### 1. QR Code Scanning +- Scan the QR code from the member details popup +- Extract the encrypted member ID from the QR code data + +### 2. API Call +```javascript +// Example using fetch in React Native/Expo +const lookupMember = async (encryptedId) => { + try { + const response = await fetch( + `https://your-domain.com/api/member-lookup?encryptedId=${encodeURIComponent(encryptedId)}` + ); + const data = await response.json(); + + if (data.success) { + // Display member information in card view + return data.data; + } else { + // Handle error + console.error('API Error:', data.message); + } + } catch (error) { + console.error('Network Error:', error); + } +}; +``` + +### 3. Display in Card View +Use the returned data to populate your Android card view with: +- Member name +- Gender +- Plan information (duration, start date, expiration) +- Plan status (active/expired with appropriate styling) + +## Security Considerations + +1. **Encryption Key**: Ensure the `ENCRYPTION_KEY` environment variable is set to a secure 32-character string in production +2. **HTTPS**: Always use HTTPS in production to protect data in transit +3. **Rate Limiting**: Consider implementing rate limiting to prevent abuse +4. **Authentication**: For additional security, consider adding API authentication + +## Environment Variables + +```env +ENCRYPTION_KEY=your-32-character-secret-key-here! +``` + +## Files Modified/Created + +1. **Created**: `src/utils/encryption.ts` - Encryption/decryption utilities +2. **Created**: `src/app/api/member-lookup/route.ts` - API endpoint +3. **Modified**: `src/components/dashboard/members/parts/detailsPopUp.tsx` - QR code generation with encryption +4. **Modified**: `src/messages/en.json` - English translations +5. **Modified**: `src/messages/ar.json` - Arabic translations + +## Testing + +1. Start the development server: `npm run dev` +2. Open a member details popup to generate a QR code +3. The QR code now contains an encrypted member ID +4. Test the API endpoint with the encrypted ID + +## Example QR Code Flow + +1. **Web App**: Generates QR code with encrypted member ID +2. **Android App**: Scans QR code and extracts encrypted ID +3. **Android App**: Calls `/api/member-lookup?encryptedId=...` +4. **API**: Decrypts ID, looks up member, returns information +5. **Android App**: Displays member information in card view \ No newline at end of file diff --git a/webapp/Dockerfile b/webapp/Dockerfile index ef38766..236e9c7 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,55 +1,55 @@ -# Stage 1: Build the application +# Stage 1: Dependencies FROM node:18-alpine AS deps -RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies -COPY webapp/package.json webapp/package-lock.json ./ +COPY package.json package-lock.json ./ RUN npm ci -# Copy the rest of the application -COPY webapp/ . +# Copy source code +COPY . . -# Set environment variables for build -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 -# Use a dummy DB_URI during build to prevent connection attempts -ENV DB_URI=mongodb://dummy:password@localhost:27017/dummy +# Stage 2: Builder +FROM node:18-alpine AS builder +WORKDIR /app + +# Copy dependencies and source code +COPY --from=deps /app/node_modules ./node_modules +COPY . . # Build the application +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build -# Stage 2: Create the runner image +# Stage 3: Runner FROM node:18-alpine AS runner WORKDIR /app -# Install dependencies only needed for production +# Install runtime dependencies RUN apk add --no-cache dumb-init -# Create a non-root user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs -# Copy the built application from the builder stage -COPY --from=deps /app/next.config.js ./ -COPY --from=deps /app/public ./public -COPY --from=deps /app/package.json ./ +# Copy necessary files from builder +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public -# Copy the standalone server and static files -COPY --from=deps --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=deps --chown=nextjs:nodejs /app/.next/static ./.next/static +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 -# Set the user to non-root +# Use non-root user USER nextjs -# Expose the port the app runs on +# Expose port EXPOSE 3000 -# Set the environment variable for the port -ENV PORT 3000 - # Use dumb-init to handle signals properly ENTRYPOINT ["/usr/bin/dumb-init", "--"] -# Set the command to run the application +# Start the application CMD ["node", "server.js"] diff --git a/webapp/next.config.js b/webapp/next.config.js index 83ac05c..aa397ec 100644 --- a/webapp/next.config.js +++ b/webapp/next.config.js @@ -1,6 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', typescript: { ignoreBuildErrors: true, }, diff --git a/webapp/package-lock.json b/webapp/package-lock.json index f76bd78..cc5e61c 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -23,6 +23,7 @@ "@tippyjs/react": "^4.2.6", "@types/bcrypt": "^5.0.0", "@types/node": "20.4.4", + "@types/qrcode": "^1.5.5", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "apexcharts": "^3.42.0", @@ -37,6 +38,7 @@ "next": "^13.4.12", "next-intl": "^2.19.0", "postcss": "8.4.27", + "qrcode": "^1.5.4", "react": "^18.2.0", "react-animate-height": "^3.2.2", "react-apexcharts": "^1.4.1", @@ -1616,6 +1618,15 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/quill": { "version": "1.3.10", "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", @@ -2307,6 +2318,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2407,6 +2427,17 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -2580,6 +2611,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -2715,6 +2755,12 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3680,6 +3726,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -5366,6 +5421,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parchment": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", @@ -5473,6 +5537,15 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.4.27", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", @@ -5633,6 +5706,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5955,6 +6045,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", @@ -7207,6 +7312,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -7233,11 +7344,31 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -7251,6 +7382,93 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index 8733cff..aa37e63 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -24,6 +24,7 @@ "@tippyjs/react": "^4.2.6", "@types/bcrypt": "^5.0.0", "@types/node": "20.4.4", + "@types/qrcode": "^1.5.5", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "apexcharts": "^3.42.0", @@ -38,6 +39,7 @@ "next": "^13.4.12", "next-intl": "^2.19.0", "postcss": "8.4.27", + "qrcode": "^1.5.4", "react": "^18.2.0", "react-animate-height": "^3.2.2", "react-apexcharts": "^1.4.1", diff --git a/webapp/src/app/api/member-lookup/route.ts b/webapp/src/app/api/member-lookup/route.ts new file mode 100644 index 0000000..577e850 --- /dev/null +++ b/webapp/src/app/api/member-lookup/route.ts @@ -0,0 +1,109 @@ +/** + * @description API route for encrypted member lookup + * This endpoint receives an encrypted member ID and returns member information + * for the Expo Android app + */ + +import dbConnect from "@/database/dbConnect"; +import { NextResponse } from "next/server"; +import memberModel from "@/database/models/memberModel"; +import { decryptMemberId } from "@/utils/encryption"; + +// GET METHOD - Lookup member by encrypted ID +export async function GET(req: Request) { + try { + // Connect to the database + await dbConnect(); + + // Get the encrypted member ID from query parameters + const { searchParams } = new URL(req.url); + const encryptedId = searchParams.get('encryptedId'); + + // Validate the encrypted ID parameter + if (!encryptedId) { + return NextResponse.json({ + success: false, + message: "Missing encrypted member ID", + }, { + status: 400, + headers: { + "content-type": "application/json" + } + }); + } + + // Decrypt the member ID + let memberId: string; + try { + memberId = decryptMemberId(encryptedId); + } catch (error) { + return NextResponse.json({ + success: false, + message: "Invalid encrypted member ID", + }, { + status: 400, + headers: { + "content-type": "application/json" + } + }); + } + + // Find the member by ID + const member = await memberModel.findById(memberId); + + if (!member) { + return NextResponse.json({ + success: false, + message: "Member not found", + }, { + status: 404, + headers: { + "content-type": "application/json" + } + }); + } + + // Calculate plan status (active/expired) + const currentTime = Math.floor(Date.now() / 1000); + const planStatus = member.planExpAt_unix > currentTime ? 'active' : 'expired'; + + // Prepare the response data with only the required fields + const memberInfo = { + name: `${member.firstName} ${member.lastName}`, + gender: member.gendre, + planDelay: member.planDelay, + planStart: member.planUpdatedAt, + planStatus: planStatus, + planExpAt: member.planExpAt + }; + + // Return the member information + return NextResponse.json({ + success: true, + message: "Member information retrieved successfully", + data: memberInfo, + }, { + status: 200, + headers: { + "content-type": "application/json" + } + }); + + } catch (error) { + console.error('Error in member lookup API:', error); + + // Return server error response + return NextResponse.json({ + success: false, + message: "Server error", + }, { + status: 500, + headers: { + "content-type": "application/json" + } + }); + } +} + +// Set revalidation time +export const revalidate = 5; \ No newline at end of file diff --git a/webapp/src/app/api/user/actions/statistics/route.ts b/webapp/src/app/api/user/actions/statistics/route.ts index c373913..d433e8d 100644 --- a/webapp/src/app/api/user/actions/statistics/route.ts +++ b/webapp/src/app/api/user/actions/statistics/route.ts @@ -152,7 +152,7 @@ async function updateMembersOverviewStatistics() { registerAt_unix: { $lt: endOfDayUnix } }); - daysInWeek[weekDaysOrder[i]] = { + (daysInWeek as {[key: string]: any})[weekDaysOrder[i]] = { totalMembers, totalActiveSubs, totalUnActiveSubs, @@ -253,7 +253,7 @@ async function updateMembersOverviewStatistics() { registerAt_unix: { $lt: endOfDayUnix } }); - daysInMonthlyWeekForYear[weekDaysOrder[i]] = { + (daysInMonthlyWeekForYear as {[key: string]: any})[weekDaysOrder[i]] = { totalMembers, totalActiveSubs, totalUnActiveSubs, diff --git a/webapp/src/components/dashboard/members/parts/detailsPopUp.tsx b/webapp/src/components/dashboard/members/parts/detailsPopUp.tsx index c336aef..ec86c2e 100644 --- a/webapp/src/components/dashboard/members/parts/detailsPopUp.tsx +++ b/webapp/src/components/dashboard/members/parts/detailsPopUp.tsx @@ -10,15 +10,17 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import Slide from '@mui/material/Slide'; import { TransitionProps } from '@mui/material/transitions'; import { Formik } from 'formik'; +import { useRef } from 'react'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; import DialogActions from '@mui/material/DialogActions'; +import { encryptMemberId } from '@/utils/encryption'; export default function ServiceDetailsPopUp() { // declare the needed variables // redux - const dispatch = useDispatch(); + const dispatch = useDispatch(); // intl-18 const t = useTranslations('members'); // mui @@ -43,6 +45,31 @@ export default function ServiceDetailsPopUp() data: null })); }; + + // QR Code generation function using QR Server API with encrypted member ID + const generateQRCode = (memberId: string) => { + const encryptedId = encryptMemberId(memberId); + return `https://api.qrserver.com/v1/create-qr-code/?size=360x360&data=${encodeURIComponent(encryptedId)}`; + }; + + // Download QR code function + const downloadQRCode = async (memberId: string) => { + try { + const qrUrl = generateQRCode(memberId); + const response = await fetch(qrUrl); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `member-${memberId}-qr.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading QR code:', error); + } + }; // we dont mount the pop up if we dont need it if(!detailsPopUp) return <> // convert unix into read able date @@ -238,7 +265,56 @@ export default function ServiceDetailsPopUp()
-

{t('registerAt') + ' ' + UnixToReadAbleDate(parseInt(detailsPopUpData?.registerAt_unix))}

+

+ {t('registerAt') + ' ' + '[ ' + + new Date(detailsPopUpData?.registerAt || '').toLocaleDateString( + t('locale') === 'ar' ? 'ar-EG' : 'en-US', + { + weekday: 'long', + day: 'numeric', + month: 'numeric', + year: 'numeric', + calendar: t('locale') === 'ar' ? 'gregory' : undefined + } + ) + + ' ] '} +

+
+ + {/* QR Code Section */} +
+

{t('memberQRCode') || 'Member QR Code'}

+
+
+

+ {t('qrCodeDescription') || 'Scan this QR code to quickly access member information'} +

+ Member QR Code +
+
+ +

+ {t('qrDownloadNote') || 'Downloads as PNG image'} +

+
+
diff --git a/webapp/src/messages/ar.json b/webapp/src/messages/ar.json index 6093c59..625f14d 100644 --- a/webapp/src/messages/ar.json +++ b/webapp/src/messages/ar.json @@ -140,7 +140,11 @@ "BodyStateInformations": "معلومات عن حالة الجسم", "planDetails": "معلومات عن الاشتراك", "currentWeight": "الوزن الحالي", - "currentBodyForm": "حالة الجسم الحالية" + "currentBodyForm": "حالة الجسم الحالية", + "memberQRCode": "رمز QR للعضو", + "qrCodeDescription": "امسح رمز الاستجابة السريعة هذا للوصول السريع إلى معلومات العضو:", + "downloadQR": "تحميل رمز QR", + "qrDownloadNote": "يتم التحميل كصورة PNG" }, "workers": { "workers": "العمال", diff --git a/webapp/src/messages/en.json b/webapp/src/messages/en.json index f207388..2dad4d0 100644 --- a/webapp/src/messages/en.json +++ b/webapp/src/messages/en.json @@ -144,7 +144,11 @@ "BodyStateInformations": "Body state informations", "planDetails": "Plan details", "currentWeight": "Current weight", - "currentBodyForm": "Current body form" + "currentBodyForm": "Current body form", + "memberQRCode": "Member QR Code", + "qrCodeDescription": "Scan this QR code to quickly access member information:", + "downloadQR": "Download QR Code", + "qrDownloadNote": "Downloads as PNG image" }, "workers": { "workers": "Workers", diff --git a/webapp/src/middleware/validateAuthToken.ts b/webapp/src/middleware/validateAuthToken.ts index c9be2c0..83d7786 100644 --- a/webapp/src/middleware/validateAuthToken.ts +++ b/webapp/src/middleware/validateAuthToken.ts @@ -6,8 +6,6 @@ export default async function validateAuthToken(authToken : string | undefined) : Promise { - console.log("----------NEXT_PUBLIC_API_BASE : " , process.env.NEXT_PUBLIC_API_BASE) - //process.env.NEXT_PUBLIC_API_BASE = "http://localhost:3000" let data : { success : boolean, } = await (await fetch(process.env.NEXT_PUBLIC_API_BASE+"/api/auth?authToken="+authToken)).json() diff --git a/webapp/src/utils/encryption.ts b/webapp/src/utils/encryption.ts new file mode 100644 index 0000000..9e474f0 --- /dev/null +++ b/webapp/src/utils/encryption.ts @@ -0,0 +1,41 @@ +/** + * @description Utility functions for encrypting and decrypting member IDs + */ + +import crypto from 'crypto'; + +// Secret key for encryption - in production, this should be in environment variables +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'your-32-character-secret-key-here!'; +const ALGORITHM = 'aes-256-cbc'; + +/** + * Encrypts a member ID + * @param memberId - The member ID to encrypt + * @returns Encrypted string + */ +export function encryptMemberId(memberId: string): string { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipher(ALGORITHM, ENCRYPTION_KEY); + let encrypted = cipher.update(memberId, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; +} + +/** + * Decrypts an encrypted member ID + * @param encryptedMemberId - The encrypted member ID + * @returns Decrypted member ID + */ +export function decryptMemberId(encryptedMemberId: string): string { + try { + const textParts = encryptedMemberId.split(':'); + const iv = Buffer.from(textParts.shift()!, 'hex'); + const encryptedText = textParts.join(':'); + const decipher = crypto.createDecipher(ALGORITHM, ENCRYPTION_KEY); + let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (error) { + throw new Error('Invalid encrypted member ID'); + } +} \ No newline at end of file diff --git a/webapp/test-api.js b/webapp/test-api.js new file mode 100644 index 0000000..2a8e596 --- /dev/null +++ b/webapp/test-api.js @@ -0,0 +1,64 @@ +/** + * Test script for the member lookup API + * This script tests the encryption/decryption and API functionality + */ + +const crypto = require('crypto'); + +// Same encryption configuration as in the utils +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'your-32-character-secret-key-here!'; +const ALGORITHM = 'aes-256-cbc'; + +// Encryption function (same as in utils/encryption.ts) +function encryptMemberId(memberId) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipher(ALGORITHM, ENCRYPTION_KEY); + let encrypted = cipher.update(memberId, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; +} + +// Decryption function (same as in utils/encryption.ts) +function decryptMemberId(encryptedMemberId) { + try { + const textParts = encryptedMemberId.split(':'); + const iv = Buffer.from(textParts.shift(), 'hex'); + const encryptedText = textParts.join(':'); + const decipher = crypto.createDecipher(ALGORITHM, ENCRYPTION_KEY); + let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (error) { + throw new Error('Invalid encrypted member ID'); + } +} + +// Test the encryption/decryption +const testMemberId = '507f1f77bcf86cd799439011'; // Example MongoDB ObjectId +console.log('Original Member ID:', testMemberId); + +const encrypted = encryptMemberId(testMemberId); +console.log('Encrypted Member ID:', encrypted); + +const decrypted = decryptMemberId(encrypted); +console.log('Decrypted Member ID:', decrypted); + +console.log('Encryption/Decryption Test:', testMemberId === decrypted ? 'PASSED' : 'FAILED'); + +// Test API URL +const apiUrl = `http://localhost:3000/api/member-lookup?encryptedId=${encodeURIComponent(encrypted)}`; +console.log('\nTest API URL:', apiUrl); +console.log('\nTo test the API, make a GET request to the above URL after starting the development server.'); +console.log('Expected response format:'); +console.log(JSON.stringify({ + success: true, + message: "Member information retrieved successfully", + data: { + name: "John Doe", + gender: "m", + planDelay: 1, + planStart: "Mon, 01 Jan 2024 00:00:00 GMT", + planStatus: "active", + planExpAt: "Thu, 01 Feb 2024 00:00:00 GMT" + } +}, null, 2)); \ No newline at end of file