Skip to content

Commit

Permalink
Merge pull request #21 from devondragon/issue-20-Switch-to-using-Itty…
Browse files Browse the repository at this point in the history
…-Router

Issue 20 switch to using itty router
  • Loading branch information
devondragon authored Jul 6, 2024
2 parents 0c69fb8 + beabf40 commit cd248b4
Show file tree
Hide file tree
Showing 15 changed files with 724 additions and 552 deletions.
243 changes: 181 additions & 62 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
],
"devDependencies": {
"lerna": "^8.1.2"
},
"dependencies": {
"itty-router": "^5.0.17"
}
}
4 changes: 2 additions & 2 deletions packages/account-pages/static/js/api.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// For local development, use the following line:
// const apiBaseUrl = 'http://localhost:51512';
const apiBaseUrl = 'http://localhost:51512';

// For production, use the following line with your domain name:
//const apiBaseUrl = 'https://user-mgmt.yourdomain.com';

// Or, if you're running the user-mgmt worker under a route on the same domain as this front end:
const apiBaseUrl = `${window.location.protocol}//${window.location.host}/user-api`;
// const apiBaseUrl = `${window.location.protocol}//${window.location.host}/user-api`;


export function callApi(endpoint, data = null) {
Expand Down
5 changes: 4 additions & 1 deletion packages/session-state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
"wrangler": "^3.63.1"
},
"dependencies": {
"itty-router": "^5.0.17"
}
}
3 changes: 3 additions & 0 deletions packages/session-state/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Env {
sessionstore: KVNamespace;
}
108 changes: 39 additions & 69 deletions packages/session-state/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,49 @@
* It is designed to be deployed as part of a Cloudflare Worker.
*
*/
import { AutoRouter, IRequest } from 'itty-router';
import { Env } from './env';
import {
createSession,
getSessionData,
deleteSession,
} from './session';

const router = AutoRouter<IRequest, [Env, ExecutionContext]>();

export interface Env {
sessionstore: KVNamespace;
}

function generateSessionId(): string {
// Generate a random and unique session identifier.
// This should be VERY unique, but with a very high traffic system or one with extremely high security requirements,
// you may want to ensure the key is not in use.
return crypto.randomUUID();
}

// Create a new session in the KV store
async function createSession(data: any, env: Env): Promise<string> {
const sessionId = generateSessionId();
try {
await env.sessionstore.put(sessionId, JSON.stringify(data));
} catch (error) {
console.error("Error creating session: " + error);
}
return sessionId;
}

// Update session data in the KV store
async function updateSession(sessionId: string, data: any, env: Env): Promise<void> {
await env.sessionstore.put(sessionId, JSON.stringify(data));
}

// Add data to an existing session
async function addToSession(sessionId: string, data: any, env: Env): Promise<void> {
const sessionData = await getSessionData(sessionId, env);
await updateSession(sessionId, { ...sessionData, ...data }, env);
}

// Retrieve session data from the KV store
async function getSessionData(sessionId: string, env: Env): Promise<any> {
const data = await env.sessionstore.get(sessionId);
return data ? JSON.parse(data) : null;
}

// Delete a session from the KV store
async function deleteSession(sessionId: string, env: Env): Promise<void> {
await env.sessionstore.delete(sessionId);
}

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;

router
.post('/create', async (request, env, ctx) => {
try {
if (path === '/create' && request.method === 'POST') {
const requestData = await request.json();
const sessionId = await createSession(requestData, env);
return new Response(sessionId);

} else if (path.startsWith('/get/') && request.method === 'GET') {
const sessionId = path.split('/')[2];
const data = await getSessionData(sessionId, env);
return new Response(JSON.stringify(data));

} else if (path.startsWith('/delete/') && request.method === 'DELETE') {
const sessionId = path.split('/')[2];
await deleteSession(sessionId, env);
return new Response('Session deleted');

} else {
return new Response('Invalid request', { status: 404 });
const requestData = await request.json();
const sessionId = await createSession(requestData, env);
return new Response(sessionId, { status: 201 });
} catch (error) {
return new Response('Failed to create session', { status: 500 });
}
})
.get('/get/:sessionId', async ({ params }, env) => {
try {
const { sessionId } = params;
const data = await getSessionData(sessionId, env);
if (!data) {
return new Response('Session not found', { status: 404 });
}
return new Response(JSON.stringify(data), { status: 200 });
} catch (error) {
console.error("Error processing request: " + error);
return new Response('Error processing request', { status: 500 });
return new Response('Failed to retrieve session', { status: 500 });
}
},
})
.delete('/delete/:sessionId', async ({ params }, env) => {
try {
const { sessionId } = params;
await deleteSession(sessionId, env);
return new Response('Session deleted', { status: 200 });
} catch (error) {
return new Response('Failed to delete session', { status: 500 });
}
})
.all('*', () => new Response('Invalid request', { status: 404 }));

export default {
fetch: router.handle,
};
44 changes: 44 additions & 0 deletions packages/session-state/src/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Env } from './env';

export function generateSessionId(): string {
return crypto.randomUUID();
}

export async function createSession(data: any, env: Env): Promise<string> {
const sessionId = generateSessionId();
try {
await env.sessionstore.put(sessionId, JSON.stringify(data));
} catch (error) {
console.error("Error creating session: " + error);
throw new Error('Failed to create session');
}
return sessionId;
}

export async function updateSession(sessionId: string, data: any, env: Env): Promise<void> {
try {
await env.sessionstore.put(sessionId, JSON.stringify(data));
} catch (error) {
console.error("Error updating session: " + error);
throw new Error('Failed to update session');
}
}

export async function addToSession(sessionId: string, data: any, env: Env): Promise<void> {
const sessionData = await getSessionData(sessionId, env);
await updateSession(sessionId, { ...sessionData, ...data }, env);
}

export async function getSessionData(sessionId: string, env: Env): Promise<any> {
const data = await env.sessionstore.get(sessionId);
return data ? JSON.parse(data) : null;
}

export async function deleteSession(sessionId: string, env: Env): Promise<void> {
try {
await env.sessionstore.delete(sessionId);
} catch (error) {
console.error("Error deleting session: " + error);
throw new Error('Failed to delete session');
}
}
3 changes: 2 additions & 1 deletion packages/user-mgmt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
"start": "wrangler dev"
},
"dependencies": {
"itty-router": "^5.0.17",
"session-state": "file:../session-state"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
"wrangler": "^3.63.1"
}
}
30 changes: 30 additions & 0 deletions packages/user-mgmt/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Hashes a password using the SHA-256 algorithm.
*
* @param password - The password to be hashed.
* @returns A Promise that resolves to the hashed password.
*/
export async function hashPassword(password: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(16)).join('');
const data = new TextEncoder().encode(salt + password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return `${salt}:${hashHex}`;
}


/**
* Compares a provided password with a stored hash.
* @param providedPassword - The password provided by the user.
* @param storedHash - The stored hash of the password.
* @returns A promise that resolves to a boolean indicating whether the provided password matches the stored hash.
*/
export async function comparePassword(providedPassword: string, storedHash: string): Promise<boolean> {
const [salt, originalHash] = storedHash.split(':');
const data = new TextEncoder().encode(salt + providedPassword);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex === originalHash;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
/**
* This module provides functionality for sending emails within a Cloudflare Workers environment. It is designed to
* integrate with external email services, specifically using the MailChannels API for email transmission. The module
* supports DKIM (DomainKeys Identified Mail) signatures to enhance email security and deliverability. It allows sending
* emails with personalized content and subject lines to specified recipients.
*
* The `sendEmail` function encapsulates the process of constructing the email payload, including DKIM configuration,
* and sending the email through a POST request to the MailChannels API. Error handling mechanisms are in place to
* ensure robustness and provide feedback on the email sending process.
*
* Interfaces are defined for the structure of email recipients, content, DKIM data, and the overall email payload
* to ensure type safety and clarity throughout the email construction and sending process.
*
* The `Env` interface outlines the expected environment variables, including configurations for the email sender,
* DKIM setup, and other service-specific settings.
*/
import { Env } from './env';

// Interfaces

interface EmailRecipient {
email: string;
name: string;
Expand All @@ -27,12 +11,6 @@ interface EmailContent {
value: string;
}

interface DkimData {
dkim_domain: string;
dkim_selector: string;
dkim_private_key: string;
}

interface EmailPayload {
personalizations: Array<{
to: EmailRecipient[];
Expand All @@ -45,19 +23,6 @@ interface EmailPayload {
content: EmailContent[];
}

export interface Env {
usersDB: D1Database; // Reference to Cloudflare's D1 Database for user data.
sessionService: Fetcher; // Direct reference to session-state Worker for session management.
EMAIL_FROM: string; // Email address to use as the sender for password reset emails.
EMAIL_FROM_NAME: string; // Name to use as the sender for password reset emails.
FORGOT_PASSWORD_URL: string; // URL to use as the password reset link in the email.
TOKEN_VALID_MINUTES: number; // Time in minutes for the password reset token to expire.
EMAIL_DKIM_DOMAIN: string; // Domain for DKIM signature
EMAIL_DKIM_SELECTOR: string; // Selector for DKIM signature
EMAIL_DKIM_PRIVATE_KEY: string; // Private key for DKIM signature
}


/**
* Sends an email using MailChannels API with specified recipient, subject, and content.
* DKIM settings are included in the payload for email security.
Expand Down
50 changes: 50 additions & 0 deletions packages/user-mgmt/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Represents the environment configuration for the user management module.
*/
export interface Env {
usersDB: D1Database;
sessionService: Fetcher;
EMAIL_FROM: string;
EMAIL_FROM_NAME: string;
FORGOT_PASSWORD_URL: string;
TOKEN_VALID_MINUTES: number;
EMAIL_DKIM_DOMAIN: string;
EMAIL_DKIM_SELECTOR: string;
EMAIL_DKIM_PRIVATE_KEY: string;
}

export function getUsersDB(env: Env): D1Database {
return env.usersDB;
}

export function getSessionService(env: Env): Fetcher {
return env.sessionService;
}

export function getEmailFrom(env: Env): string {
return env.EMAIL_FROM;
}

export function getEmailFromName(env: Env): string {
return env.EMAIL_FROM_NAME;
}

export function getForgotPasswordUrl(env: Env): string {
return env.FORGOT_PASSWORD_URL;
}

export function getTokenValidMinutes(env: Env): number {
return env.TOKEN_VALID_MINUTES;
}

export function getEmailDkimDomain(env: Env): string {
return env.EMAIL_DKIM_DOMAIN;
}

export function getEmailDkimSelector(env: Env): string {
return env.EMAIL_DKIM_SELECTOR;
}

export function getEmailDkimPrivateKey(env: Env): string {
return env.EMAIL_DKIM_PRIVATE_KEY;
}
Loading

0 comments on commit cd248b4

Please sign in to comment.