Skip to content

Commit

Permalink
Added ittyrouter
Browse files Browse the repository at this point in the history
Refactored code into smaller files
  • Loading branch information
devondragon committed Jul 6, 2024
1 parent 0c69fb8 commit e903d9f
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 481 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"
}
}
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: 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;
}
224 changes: 224 additions & 0 deletions packages/user-mgmt/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { Env, getForgotPasswordUrl } from './env';
import { getSessionIdFromCookies, checkUserExists, getUser, storeResetToken, storeUser, isTokenExpired, getUserByResetToken, updatePassword, RegistrationData, Credentials } from './utils';
import { hashPassword, comparePassword } from './auth';
import { createSession, deleteSession, loadSession } from './session';
import { sendEmail } from './email';

// Handles loading user data based on the session ID extracted from cookies.
/**
* Handles the request to load a user.
*
* @param request - The incoming request object.
* @param env - The environment object.
* @returns A Promise that resolves to a Response object.
*/
export async function handleLoadUser(request: Request, env: Env): Promise<Response> {
const sessionId = getSessionIdFromCookies(request);
if (sessionId) {
const sessionData = await loadSession(env, sessionId);
if (sessionData) {
return new Response(JSON.stringify(sessionData), {
headers: { 'Content-Type': 'application/json' }
});
}
}
return new Response(JSON.stringify({ error: 'User not logged in' }), { status: 401 });
}

// Processes user registration requests, including validation, password hashing, and database insertion.
/* `handleRegister` is a function that processes user registration requests. It performs the
following tasks:
1. Parses the registration data from the incoming request.
2. Checks if the required fields (username and password) are present in the registration data.
3. Checks if the user already exists in the database.
4. Hashes the user's password for secure storage.
5. Stores the user's information (username, hashed password, first name, last name) in the
database.
6. Returns a response indicating whether the user registration was successful or if any errors
occurred during the process. */
export async function handleRegister(request: Request, env: Env): Promise<Response> {
try {
const regData = await request.json() as RegistrationData;
const { username, password, firstName, lastName } = regData;

if (!username || !password) {
return new Response(JSON.stringify({ error: 'Missing required fields' }), { status: 400 });
}

const userExists = await checkUserExists(env, username);
if (userExists) {
return new Response(JSON.stringify({ error: 'User already exists' }), { status: 409 });
}

const hashedPassword = await hashPassword(password);
await storeUser(env, { username, hashedPassword, firstName, lastName });

return new Response(JSON.stringify({ message: 'User registered successfully' }), { status: 201 });
} catch (error) {
console.error('Error during registration:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500 });
}
}

// Authenticates users by validating credentials and creating a session on successful login.
/**
* Handles the login request.
*
* @param request - The incoming request object.
* @param env - The environment object.
* @returns A Promise that resolves to a Response object.
*/
export async function handleLogin(request: Request, env: Env): Promise<Response> {
const credentials = await request.json() as Credentials;
const { username, password } = credentials;

try {
if (!username || !password) {
return new Response(JSON.stringify({ error: 'Missing username or password' }), { status: 400 });
}

const user = await getUser(env, username);
if (!user) {
return new Response(JSON.stringify({ error: 'Invalid credentials' }), { status: 401 });
}

const passwordMatch = await comparePassword(password, user.Password as string);
if (!passwordMatch) {
return new Response(JSON.stringify({ error: 'Invalid credentials' }), { status: 401 });
}

const sessionId = await createSession(env, user);
return new Response(JSON.stringify({ message: 'Login successful' }), {
headers: { 'Set-Cookie': `cfw_session=${sessionId}; Secure; Path=/; SameSite=None; Max-Age=${60 * 30}` }
});
} catch (error) {
console.error('Error during login:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500 });
}
}

// Ends a user's session and clears session-related data.
/**
* This function handles the logout process for a user.
* @param {Request} request - The `request` parameter represents the incoming request to the server, containing information
* such as headers, body, method, and other details sent by the client.
* @param {Env} env - The `env` parameter typically refers to the environment variables that are available to your
* serverless function. These variables can be used to store sensitive information or configuration settings that your
* function may need.
*/
export async function handleLogout(request: Request, env: Env): Promise<Response> {
const sessionId = getSessionIdFromCookies(request);
if (sessionId) {
await deleteSession(env, sessionId);
}

const headers = new Headers({
'Set-Cookie': 'cfw_session=; HttpOnly; Secure; SameSite=Strict; Max-Age=0',
});

return new Response(JSON.stringify({ message: 'Logout successful' }), { headers });
}

// Placeholder for initiating the password reset process.
/**
* The function `handleForgotPassword` handles the process of initiating a password reset for a user by generating a reset
* token, storing it, sending a reset link via email, and returning a success message or error response.
* @param {Request} request - The `request` parameter in the `handleForgotPassword` function is of type `Request`, which
* likely represents an incoming HTTP request containing data such as the username for which the password reset is
* requested. This parameter is used to extract the necessary information from the request body to initiate the password
* reset process.
* @param {Env} env - The `env` parameter typically represents the environment configuration or settings needed for the
* function to operate correctly. This can include things like database connections, API keys, email service
* configurations, and other environment-specific variables required for the function to run in different environments
* (e.g., development, staging, production).
* @returns The `handleForgotPassword` function returns a `Response` object. If the user is not found, it returns a
* response with a 404 status and an error message. If the password reset is initiated successfully, it returns a response
* with a success message. If there is an error during the process, it returns a response with a 500 status and an error
* message.
*/
export async function handleForgotPassword(request: Request, env: Env): Promise<Response> {
try {
const { username } = await request.json() as { username: string };
const user = await getUser(env, username);
if (!user) {
return new Response(JSON.stringify({ error: 'User not found' }), { status: 404 });
}

const resetToken = crypto.getRandomValues(new Uint8Array(16)).join('');
await storeResetToken(env, username, resetToken);

const resetLink = `${getForgotPasswordUrl(env)}?token=${resetToken}`;
const toEmail = username;
const toName = `${user.FirstName} ${user.LastName}`;
const subject = 'Password Reset Link';
const contentValue = `Click the following link to reset your password: ${resetLink}`;
await sendEmail(toEmail, toName, subject, contentValue, env);

return new Response(JSON.stringify({ message: 'Password reset initiated' }));
} catch (error) {
console.error('Error during password reset:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500 });
}
}

// Validates a password reset token.
/**
* The function `handleForgotPasswordValidate` validates a reset token for a user and returns an appropriate response based
* on the token's validity.
* @param {Request} request - The `request` parameter in the `handleForgotPasswordValidate` function is of type `Request`,
* which represents an incoming HTTP request. It contains information such as headers, body, and other request details sent
* by the client to the server. In this function, the request body is expected to contain a
* @param {Env} env - The `env` parameter typically refers to the environment object that contains configuration settings,
* environment variables, and other resources needed for the application to run. It is commonly used to access database
* connections, API keys, and other external services.
* @returns The function `handleForgotPasswordValidate` returns a `Response` object with a JSON stringified message
* indicating whether the token is valid or not. If the token is invalid, it returns an error message with status code 400.
* If the token is valid, it returns a message indicating that the token is valid.
*/
export async function handleForgotPasswordValidate(request: Request, env: Env): Promise<Response> {
const { token } = await request.json() as { token: string };
const user = await getUserByResetToken(env, token);
if (!user) {
return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 400 });
}

const tokenExpired = isTokenExpired(env, user.ResetTokenTime as number);
if (tokenExpired) {
return new Response(JSON.stringify({ error: 'Token expired' }), { status: 400 });
}

return new Response(JSON.stringify({ message: 'Valid Token' }));
}

// Sets a new password for the user after validating the reset token.
/**
* The function `handleForgotPasswordNewPassword` handles the process of resetting a user's password using a reset token
* and updating the password in the system.
* @param {Request} request - The `request` parameter in the `handleForgotPasswordNewPassword` function is of type
* `Request`, which likely represents an HTTP request object containing data sent by the client to the server. This object
* may include information such as headers, body content, and request method.
* @param {Env} env - The `env` parameter in the `handleForgotPasswordNewPassword` function likely represents the
* environment configuration or context needed for the function to interact with external services or resources. This could
* include database connections, API keys, or other settings required for the function to execute successfully. It is
* typically passed in to
* @returns The function `handleForgotPasswordNewPassword` returns a `Promise` that resolves to a `Response` object. The
* response can contain either a success message indicating that the password reset was successful, or an error message in
* case of an invalid token or internal server error.
*/
export async function handleForgotPasswordNewPassword(request: Request, env: Env): Promise<Response> {
try {
const { token, password } = await request.json() as { token: string, password: string };
const user = await getUserByResetToken(env, token);
if (!user) {
return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 400 });
}

const hashedPassword = await hashPassword(password);
await updatePassword(env, user.Username, hashedPassword);

return new Response(JSON.stringify({ message: 'Password reset successful' }));
} catch (error) {
console.error('Error resetting password:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500 });
}
}
Loading

0 comments on commit e903d9f

Please sign in to comment.