Initial commit from template
Some checks failed
Cloudflare Worker API Template / Deploy to ${{ github.ref_name }} environment (push) Failing after 15s

This commit is contained in:
purvarao
2025-11-21 16:27:27 +05:30
commit 07a983d24f
21 changed files with 1742 additions and 0 deletions

16
src/db/schema.sql Normal file
View File

@@ -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);

141
src/index.ts Normal file
View File

@@ -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/cf-template/*', 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/cf-template/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/cf-template/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/cf-template/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/cf-template/health', (c) => {
return c.json({ status: 'ok' });
});
// Add Swagger UI routes
app.get('/api/cf-template/docs', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template'));
app.get('/api/cf-template/docs/', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template'));
app.get('/api/cf-template/swagger.json', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template'));
app.get('/api/cf-template/openapi.json', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template'));
// Export the app
export default {
fetch: app.fetch,
};

92
src/middleware/auth.js Normal file
View File

@@ -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<boolean>} - 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|void>} - 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();
}

146
src/services/db-service.js Normal file
View File

@@ -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<boolean>} - Whether initialization was successful
*/
async function initializeDatabase(env) {
try {
if (!env.CF_TEMPLATE_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.CF_TEMPLATE_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.CF_TEMPLATE_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_uid ON user_data(uid);`);
await env.CF_TEMPLATE_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<Object>} - 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.CF_TEMPLATE_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.CF_TEMPLATE_DB.prepare(
'SELECT id FROM user_data WHERE uid = ?'
).bind(uid).first();
let result;
if (existingUser) {
// Update existing user
result = await env.CF_TEMPLATE_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.CF_TEMPLATE_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<Object|null>} - User data or null if not found
*/
export async function getUserByUid(uid, env) {
try {
// Initialize database schema if needed
await initializeDatabase(env);
if (!env.CF_TEMPLATE_DB) {
console.log('D1 database binding not found, using local database');
// Use our local database implementation
return await getUserByUidLocal(uid);
}
const user = await env.CF_TEMPLATE_DB.prepare(
'SELECT * FROM user_data WHERE uid = ?'
).bind(uid).first();
return user;
} catch (error) {
console.error('Error getting user data:', error);
return null;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Object>} - 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<Object|null>} - 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>} - 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 [];
}
}

93
src/swagger-ui.js Normal file
View File

@@ -0,0 +1,93 @@
import { swaggerConfig } from './swagger';
// HTML template for Swagger UI
const swaggerHtml = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css">
<style>
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
.topbar { display: none; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
spec: SWAGGER_SPEC,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
</script>
</body>
</html>
`;
/**
* 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' }
});
}
}

7
src/swagger.js Normal file
View File

@@ -0,0 +1,7 @@
/**
* OpenAPI/Swagger configuration
*/
import { swaggerConfig } from './swagger/config';
export { swaggerConfig };

46
src/swagger/config.js Normal file
View File

@@ -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/leave-agent',
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
}
};

View File

@@ -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'
}
}
}
}
}
}
}
}
}
};

View File

@@ -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'
}
}
}
}
}
}
}
}
}
};

View File

@@ -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',
},
},
},
},
},
},
},
},
}
};