Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .github/workflows/presubmits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on: [pull_request]
jobs:
prettier-check:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}

steps:
- name: Checkout code
Expand All @@ -28,6 +29,7 @@ jobs:
run: pnpm run format-check
builder:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}

steps:
- name: Checkout code
Expand Down
31 changes: 23 additions & 8 deletions apps/api/src/lib/functions/database.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { userToTeam, db, and, eq, log, team, teamJoinRequest } from "db";
import type { UserType, SiteRoleType } from "db/types";
import type { LoggingOptions, LoggingType } from "../types";
import { type Context } from "hono";
import type { LoggingOptions, LoggingType, LoggingSource } from "../types";
import { isInDevMode } from ".";

/**
Expand Down Expand Up @@ -125,7 +124,8 @@ export async function isUserSiteAdminOrQueryHasPermissions<T = unknown>(
*/
export async function logError(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("ERROR", message, options);
const source = getLoggingSourceFromContext(c);
await logToDb("ERROR", message, source, options);
}

/**
Expand All @@ -135,7 +135,8 @@ export async function logError(message: string, c?: Context) {
*/
export async function logInfo(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("INFO", message, options);
const source = getLoggingSourceFromContext(c);
await logToDb("INFO", message, source, options);
}

/**
Expand All @@ -145,7 +146,8 @@ export async function logInfo(message: string, c?: Context) {
*/
export async function logWarning(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("WARNING", message, options);
const source = getLoggingSourceFromContext(c);
await logToDb("WARNING", message, source, options);
}

/**
Expand All @@ -156,19 +158,24 @@ export async function logWarning(message: string, c?: Context) {
* @param options - Optional logging metadata (user ID, team ID, route, request ID)
*/
export async function logToDb(
loggingType: LoggingType,
logType: LoggingType,
message: string,
source: LoggingSource,
options?: LoggingOptions,
) {
if (isInDevMode()) {
console.log(`[${loggingType}] - ${message} - Options: `, options);
console.log(
`[${logType}] from ${source} - ${message} - Options: `,
options,
);
return;
}
try {
await db.insert(log).values({
...options,
logType: loggingType,
logType,
message,
source,
});
} catch (e) {
// Silently fail if logging to the db fails.
Expand All @@ -194,6 +201,14 @@ function getAllContextValues(c?: Context): LoggingOptions | undefined {
};
}

function getLoggingSourceFromContext(c?: Context): LoggingSource {
if (!c) {
return "SERVER";
}

return "SERVER";
}

/**
* 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.
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/lib/functions/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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
Expand Down
11 changes: 8 additions & 3 deletions apps/api/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { log } from "db";
import type { SessionType, UserType } from "db/types";
import type { Context } from "hono";
import type { Context as HonoContext } from "hono";
import { LambdaContext } from "hono/aws-lambda";

// Match the Variables shape declared in HonoBetterAuth
export type ApiContextVariables = {
Expand All @@ -9,13 +10,17 @@ export type ApiContextVariables = {
teamId: string | null;
requestId: string | null;
};
export type ApiContext = Context<{
export type ApiContext = HonoContext<{
Variables: ApiContextVariables;
}>;

export type LoggingOptions = Omit<
typeof log.$inferInsert,
"id" | "occurredAt" | "logType" | "message"
"id" | "occurredAt" | "logType" | "message" | "source"
>;
// Single type representing the logType value (e.g. "INFO" | "WARNING" | "ERROR")
export type LoggingType = (typeof log.$inferSelect)["logType"];

export type LoggingSource = (typeof log.$inferSelect)["source"];

export type FallbackContext = {honoContext?: HonoContext; lambdaContext?: LambdaContext };
6 changes: 3 additions & 3 deletions apps/api/src/routes/log.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { zValidator } from "@hono/zod-validator";
import { HonoBetterAuth } from "../lib/functions";
import { logSchema } from "../lib/zod";
import { logToDb } from "../lib/functions/database";
import type { LoggingType } from "../lib/types";
import { teamIdSchema } from "shared";
import { db, eq, log } from "db";
Expand All @@ -11,18 +10,19 @@ import {
getAdminUserForTeam,
isSiteAdminUser,
} from "../lib/functions/database";
import { logToDb } from "../lib/functions/database";

// TODO(https://github.com/acmutsa/Fallback/issues/36): We need to allow authenticated users to log client errors, but we should rethink this a bit to add extra protections against abuse.
const logHandler = HonoBetterAuth()
.post("/", zValidator("form", logSchema), async (c) => {
const logData = c.req.valid("form");

const { message, logType, ...optionals } = logData;
await logToDb(logType as LoggingType, message, {
await logToDb(logType as LoggingType, message, "CLIENT",{
...optionals,
});

return c.json({ message: "Log endpoint hit" }, 200);
return c.json({ message: "Log successful" }, 200);
})
.get("/admin/all", async (c) => {
const user = c.get("user");
Expand Down
1 change: 1 addition & 0 deletions packages/db/drizzle/0012_lumpy_the_anarchist.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `log` ADD `source` text NOT NULL;
Loading