@getmocha/users-service
Version:
An API client, Hono middleware, and a React provider for the Mocha Users Service
145 lines (144 loc) • 5.45 kB
JavaScript
/// <reference types="hono" />
import { getCookie } from 'hono/cookie';
import { createMiddleware } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
export const DEFAULT_MOCHA_USERS_SERVICE_API_URL = 'https://getmocha.com/u';
export const MOCHA_SESSION_TOKEN_COOKIE_NAME = 'mocha_session_token';
export const SUPPORTED_OAUTH_PROVIDERS = ['google'];
/**
* Fetch the OAuth redirect URL from the Mocha Users Service.
* @param provider - The OAuth provider to use (currently only "google" is supported)
* @param options - Configuration options including API key and optional API URL
* @returns The redirect URL to initiate the OAuth flow
*/
export async function getOAuthRedirectUrl(provider, options) {
if (!SUPPORTED_OAUTH_PROVIDERS.includes(provider)) {
throw new Error(`Unsupported OAuth provider: ${provider}`);
}
const apiUrl = options.apiUrl || DEFAULT_MOCHA_USERS_SERVICE_API_URL;
const response = await fetch(`${apiUrl}/oauth/${provider}/redirect_url`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-api-key': options.apiKey,
},
});
if (!response.ok) {
throw new Error(`Failed to get redirect URL for provider ${provider}: ${response.statusText}`);
}
const { redirect_url } = await response.json();
return redirect_url;
}
/**
* Exchanges a code for a session token using the Mocha Users Service.
* @param code - The OAuth code received after successful authentication
* @param options - Configuration options including API key and optional API URL
* @returns The session token to use for authenticated requests
*/
export async function exchangeCodeForSessionToken(code, options) {
const apiUrl = options.apiUrl || DEFAULT_MOCHA_USERS_SERVICE_API_URL;
const response = await fetch(`${apiUrl}/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': options.apiKey,
},
body: JSON.stringify({ code }),
});
if (!response.ok) {
throw new Error(`Failed to exchange code for session token: ${response.statusText}`);
}
const { session_token } = await response.json();
return session_token;
}
/**
* Fetch the current user by their session token from the Mocha Users Service.
* @param sessionToken - The session token obtained from exchangeCodeForSessionToken
* @param options - Configuration options including API key and optional API URL
* @returns The user object or null if the session is invalid
*/
export async function getCurrentUser(sessionToken, options) {
const apiUrl = options.apiUrl || DEFAULT_MOCHA_USERS_SERVICE_API_URL;
try {
const response = await fetch(`${apiUrl}/users/me`, {
method: 'GET',
headers: {
Authorization: `Bearer ${sessionToken}`,
'x-api-key': options.apiKey,
},
});
if (!response.ok) {
return null;
}
const { data: user } = await response.json();
return user;
}
catch (error) {
console.error('Error validating session:', error);
return null;
}
}
/**
* Delete the current session in the Mocha Users Service when logging out.
* @param sessionToken - The users session token from their cookie.
* @param options - Configuration options including API key and optional API URL
*/
export async function deleteSession(sessionToken, options) {
const apiUrl = options.apiUrl || DEFAULT_MOCHA_USERS_SERVICE_API_URL;
try {
await fetch(`${apiUrl}/sessions`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${sessionToken}`,
'x-api-key': options.apiKey,
},
});
}
catch (error) {
console.error('Error deleting session:', error);
}
}
/**
* Hono middleware that authenticates requests against the Mocha Users Service.
*
* This middleware requests the current user using the session token stored in
* cookies. If the request fails to return a valid user object, the middleware
* throws an HTTPException with status 401. On success, it sets the authenticated
* user in the Hono context for use in subsequent route handlers.
*
* Use this to protect routes and load the current user.
*
* @throws {HTTPException} 401 - When session token is invalid or not provided
*
* @example
*
* // Fetch the authenticated user's todos.
* // Doesn't execute if the user is not authenticated.
* app.get("/api/todos", authMiddleware, async (c) => {
* const user = c.get("user");
*
* const { results } = await c.env.DB.prepare(
* "SELECT * FROM todos WHERE user_id = ? ORDER BY created_at DESC"
* )
* .bind(user.id)
* .all();
*
* return c.json(results);
* });
*/
export const authMiddleware = createMiddleware(async (c, next) => {
const sessionToken = getCookie(c, MOCHA_SESSION_TOKEN_COOKIE_NAME);
if (typeof sessionToken !== 'string') {
return c.json({ error: 'Unauthorized' }, 401);
}
const options = {
apiUrl: c.env.MOCHA_USERS_SERVICE_API_URL,
apiKey: c.env.MOCHA_USERS_SERVICE_API_KEY,
};
const user = await getCurrentUser(sessionToken, options);
if (!user) {
throw new HTTPException(401, { message: 'Invalid session token' });
}
c.set('user', user);
await next();
});