commit d63962d6d62e00524b830bfc9cc99518099f67a4 Author: Harsh Gupta Date: Thu Mar 5 16:32:07 2026 +0530 Initial commit from template diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a6483e4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,106 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.development +.env.test +.env.production +.dev.vars + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Docker +Dockerfile* +.dockerignore +docker-compose.yml + +# Cloudflare Workers +.wrangler/ +wrangler.toml + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..9dddd25 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,70 @@ +name: PROMPT_MANAGER Cloudflare Worker + +on: + push: + branches: [ dev, main, playtest ] + pull_request: + branches: [ dev, main, playtest ] + +jobs: + deploy: + runs-on: ubuntu-latest + + # Extract branch name from ref + env: + BRANCH_NAME: ${{ github.ref_name }} + + name: Deploy to ${{ github.ref_name }} environment + + steps: + - name: Set deployment variables + id: vars + run: | + # Extract environment from branch name + BRANCH="$BRANCH_NAME" + + # Set environment name and flags based on branch + if [[ "$BRANCH" == "main" ]]; then + envname="production" + env_flag="--env production" + elif [[ "$BRANCH" == "dev" ]]; then + envname="development" + env_flag="--env development" + elif [[ "$BRANCH" == "playtest" ]]; then + envname="playtest" + env_flag="--env playtest" + else + # Default to development if branch is unknown + envname="playtest" + env_flag="--env playtest" + fi + + # Write outputs for use in later steps + echo "envname=$envname" >> $GITHUB_OUTPUT + echo "env_flag=$env_flag" >> $GITHUB_OUTPUT + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Install Wrangler CLI + run: npm install -g wrangler + + # Variable setting is already done in the first step + + - name: Deploy to ${{ steps.vars.outputs.envname }} Environment + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} + run: | + echo "Deploying to ${{ steps.vars.outputs.envname }} environment" + wrangler deploy ${{ steps.vars.outputs.env_flag }} + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b49e16e --- /dev/null +++ b/.gitignore @@ -0,0 +1,215 @@ +# Logs + +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data + +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# wrangler project + +.dev.vars +.wrangler/ + +# Cloudflare Workers specific +worker-configuration.d.ts + +# IDE and Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp + +# Build outputs +dist/ +build/ +out/ + +# Local development files +.local/ +local/ + +# Secrets and sensitive data +secrets/ +*.key +*.pem +*.p12 +*.pfx + +# Logs and debugging +debug/ +*.log.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c556177 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20 + +WORKDIR /mcp-server + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 8787 + +# Run type check first, then start dev server +CMD ["npm", "run", "dev"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..745d071 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Cloudflare Worker API Template + +A production-ready template for building secure, modular APIs with Cloudflare Workers, featuring built-in authentication, database integration, and comprehensive documentation. + +## Features + +- **Authentication & Authorization**: Pre-configured with API key and Bearer token authentication +- **Database Integration**: Ready-to-use D1 database connections +- **Storage**: R2 bucket integration for file storage +- **Documentation**: Auto-generated Swagger/OpenAPI documentation +- **CI/CD**: GitHub Actions workflows for automated deployments +- **Environment Management**: Development, Playtest, and Production environments + +## Design Documentation + +All design documentation should be maintained in the blueprint repository: + +1. Create design documentation in the blueprint repository located at `prompt-manager` +2. Submit the design as a Pull Request to the blueprint repository +3. Once approved, add the link to the design file in `docs/DESIGN.md` + +The `docs/DESIGN.md` file should contain a reference to the design document's location in the blueprint repository. + +## Getting Started + +### Using This Template + +1. **Checkout the template branch**: + ```bash + git clone + git checkout template + ``` + +2. **Replace placeholder variables**: + Search and replace the following placeholders throughout the codebase: + - `prompt-manager` - Your application name in lowercase (e.g., `my-api`) + - `PROMPT_MANAGER` - Your application name in uppercase (e.g., `MY_API`) + + Make sure to check all files, especially: + - `wrangler.toml` + - `.github/workflows/deploy.yaml` + - `src/swagger/config.js` + +3. **Set up a new Git repository**: + ```bash + # Remove existing Git history + rm -rf .git + + # Initialize a new Git repository + git init + + # Add your remote repository + git remote add origin + + # Add all files + git add . + + # Commit + git commit -m "Initial commit from template" + + # Push to main branch + git push -u origin main + ``` + +4. **Create development branches**: + ```bash + # Create and push dev branch + git checkout -b dev + git push -u origin dev + + # Create and push playtest branch + git checkout -b playtest + git push -u origin playtest + ``` + +## Branching Strategy + +Follow this workflow for code changes: + +1. **Feature Development**: + - Create feature branches from `dev` + - Name format: `feature/your-feature-name` + +2. **Deployment Flow**: + - **Playtest**: Merge directly from feature branches for testing + - **Development**: Submit Pull Requests from feature branches + - **Production**: Submit Pull Requests from `dev` to `main` + +## Configuration + +### Database (D1) + +The template comes with D1 database configuration. If you need to use it: + +1. Create your D1 databases for each environment: + ```bash + create prompt-manager_dev + create prompt-manager_playtest + create prompt-manager_prod + ``` + +2. Update the database IDs in `wrangler.toml` with the IDs generated from the commands above. + +If you don't need D1, remove the `d1_databases` sections from `wrangler.toml`. + +### Storage (R2) + +The template includes R2 bucket configuration. If you need to use it: + +1. Create your R2 buckets for each environment: + ```bash + create prompt-manager-dev + create prompt-manager-playtest + create prompt-manager-prod + ``` + +2. Update the bucket names in `wrangler.toml`. + +If you don't need R2, remove the `r2_buckets` sections from `wrangler.toml`. + +## Authentication + +The API comes with two authentication methods: + +1. **API Key Authentication**: Using the `X-Api-Key` header +2. **Bearer Token Authentication**: Using the `Authorization: Bearer ` header + +These are configured in the Swagger documentation and middleware. Do not modify the authentication mechanisms unless necessary. + +## API Documentation + +The API documentation is generated using Swagger/OpenAPI. The configuration is modular and located in: + +- `src/swagger/config.js`: Main configuration +- `src/swagger/endpoints/`: Individual endpoint definitions + +When adding new endpoints: + +1. Create a new file in `src/swagger/endpoints/` for your endpoint +2. Export the endpoint configuration +3. Import and add it to `src/swagger/config.js` + +Access the documentation at `/api/prompt-manager/docs` when the API is running. + +## Design Documentation + +All design documentation should be maintained in the blueprint repository: + +1. Create design documentation in the blueprint repository located at `prompt-manager` +2. Submit the design as a Pull Request to the blueprint repository +3. Once approved, add the link to the design file in `docs/DESIGN.md` + +The `docs/DESIGN.md` file should contain a reference to the design document's location in the blueprint repository. + +## Code Structure Guidelines + +- Keep files modular and under 100 lines +- Organize related functionality into separate modules +- Place shared logic in `src/helpers/` or appropriate service directories +- Follow the existing pattern for new endpoints and services + +## Development + +### Local Development + +This project uses Docker for local development to ensure consistency across environments: + +```bash +# Start the local development server +docker compose up + +# To run in detached mode +docker compose up -d + +# To stop the server +docker compose down +``` + +The server will be available at http://localhost:8787. + +### Deployment + +Deployments are handled automatically by GitHub Actions when pushing to the appropriate branches: + +- Push to `playtest` → Deploys to Playtest environment +- Push to `dev` → Deploys to Development environment +- Push to `main` → Deploys to Production environment + +## License + +[MIT](LICENSE) + +## Development Checklist + +Follow this checklist when working on a new feature or issue: + +- [ ] Define Issue with Acceptance Criteria +- [ ] Create one or more Tasks to Work on Issue +- [ ] Create/Update Design Documentation +- [ ] Create PR for Design Documentation +- [ ] Follow Steps in README to create your own project +- [ ] Commit Code and test default code in all 3 environments +- [ ] Create feature branch for your issue +- [ ] Write your own code +- [ ] Test locally using docker compose +- [ ] Merge to playtest branch +- [ ] Test on Cloudflare +- [ ] Request PR to dev branch diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f668ade --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + prompt-manager-server: + build: + context: . + dockerfile: Dockerfile + ports: + - "8787:8787" + volumes: + - .:/mcp-server + - node_modules:/mcp-server/node_modules + environment: + - WORKER_ENV=${WORKER_ENV:-development} + command: npx wrangler dev --ip 0.0.0.0 + +volumes: + node_modules: diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..fd10dc6 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,15 @@ +# Design Documentation + +The design documentation for this project is maintained in the blueprint repository. + +## Blueprint Repository Location + +Design Document: [Blueprint Repository](https://git.code.svchub.com/HumanizeIQ/ai-blueprint/src/branch/feature/prompt_manager/design/PROMPT_MANAGER/design.md) + +## How to Update + +1. Create or update design documentation in the blueprint repository +2. Submit changes as a Pull Request to the blueprint repository +3. Once approved, update this file with the correct link to the design document + +*Note: Replace `prompt-manager` with your actual application name and update the path to the design document as needed.* https://github.com/HumanizeIQ-LLC/ai-blueprint/blob/main/blueprint.md \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9441b65 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "auth-securechat", + "version": "1.0.0", + "description": "Authentication worker for SecureChat using AnythingLLM", + "main": "src/index.js", + "scripts": { + "dev": "wrangler dev --ip 0.0.0.0", + "deploy": "wrangler deploy" + }, + "dependencies": { + "hono": "^3.12.0", + "cookie": "^0.6.0" + }, + "devDependencies": { + "wrangler": "^4.24.0" + } + } + \ No newline at end of file diff --git a/src/db/schema.sql b/src/db/schema.sql new file mode 100644 index 0000000..b184b75 --- /dev/null +++ b/src/db/schema.sql @@ -0,0 +1,16 @@ +-- Schema for user_data table +CREATE TABLE IF NOT EXISTS user_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, + firebase_uid TEXT, + firstname TEXT, + lastname TEXT, + company_name TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Index for faster lookups +CREATE INDEX IF NOT EXISTS idx_user_data_uid ON user_data(uid); +CREATE INDEX IF NOT EXISTS idx_user_data_email ON user_data(email); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4c9e1cb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,141 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { getCookie } from 'hono/cookie'; +import { decryptAuthCookie } from './services/decrypt-service'; +import { handleSwaggerRequest } from './swagger-ui'; +import { authMiddleware } from './middleware/auth'; +import { saveUserData, getUserByUid } from './services/db-service'; + +// Create a new Hono app +const app = new Hono(); + +// Add middleware +app.use('*', async (c, next) => { + console.log(`[${c.req.method}] ${c.req.url}`); + await next(); +}); + +// Add CORS middleware +app.use('*', cors()); + +// Create a group for protected routes with auth middleware +// Exclude Swagger docs routes from auth middleware +app.use('/api//*', async (c, next) => { + const path = new URL(c.req.url).pathname; + + // Skip auth for Swagger docs routes + if (path.includes('/docs') || path.includes('/swagger.json') || path.includes('/openapi.json')) { + return next(); + } + + // Apply auth middleware for all other routes + return authMiddleware(c, next); +}); + +// Add auth validation endpoint (GET method) +app.get('/api//auth/validate', async (c) => { + try { + // Get auth from query parameter + const authToken = c.req.query('auth'); + + if (!authToken) { + return c.json({ error: 'No auth parameter found' }, 401); + } + + // Decrypt the auth token + const decryptedData = await decryptAuthCookie(authToken, c.env); + + // Check if the decrypted data contains the expected fields + if (!decryptedData || !decryptedData.firstname || !decryptedData.lastname) { + console.error('Invalid decrypted data format:', decryptedData); + return c.json({ error: 'Invalid auth parameter' }, 401); + } + return c.json(decryptedData, 200); + } catch (error) { + console.error('Authentication error:', error); + return c.json({ error: 'Authentication failed' }, 500); + } +}); + +// Add auth validation endpoint (POST method) +app.post('/api//auth/validate', async (c) => { + try { + // Get auth from request body + const body = await c.req.json(); + const authToken = body.auth; + + if (!authToken) { + return c.json({ error: 'No auth parameter found in request body' }, 401); + } + + // Decrypt the auth token + const decryptedData = await decryptAuthCookie(authToken, c.env); + + // Check if the decrypted data contains the expected fields + if (!decryptedData || !decryptedData.firstname || !decryptedData.lastname) { + console.error('Invalid decrypted data format:', decryptedData); + return c.json({ error: 'Invalid auth parameter' }, 401); + } + return c.json(decryptedData, 200); + } catch (error) { + console.error('Authentication error:', error); + return c.json({ error: 'Authentication failed' }, 500); + } +}); + +// Add endpoint to decrypt and save cookie data to D1 database +app.post('/api//auth/decrypt-and-save', async (c) => { + try { + // Get auth token from request body + const body = await c.req.json(); + const authToken = body.auth; + + if (!authToken) { + return c.json({ error: 'No auth parameter found in request body' }, 401); + } + + // Decrypt the auth token + const decryptedData = await decryptAuthCookie(authToken, c.env); + + // Check if the decrypted data contains the expected fields + if (!decryptedData || !decryptedData.uid || !decryptedData.email) { + console.error('Invalid decrypted data format:', decryptedData); + return c.json({ error: 'Invalid auth parameter' }, 401); + } + + // Save the decrypted data to the D1 database + const result = await saveUserData(decryptedData, c.env); + + if (!result.success) { + return c.json({ error: result.error || 'Failed to save user data' }, 500); + } + + // Return success response with the saved data + return c.json({ + success: true, + message: result.message, + updated: result.updated, + uid: result.uid, + userData: decryptedData + }, 200); + } catch (error) { + console.error('Error processing request:', error); + return c.json({ error: 'Failed to process request', details: error.message }, 500); + } +}); + +// Add health check endpoint +app.get('/api//health', (c) => { + return c.json({ status: 'ok' }); +}); + +// Add Swagger UI routes +app.get('/api//docs', (c) => handleSwaggerRequest(c.req.raw, '/api/')); +app.get('/api//docs/', (c) => handleSwaggerRequest(c.req.raw, '/api/')); +app.get('/api//swagger.json', (c) => handleSwaggerRequest(c.req.raw, '/api/')); +app.get('/api//openapi.json', (c) => handleSwaggerRequest(c.req.raw, '/api/')); + +// Export the app +export default { + fetch: app.fetch, +}; diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 0000000..ab4cd38 --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,92 @@ +/** + * Authorization middleware for validating API keys and Bearer tokens + */ + +/** + * Validates an API key or Bearer token against the CM_BASE_URL + * @param {string} token - The API key or Bearer token to validate + * @param {Object} env - Environment variables + * @returns {Promise} - Whether the token is valid + */ +async function validateToken(token, env) { + try { + // Construct the validation URL + const validationUrl = `${env.CM_BASE_URL}/api/keys/validate?key=${token}`; + + // Make the validation request + const response = await fetch(validationUrl, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'X-Api-Key': token + } + }); + + // Check if the response is successful + if (!response.ok) { + console.error(`Token validation failed with status: ${response.status}`); + return false; + } + + // Parse the response + const data = await response.json(); + + // Check if the token is valid + return data.valid === true; + } catch (error) { + console.error('Error validating token:', error); + return false; + } +} + +/** + * Middleware to check for valid API key or Bearer token + * @param {Object} c - Hono context + * @param {Function} next - Next middleware function + * @returns {Promise} - Response or next middleware + */ +export async function authMiddleware(c, next) { + // Get the authorization header + const authHeader = c.req.header('Authorization'); + const apiKey = c.req.header('X-Api-Key'); + + let token = null; + + // Check for API key + if (apiKey) { + token = apiKey; + } + // Check for Bearer token + else if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } + + // If no token is provided, return 401 + if (!token) { + return c.json({ + error: 'Unauthorized', + message: 'API key or Bearer token required' + }, 401); + } + + // Special case for testing: allow system-key to bypass validation + if (token === 'system-key') { + console.log('Using system-key bypass for testing'); + await next(); + return; + } + + // Validate the token + const isValid = await validateToken(token, c.env); + + // If the token is not valid, return 401 + if (!isValid) { + return c.json({ + error: 'Unauthorized', + message: 'Invalid API key or Bearer token' + }, 401); + } + + // Token is valid, continue to the next middleware + await next(); +} diff --git a/src/services/db-service.js b/src/services/db-service.js new file mode 100644 index 0000000..8665b0d --- /dev/null +++ b/src/services/db-service.js @@ -0,0 +1,146 @@ +/** + * Database service for user data operations + */ + +import { saveUserDataLocal, getUserByUidLocal, initLocalDb } from './local-db-service'; + +/** + * Initialize the database schema + * @param {Object} env - Environment variables containing D1 database binding + * @returns {Promise} - Whether initialization was successful + */ +async function initializeDatabase(env) { + try { + if (!env.PROMPT_MANAGER_DB) { + console.log('D1 database binding not found, skipping initialization'); + return false; + } + + // Create user_data table if it doesn't exist + try { + // Use a single-line SQL statement to avoid parsing issues with D1 + await env.PROMPT_MANAGER_DB.exec(`CREATE TABLE IF NOT EXISTS user_data (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE NOT NULL, email TEXT NOT NULL, firebase_uid TEXT, firstname TEXT, lastname TEXT, company_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`); + + // Create indexes in a single-line format as well + await env.PROMPT_MANAGER_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_uid ON user_data(uid);`); + await env.PROMPT_MANAGER_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_email ON user_data(email);`); + + console.log('Database schema initialized successfully'); + } catch (dbError) { + console.error('Error creating schema:', dbError); + // If we can't create the table, just return mock data + return false; + } + + return true; + } catch (error) { + console.error('Error initializing database schema:', error); + return false; + } +} + +/** + * Save user data from decrypted cookie to D1 database + * @param {Object} userData - The decrypted user data + * @param {Object} env - Environment variables containing D1 database binding + * @returns {Promise} - Result of the operation + */ +export async function saveUserData(userData, env) { + try { + // Initialize database schema if needed + await initializeDatabase(env); + // Check if required database binding exists + if (!env.PROMPT_MANAGER_DB) { + console.log('D1 database binding not found, using local database'); + // Use our local database implementation + return await saveUserDataLocal(userData); + } + + // Extract user data from the decrypted cookie + const { uid, email, firebase_uid, firstname, lastname, company_name } = userData; + + // Validate required fields + if (!uid || !email) { + return { success: false, error: 'Missing required fields (uid, email)' }; + } + + try { + // Check if user already exists + const existingUser = await env.PROMPT_MANAGER_DB.prepare( + 'SELECT id FROM user_data WHERE uid = ?' + ).bind(uid).first(); + + let result; + + if (existingUser) { + // Update existing user + result = await env.PROMPT_MANAGER_DB.prepare(` + UPDATE user_data + SET email = ?, firebase_uid = ?, firstname = ?, lastname = ?, company_name = ?, updated_at = CURRENT_TIMESTAMP + WHERE uid = ? + `).bind(email, firebase_uid, firstname, lastname, company_name, uid).run(); + + return { + success: true, + message: 'User data updated successfully', + updated: true, + uid + }; + } else { + // Insert new user + result = await env.PROMPT_MANAGER_DB.prepare(` + INSERT INTO user_data (uid, email, firebase_uid, firstname, lastname, company_name) + VALUES (?, ?, ?, ?, ?, ?) + `).bind(uid, email, firebase_uid, firstname, lastname, company_name).run(); + + return { + success: true, + message: 'User data saved successfully', + updated: false, + uid + }; + } + } catch (dbError) { + console.error('Database operation failed:', dbError); + // Return success with the data even if DB operation failed + return { + success: true, + message: 'User data processed (DB operation failed)', + error: dbError.message, + updated: false, + uid, + userData + }; + } + } catch (error) { + console.error('Error saving user data:', error); + return { success: false, error: error.message }; + } +} + +/** + * Get user data by uid + * @param {string} uid - The user's unique identifier + * @param {Object} env - Environment variables containing D1 database binding + * @returns {Promise} - User data or null if not found + */ +export async function getUserByUid(uid, env) { + try { + // Initialize database schema if needed + await initializeDatabase(env); + if (!env.PROMPT_MANAGER_DB) { + console.log('D1 database binding not found, using local database'); + // Use our local database implementation + return await getUserByUidLocal(uid); + } + + const user = await env.PROMPT_MANAGER_DB.prepare( + 'SELECT * FROM user_data WHERE uid = ?' + ).bind(uid).first(); + + return user; + } catch (error) { + console.error('Error getting user data:', error); + return null; + } +} diff --git a/src/services/decrypt-service.js b/src/services/decrypt-service.js new file mode 100644 index 0000000..2759dd2 --- /dev/null +++ b/src/services/decrypt-service.js @@ -0,0 +1,47 @@ +/** + * Service for decrypting authentication cookies + */ + +/** + * Decrypts an authentication cookie by calling the decrypt API + */ +export async function decryptAuthCookie(authCookie, env) { + try { + // For development/testing: handle test-token specially + if (authCookie === 'test-token') { + console.log('Using test token, returning mock data'); + return { + uid: '426bcea5-adb9-4580-a8ca-3a40fdb0ef85', + email: 'humanizeiq@eteaminc.com', + firebase_uid: 'wFyjGE1j8wclXayUvPbkF4c15f92', + firstname: 'Rajeev', + lastname: 'Borborah', + company_name: null, + success: true, + message: 'All cookies decrypted successfully' + }; + } + console.log(`Calling decrypt API at ${env.DECRYPT_API_URL}/secure-auth/decrypt-cookie`); + const response = await fetch(`${env.DECRYPT_API_URL}/secure-auth/decrypt-cookie`, { + method: 'POST', + headers: { + 'accept': 'application/json', + 'X-API-Key': env.DECRYPT_API_KEY, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ auth: authCookie }), + }); + console.log("Payload",env.DECRYPT_API_KEY,authCookie) + if (!response.ok) { + console.error('Decrypt API error:', response.status); + return null; + } + + const data = await response.json(); + console.log('Decrypted data:', JSON.stringify(data)); + return data; + } catch (error) { + console.error('Error decrypting auth cookie:', error); + return null; + } +} diff --git a/src/services/local-db-service.js b/src/services/local-db-service.js new file mode 100644 index 0000000..bdfa210 --- /dev/null +++ b/src/services/local-db-service.js @@ -0,0 +1,91 @@ +/** + * Local SQLite database service for development + */ + +// This service provides a fallback for when Cloudflare D1 is not available +// It uses in-memory storage for development and testing + +// In-memory database store +const inMemoryDb = { + users: new Map() +}; + +/** + * Initialize the local database + */ +export function initLocalDb() { + console.log('Initializing local in-memory database'); + // Nothing to do for in-memory database + return true; +} + +/** + * Save user data to local storage + * @param {Object} userData - The user data to save + * @returns {Promise} - Result of the operation + */ +export async function saveUserDataLocal(userData) { + try { + if (!userData || !userData.uid) { + return { success: false, error: 'Missing required user data' }; + } + + const { uid } = userData; + const existingUser = inMemoryDb.users.get(uid); + const isUpdate = !!existingUser; + + // Add timestamp + const now = new Date().toISOString(); + const userWithTimestamp = { + ...userData, + created_at: existingUser?.created_at || now, + updated_at: now + }; + + // Save to in-memory database + inMemoryDb.users.set(uid, userWithTimestamp); + + console.log(`User data ${isUpdate ? 'updated' : 'saved'} in local database:`, uid); + + return { + success: true, + message: `User data ${isUpdate ? 'updated' : 'saved'} successfully in local database`, + updated: isUpdate, + uid + }; + } catch (error) { + console.error('Error saving user data to local database:', error); + return { success: false, error: error.message }; + } +} + +/** + * Get user data by uid from local storage + * @param {string} uid - The user's unique identifier + * @returns {Promise} - User data or null if not found + */ +export async function getUserByUidLocal(uid) { + try { + if (!uid) { + return null; + } + + return inMemoryDb.users.get(uid) || null; + } catch (error) { + console.error('Error getting user data from local database:', error); + return null; + } +} + +/** + * Get all users from local storage + * @returns {Promise} - Array of users + */ +export async function getAllUsersLocal() { + try { + return Array.from(inMemoryDb.users.values()); + } catch (error) { + console.error('Error getting all users from local database:', error); + return []; + } +} diff --git a/src/swagger-ui.js b/src/swagger-ui.js new file mode 100644 index 0000000..5f6148a --- /dev/null +++ b/src/swagger-ui.js @@ -0,0 +1,92 @@ +import { swaggerConfig } from './swagger'; + +const swaggerHtml = ` + + + + + API Documentation + + + + +
+ + + + + +`; + +/** + * Handles Swagger UI requests + * @param {Request} request - The incoming request + * @param {string} basePath - The base path for the API + * @returns {Response} - The response with Swagger UI or JSON + */ +export function handleSwaggerRequest(request, basePath = '') { + try { + const url = new URL(request.url); + const path = url.pathname; + + // Serve the OpenAPI/Swagger JSON specification + if (path === `${basePath}/openapi.json` || path === `${basePath}/swagger.json`) { + return new Response(JSON.stringify(swaggerConfig), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Serve the Swagger UI HTML + if (path === `${basePath}/docs` || path === `${basePath}/docs/` || path === basePath) { + // Replace the placeholder with the actual Swagger definition + const html = swaggerHtml.replace( + 'SWAGGER_SPEC', + JSON.stringify(swaggerConfig) + ); + + return new Response(html, { + headers: { 'Content-Type': 'text/html' }, + }); + } + + // If not a valid Swagger UI path, return a JSON error + return new Response(JSON.stringify({ + error: 'Not Found', + message: 'The requested Swagger UI resource was not found' + }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + // Return a proper JSON error response instead of HTML + return new Response(JSON.stringify({ + error: 'Swagger UI Error', + details: error.message + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +} diff --git a/src/swagger.js b/src/swagger.js new file mode 100644 index 0000000..a95e632 --- /dev/null +++ b/src/swagger.js @@ -0,0 +1,7 @@ +/** + * OpenAPI/Swagger configuration + */ + +import { swaggerConfig } from './swagger/config'; + +export { swaggerConfig }; diff --git a/src/swagger/config.js b/src/swagger/config.js new file mode 100644 index 0000000..cc3c316 --- /dev/null +++ b/src/swagger/config.js @@ -0,0 +1,46 @@ +/** + * Main Swagger configuration + */ +import { healthEndpoint } from './endpoints/health'; +import { authValidateEndpoint } from './endpoints/auth-validate'; +import { decryptAndSaveEndpoint } from './endpoints/decrypt-and-save'; + +export const swaggerConfig = { + openapi: '3.0.0', + info: { + title: 'Auth SecureChat API', + version: '1.0.0', + description: 'API for authentication with SecureChat using AnythingLLM', + }, + servers: [ + { + url: '/api/prompt-manager', + description: 'API Base Path', + }, + ], + components: { + securitySchemes: { + ApiKeyAuth: { + type: 'apiKey', + in: 'header', + name: 'X-Api-Key', + description: 'API key for authorization' + }, + BearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Bearer token for authorization' + } + } + }, + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + paths: { + ...healthEndpoint, + ...authValidateEndpoint, + ...decryptAndSaveEndpoint + } +}; diff --git a/src/swagger/endpoints/auth-validate.js b/src/swagger/endpoints/auth-validate.js new file mode 100644 index 0000000..845cbd2 --- /dev/null +++ b/src/swagger/endpoints/auth-validate.js @@ -0,0 +1,168 @@ +/** + * Auth validation endpoint Swagger configuration + */ + +export const authValidateEndpoint = { + '/auth/validate': { + get: { + summary: 'Validate authentication token (GET)', + description: 'Validates the auth token parameter via query parameter', + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + parameters: [ + { + name: 'auth', + in: 'query', + description: 'Authentication token', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + description: 'Authentication successful', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + firstname: { + type: 'string', + example: 'John' + }, + lastname: { + type: 'string', + example: 'Doe' + }, + email: { + type: 'string', + example: 'john.doe@example.com' + } + } + } + } + } + }, + '401': { + description: 'Authentication failed', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Authentication failed', + }, + }, + }, + }, + }, + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Authentication failed', + }, + }, + }, + }, + }, + }, + }, + }, + post: { + summary: 'Validate authentication token (POST)', + description: 'Validates the auth token parameter via request body', + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['auth'], + properties: { + auth: { + type: 'string', + description: 'Authentication token' + } + } + } + } + } + }, + responses: { + '200': { + description: 'Authentication successful', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + firstname: { + type: 'string', + example: 'John' + }, + lastname: { + type: 'string', + example: 'Doe' + }, + email: { + type: 'string', + example: 'john.doe@example.com' + } + } + } + } + } + }, + '401': { + description: 'Authentication failed', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Authentication failed' + } + } + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Authentication failed' + } + } + } + } + } + } + } + } + } +}; diff --git a/src/swagger/endpoints/decrypt-and-save.js b/src/swagger/endpoints/decrypt-and-save.js new file mode 100644 index 0000000..387d007 --- /dev/null +++ b/src/swagger/endpoints/decrypt-and-save.js @@ -0,0 +1,125 @@ +/** + * Decrypt and save endpoint Swagger configuration + */ + +export const decryptAndSaveEndpoint = { + '/auth/decrypt-and-save': { + post: { + summary: 'Decrypt auth token and save user data to database', + description: 'Decrypts the provided auth token and saves the user data to the D1 database', + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['auth'], + properties: { + auth: { + type: 'string', + description: 'Authentication token to decrypt' + } + } + } + } + } + }, + responses: { + '200': { + description: 'User data successfully saved', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + message: { + type: 'string', + example: 'User data saved successfully' + }, + updated: { + type: 'boolean', + example: false + }, + uid: { + type: 'string', + example: '426bcea5-adb9-4580-a8ca-3a40fdb0ef85' + }, + userData: { + type: 'object', + properties: { + uid: { + type: 'string', + example: '426bcea5-adb9-4580-a8ca-3a40fdb0ef85' + }, + email: { + type: 'string', + example: 'humanizeiq@eteaminc.com' + }, + firebase_uid: { + type: 'string', + example: 'wFyjGE1j8wclXayUvPbkF4c15f92' + }, + firstname: { + type: 'string', + example: 'Rajeev' + }, + lastname: { + type: 'string', + example: 'Borborah' + }, + company_name: { + type: 'string', + nullable: true, + example: null + } + } + } + } + } + } + } + }, + '401': { + description: 'Authentication failed', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Invalid auth parameter' + } + } + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + example: 'Failed to save user data' + } + } + } + } + } + } + } + } + } +}; diff --git a/src/swagger/endpoints/health.js b/src/swagger/endpoints/health.js new file mode 100644 index 0000000..ebed458 --- /dev/null +++ b/src/swagger/endpoints/health.js @@ -0,0 +1,42 @@ +/** + * Health endpoint Swagger configuration + */ + +export const healthEndpoint = { + '/health': { + get: { + summary: 'Health check endpoint', + description: 'Returns the status of the API', + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + responses: { + '200': { + description: 'API is running', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { + type: 'string', + example: 'ok', + }, + message: { + type: 'string', + example: 'Auth SecureChat API is running', + }, + version: { + type: 'string', + example: '1.0.0', + }, + }, + }, + }, + }, + }, + }, + }, + } +}; diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..844d490 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,65 @@ +# Base configuration (shared between environments) +main = "src/index.ts" +account_id = "cba4afd7666247724ece1f34e1aace6c" +compatibility_date = "2025-03-10" +compatibility_flags = ["nodejs_compat"] + +[vars] +name = '-local' +CM_BASE_URL = "https://cm.dev.svchub.com" +DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" +DECRYPT_API_KEY = "XMCKhjOTTPJJzoSiNTmRTcQauvtGKRnUZNFlhWpjhTEaIdIeIdLFM" +d1_databases= [ + { binding = "PROMPT_MANAGER_DB", database_name = "_test-db", database_id = "c020574a-5623-407b-be0c-cd192bab9545" } +] + +# Playtest Environment +[env.playtest] +name = "-playtest" +routes = [ + { pattern = ".playtest.svchub.com", custom_domain = true } +] +d1_databases = [ + { binding = "PROMPT_MANAGER_DB", database_name = "_playtest", database_id = "fd847b4b-e1e9-442e-9756-23c4540476e7" } +] +r2_buckets = [ + { binding = "PROMPT_MANAGER_BUCKET", bucket_name = "-playtest" } +] +[env.playtest.vars] +WORKER_ENV = "playtest" +CM_BASE_URL = "https://cm.dev.svchub.com" +DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" + +# Dev Environment +[env.development] +name = "-dev" +routes = [ + { pattern = ".dev.svchub.com", custom_domain = true } +] +d1_databases = [ + { binding = "PROMPT_MANAGER_DB", database_name = "_dev", database_id = "08a710a9-27ae-4886-9cab-c0a7ab204de9" } +] +r2_buckets = [ + { binding = "PROMPT_MANAGER_BUCKET", bucket_name = "-dev" } +] +[env.development.vars] +WORKER_ENV = "development" +CM_BASE_URL = "https://cm.dev.svchub.com" +DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" + +# Production Environment +[env.production] +name = "-prod" +routes = [ + { pattern = ".prod.svchub.com", custom_domain = true } +] +d1_databases = [ + { binding = "PROMPT_MANAGER_DB", database_name = "_prod", database_id = "2983ec1a-3c9c-451d-b85e-5a52ff9b114a" } +] +r2_buckets = [ + { binding = "PROMPT_MANAGER_BUCKET", bucket_name = "-prod" } +] +[env.production.vars] +WORKER_ENV = "production" +CM_BASE_URL = "https://cm.svchub.com" +DECRYPT_API_URL = "https://www.humanizeiq.ai/api/dashboard-backend"