Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 70 additions & 35 deletions apps/api/src/lib/functions/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,33 @@ import { type Context } from "hono";
import { isInDevMode } from ".";

/**
* Fetches a database dump from a Turso database instance.
* @param databseName The name of the database.
* @param organizationSlug The organization slug associated with the database.
* Checks if a user has site admin privileges.
* @param permissionEnum - The user's site role
* @returns True if the user is an ADMIN or SUPER_ADMIN, false otherwise
*/
export async function getDatabaseDumpTurso(
databseName: string,
organizationSlug: string,
) {
const res = await fetch(
`https://${databseName}-${organizationSlug}.turso.io/dump`,
{
method: "GET",
headers: new Headers({
// Authorization: `Bearer ${env.BACKUPS_DB_BEARER}`,
}),
},
);

if (!res.ok) {
throw new Error(
`Failed to get database dump: ${res.status} ${res.statusText}`,
);
}

return res.text();
}

/**
* Checks the database connection by pinging it and querying for it's table count.
* Function will take in an database information and the type to make the appropriate query.
*/
export async function pingDatabase() {}

export function isSiteAdminUser(
permissionEnum: NonNullable<UserType>["siteRole"],
): boolean {
return ["ADMIN", "SUPER_ADMIN"].some((role) => role === permissionEnum);
}

/**
* Retrieves a team by its ID from the database.
* @param teamId - The unique identifier of the team
* @returns The team object if found, undefined otherwise
*/
export async function findTeam(teamId: string) {
return db.query.team.findFirst({
where: eq(team.id, teamId),
});
}

/**
* Removes a user from a team by deleting their userToTeam relationship.
* @param userId - The unique identifier of the user
* @param teamId - The unique identifier of the team
* @returns The teamId that was deleted
*/
export async function leaveTeam(userId: string, teamId: string) {
return db
.delete(userToTeam)
Expand All @@ -59,6 +41,12 @@ export async function leaveTeam(userId: string, teamId: string) {
.returning({ teamId: userToTeam.teamId });
}

/**
* Retrieves the admin relationship between a user and a team.
* @param userId - The unique identifier of the user
* @param teamId - The unique identifier of the team
* @returns The userToTeam record if the user is an admin of the team, undefined otherwise
*/
export async function getAdminUserForTeam(userId: string, teamId: string) {
return db.query.userToTeam.findFirst({
where: and(
Expand All @@ -69,6 +57,13 @@ export async function getAdminUserForTeam(userId: string, teamId: string) {
});
}

/**
* Retrieves a specific team join request by ID, user ID, and team ID.
* @param requestId - The unique identifier of the join request
* @param userId - The unique identifier of the user who made the request
* @param teamId - The unique identifier of the team
* @returns The join request record if found, undefined otherwise
*/
export async function getJoinTeamRequest(
requestId: string,
userId: string,
Expand All @@ -83,6 +78,13 @@ export async function getJoinTeamRequest(
});
}

/**
* Retrieves a team join request by ID and team ID (admin view).
* Does not require user ID validation.
* @param requestId - The unique identifier of the join request
* @param teamId - The unique identifier of the team
* @returns The join request record if found, undefined otherwise
*/
export async function getJoinTeamRequestAdmin(
requestId: string,
teamId: string,
Expand All @@ -95,7 +97,14 @@ export async function getJoinTeamRequestAdmin(
});
}

// TODO: This function is lowkey pivotal so we should ensure it is WAI.
/**
* Checks if a user is a site admin OR if a provided query returns a truthy result.
* Useful for authorization checks that can shortcut if the user is already a site admin.
*
* @param userSiteRole - The site role of the user
* @param query - Either a Promise that resolves to a permission check result, or a function that returns such a Promise
* @returns True if the user is a site admin or if the query resolves to a truthy value, false otherwise
*/
export async function isUserSiteAdminOrQueryHasPermissions<T = unknown>(
userSiteRole: SiteRoleType,
// Accept either a Promise (already invoked query) or a function that returns a Promise
Expand All @@ -109,21 +118,43 @@ export async function isUserSiteAdminOrQueryHasPermissions<T = unknown>(
return !!result;
}

/**
* Logs an error message to the database with context information.
* @param message - The error message to log
* @param c - Optional Hono context for extracting request metadata
*/
export async function logError(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("ERROR", message, options);
}

/**
* Logs an info message to the database with context information.
* @param message - The info message to log
* @param c - Optional Hono context for extracting request metadata
*/
export async function logInfo(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("INFO", message, options);
}

/**
* Logs a warning message to the database with context information.
* @param message - The warning message to log
* @param c - Optional Hono context for extracting request metadata
*/
export async function logWarning(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("WARNING", message, options);
}

/**
* Inserts a log record into the database. In development mode, logs to console instead.
* Silently fails if database insertion fails to prevent cascading errors.
* @param loggingType - The type of log (ERROR, INFO, WARNING, etc.)
* @param message - The log message
* @param options - Optional logging metadata (user ID, team ID, route, request ID)
*/
export async function logToDb(
loggingType: LoggingType,
message: string,
Expand All @@ -145,6 +176,11 @@ export async function logToDb(
}
}

/**
* Extracts relevant context values from a Hono request context for logging purposes.
* @param c - Optional Hono context
* @returns An object containing route, userId, teamId, and requestId, or undefined if no context provided
*/
function getAllContextValues(c?: Context): LoggingOptions | undefined {
if (!c) {
return undefined;
Expand All @@ -161,8 +197,7 @@ function getAllContextValues(c?: Context): LoggingOptions | undefined {
/**
* Safely extract an error code string from an unknown thrown value from a db error.
* Returns the code as a string when present, otherwise null.
*
* This function can handle it being passed as either a number or string and will convert if need be
* @param e - The unknown error object thrown from a database operation
*/
export function maybeGetDbErrorCode(e: unknown): string | null {
if (e == null) return null;
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/lib/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { BlankEnv } from "hono/types";
import type { ApiContextVariables } from "../types";

/**
* @description Wrapper for the Hono constructor that includes the BetterAuth types
* Wrapper for the Hono constructor that includes the BetterAuth types
* @param options Hono options
* @returns A new Hono instance with the BetterAuth types included
*/
export function HonoBetterAuth(options?: HonoOptions<BlankEnv> | undefined) {
return new Hono<{
Expand All @@ -16,6 +17,10 @@ export function HonoBetterAuth(options?: HonoOptions<BlankEnv> | undefined) {
}

// TODO(https://github.com/acmutsa/Fallback/issues/38): Come back and find out what proper value needs to be here
/**
* Utility function to check if the application is running in development mode.
* @returns True if in development mode, false otherwise.
*/
export function isInDevMode() {
return process.env.NODE_ENV === "development";
}
20 changes: 15 additions & 5 deletions apps/api/src/lib/functions/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import type { ApiContext } from "../types";
import { API_ERROR_MESSAGES } from "shared";

export const MIDDLEWARE_PUBLIC_ROUTES = ["/health", "/api/auth"];

/**
* Middleware to set user and session context for each request. This middleware checks the authentication status of the incoming request, retrieves the user session if it exists, and sets relevant information in the context for downstream handlers to use. It also logs the request path and authentication status for monitoring purposes.
* @param c - The Hono context object
* @param next - The next middleware function in the chain
*/
export async function setUserSessionContextMiddleware(c: Context, next: Next) {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
const userString = session
? `Authenticated user (id: ${session?.user.id})`
? `Authenticated user (id: ${session.user.id})`
: "Unauthenticated User";

const requestId = nanoid();
Expand All @@ -30,8 +34,12 @@ export async function setUserSessionContextMiddleware(c: Context, next: Next) {
await next();
}

/**
* Middleware to enforce authentication on protected routes. This middleware checks if the incoming request is targeting a public route, and if not, it verifies that the user is authenticated by checking the context for user and session information. If the user is not authenticated, it logs an unauthorized access attempt and returns a 401 response. If the user is authenticated or if the route is public, it allows the request to proceed to the next handler.
* @param c - The Hono context object
* @param next - The next middleware function in the chain
*/
export async function authenticatedMiddleware(c: ApiContext, next: Next) {
// First check if it is a public route and if so we will return (make sure this works)
const isPublicRoute = MIDDLEWARE_PUBLIC_ROUTES.some((route) =>
c.req.path.startsWith(route),
);
Expand All @@ -53,8 +61,10 @@ export async function authenticatedMiddleware(c: ApiContext, next: Next) {
return next();
}

/*
* Middleware to handle logging the request and results of request afterwards. Context object is apparently stateful
/**
* Middleware to perform actions after the main route logic has executed. This can be used for logging, cleanup, or other post-processing tasks.
* @param c - The Hono context object
* @param next - The next middleware function in the chain
*/
export async function afterRouteLogicMiddleware(c: ApiContext, next: Next) {
// TODO(https://github.com/acmutsa/Fallback/issues/26): Come back and finish logging function
Expand Down