diff --git a/.github/workflows/presubmits.yml b/.github/workflows/presubmits.yml index fbae8f4..6e39a3d 100644 --- a/.github/workflows/presubmits.yml +++ b/.github/workflows/presubmits.yml @@ -7,6 +7,7 @@ on: [pull_request] jobs: prettier-check: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - name: Checkout code @@ -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 diff --git a/apps/api/src/lib/functions/database.ts b/apps/api/src/lib/functions/database.ts index c9f47e6..cc6eb24 100644 --- a/apps/api/src/lib/functions/database.ts +++ b/apps/api/src/lib/functions/database.ts @@ -1,6 +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 { LoggingOptions, LoggingType, LoggingSource } from "../types"; import { type Context } from "hono"; import { isInDevMode } from "."; @@ -125,7 +125,8 @@ export async function isUserSiteAdminOrQueryHasPermissions( */ 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); } /** @@ -135,7 +136,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); } /** @@ -145,7 +147,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); } /** @@ -156,19 +159,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. @@ -194,6 +202,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. diff --git a/apps/api/src/lib/functions/middleware.ts b/apps/api/src/lib/functions/middleware.ts index d3a0e55..9480ecc 100644 --- a/apps/api/src/lib/functions/middleware.ts +++ b/apps/api/src/lib/functions/middleware.ts @@ -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 diff --git a/apps/api/src/lib/types.ts b/apps/api/src/lib/types.ts index 33ef5c7..35ed34d 100644 --- a/apps/api/src/lib/types.ts +++ b/apps/api/src/lib/types.ts @@ -15,7 +15,9 @@ export type ApiContext = Context<{ 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"]; diff --git a/packages/db/drizzle/0012_lumpy_the_anarchist.sql b/packages/db/drizzle/0012_lumpy_the_anarchist.sql new file mode 100644 index 0000000..1cc3aeb --- /dev/null +++ b/packages/db/drizzle/0012_lumpy_the_anarchist.sql @@ -0,0 +1 @@ +ALTER TABLE `log` ADD `source` text NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0012_snapshot.json b/packages/db/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..8e926bb --- /dev/null +++ b/packages/db/drizzle/meta/0012_snapshot.json @@ -0,0 +1,842 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3e77dc7c-fc5e-4778-b47a-608216379cf9", + "prevId": "39b0cf06-8d6d-489a-875e-cc3b19d55c00", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_job": { + "name": "backup_job", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authentication_data": { + "name": "authentication_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "database_type": { + "name": "database_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron_string": { + "name": "cron_string", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_job_team_id_team_id_fk": { + "name": "backup_job_team_id_team_id_fk", + "tableFrom": "backup_job", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_job_run": { + "name": "backup_job_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "invocation_type": { + "name": "invocation_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_job_id": { + "name": "backup_job_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_job_run_backup_job_id_backup_job_id_fk": { + "name": "backup_job_run_backup_job_id_backup_job_id_fk", + "tableFrom": "backup_job_run", + "tableTo": "backup_job", + "columnsFrom": [ + "backup_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "log": { + "name": "log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "log_type": { + "name": "log_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route": { + "name": "route", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_elapsed_ms": { + "name": "time_elapsed_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team": { + "name": "team", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite": { + "name": "team_invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'MEMBER'" + } + }, + "indexes": {}, + "foreignKeys": { + "team_invite_team_id_team_id_fk": { + "name": "team_invite_team_id_team_id_fk", + "tableFrom": "team_invite", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_join_request": { + "name": "team_join_request", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PENDING'" + } + }, + "indexes": {}, + "foreignKeys": { + "team_join_request_team_id_team_id_fk": { + "name": "team_join_request_team_id_team_id_fk", + "tableFrom": "team_join_request", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_join_request_user_id_user_id_fk": { + "name": "team_join_request_user_id_user_id_fk", + "tableFrom": "team_join_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_name": { + "name": "last_name", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "site_role": { + "name": "site_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USER'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_to_team": { + "name": "user_to_team", + "columns": { + "user_id": { + "name": "user_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'MEMBER'" + }, + "joined_on": { + "name": "joined_on", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_to_team_user_id_user_id_fk": { + "name": "user_to_team_user_id_user_id_fk", + "tableFrom": "user_to_team", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_to_team_team_id_team_id_fk": { + "name": "user_to_team_team_id_team_id_fk", + "tableFrom": "user_to_team", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_to_team_user_id_team_id_pk": { + "columns": [ + "user_id", + "team_id" + ], + "name": "user_to_team_user_id_team_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 3395974..8c83481 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1771399307185, "tag": "0011_robust_sabretooth", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1771710466128, + "tag": "0012_lumpy_the_anarchist", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 192479d..2e2d328 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -6,6 +6,7 @@ import { primaryKey, } from "drizzle-orm/sqlite-core"; import { nanoid } from "nanoid"; +import { db } from "."; const STANDARD_NANOID_SIZE = 12; const STANDARD_VARCHAR_LENGTH = 255; @@ -43,6 +44,9 @@ const siteRoleType = text({ enum: ["SUPER_ADMIN", "ADMIN", "USER"] }); const teamJoinRequestStatusType = text({ enum: ["PENDING", "APPROVED", "REJECTED", "RESCINDED"], }); +const loggingSourceType = text({ + enum: ["SERVER", "BACKUP_SERVICE", "CLIENT"], +}); // User Table - Partially generated based on Better Auth requirements. Modify with extreme caution. export const user = sqliteTable("user", { @@ -194,6 +198,7 @@ export const log = sqliteTable("log", { logType: logType.notNull(), message: standardVarcharFactory(), occurredAt: standardDateFactory(), + source: loggingSourceType.notNull(), // TODO(https://github.com/acmutsa/Fallback/issues/39): All of these fields are nullable because not all logs have the same info. There might be a better approach. teamId: standardVarcharFactoryNullable(), userId: standardVarcharFactoryNullable(),