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
5 changes: 5 additions & 0 deletions .changeset/calm-experts-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/api": minor
---

Added procedures to retrieve and update the logger's level
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ DIRECT_URL=postgresql://blobscan:s3cr3t@localhost:5432/blobscan_dev?schema=publi
BLOBSCAN_WEB_TAG=next
BLOBSCAN_API_TAG=next
INDEXER_TAG=master
ADMIN_API_KEY=secretkey

### blobscan website

Expand Down
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ S3_STORAGE_ENABLED=true
S3_STORAGE_ENDPOINT=http://localhost:9090
S3_STORAGE_FORCE_PATH_STYLE=true

ADMIN_API_KEY=secretkey

BEE_ENDPOINT=http://localhost:1633
SWARM_BATCH_ID=f89e63edf757f06e89933761d6d46592d03026efb9871f9d244f34da86b6c242

Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/docs/environment/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nextjs:

| Variable | Description | Required | Default value |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------- |
| `ADMIN_API_KEY` | API key used to authenticate requests to admin endpoints (e.g., logging level management) | No | (empty) |
| `BLOB_DATA_API_ENABLED` | Controls whether the blob data API endpoint is enabled | No | `true` |
| `BLOB_DATA_API_KEY` | API key used to authenticate requests to retrieve blob data from the blobscan API | No | (empty) |
| `CHAIN_ID` | EVM chain id | Yes | `1` |
Expand Down
12 changes: 8 additions & 4 deletions apps/rest-api-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ async function main() {
enableTracing: env.TRACES_ENABLED,
prisma,
scope: "rest-api",
serviceApiKeys: {
blobDataReadKey: env.BLOB_DATA_API_KEY,
indexerServiceSecret: env.SECRET_KEY,
loadNetworkServiceKey: env.WEAVEVM_API_KEY,
apiKeys: {
accesses: {
blobDataRead: env.BLOB_DATA_API_KEY,
},
services: {
indexer: env.SECRET_KEY,
loadNetwork: env.WEAVEVM_API_KEY,
},
},
}),
onError({ error }) {
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/pages/api/trpc/[...trpc].ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export default createNextApiHandler({
chainId: env.CHAIN_ID,
prisma,
enableTracing: env.TRACES_ENABLED,
serviceApiKeys: {
blobDataReadKey: env.BLOB_DATA_API_KEY,
apiKeys: {
accesses: {
blobDataRead: env.BLOB_DATA_API_KEY,
},
},
}),
onError({ error }) {
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/app-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { blockchainSyncStateRouter } from "./routers/blockchain-sync-state";
import { ethPriceRouter } from "./routers/eth-price";
import { healthcheck } from "./routers/healthcheck";
import { indexerRouter } from "./routers/indexer";
import { loggingRouter } from "./routers/logging";
import { search } from "./routers/search";
import { stateRouter } from "./routers/state";
import { statsRouter } from "./routers/stats";
Expand All @@ -25,6 +26,7 @@ export function createAppRouter(config?: AppRouterConfig) {
blob: createBlobRouter(config?.blobRouter),
blobStoragesState: blobStoragesStateRouter,
block: blockRouter,
loggingRouter,
syncState: blockchainSyncStateRouter,
ethPrice: ethPriceRouter,
indexer: indexerRouter,
Expand Down
34 changes: 21 additions & 13 deletions packages/api/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@ import type {
import type { BlobPropagator } from "@blobscan/blob-propagator";
import type { BlobscanPrismaClient } from "@blobscan/db";

import type { ServiceClient } from "./utils";
import { createServiceClient } from "./utils";
import type { ApiClient } from "./utils";
import { createApiClient } from "./utils";

export type CreateContextOptions =
| NodeHTTPCreateContextFnOptions<NodeHTTPRequest, NodeHTTPResponse>
| CreateNextContextOptions;

type CreateInnerContextOptions = Partial<CreateContextOptions> & {
serviceClient?: ServiceClient;
apiClient?: ApiClient;
blobPropagator?: BlobPropagator;
prisma: BlobscanPrismaClient;
};

export type ServiceApiKeys = Partial<{
indexerServiceSecret: string;
loadNetworkServiceKey: string;
blobDataReadKey: string;
indexer: string;
loadNetwork: string;
}>;

export type AccessKeys = Partial<{
blobDataRead: string;
}>;

export type ApiKeys = Partial<{
services: ServiceApiKeys;
accesses: AccessKeys;
}>;

export type CreateContextParams = {
Expand All @@ -35,20 +43,20 @@ export type CreateContextParams = {
prisma: BlobscanPrismaClient;
enableTracing?: boolean;
scope: ContextScope;
serviceApiKeys?: ServiceApiKeys;
apiKeys?: ApiKeys;
};

export type TRPCInnerContext = {
prisma: BlobscanPrismaClient;
blobPropagator?: BlobPropagator;
apiClient?: ServiceClient;
apiClient?: ApiClient;
};

export function createTRPCInnerContext(opts: CreateInnerContextOptions) {
return {
prisma: opts.prisma,
blobPropagator: opts.blobPropagator,
apiClient: opts.serviceClient,
apiClient: opts.apiClient,
};
}

Expand All @@ -60,17 +68,17 @@ export function createTRPCContext({
chainId,
enableTracing,
scope,
serviceApiKeys,
apiKeys,
}: CreateContextParams) {
return async (opts: CreateContextOptions) => {
try {
const serviceClient = serviceApiKeys
? createServiceClient(serviceApiKeys, opts.req)
const apiClient = apiKeys
? createApiClient(apiKeys, opts.req)
: undefined;

const innerContext = createTRPCInnerContext({
prisma,
serviceClient,
apiClient,
blobPropagator,
});

Expand Down
26 changes: 26 additions & 0 deletions packages/api/src/routers/logging/getLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { LoggerLevel } from "@blobscan/logger";
import { logger, logLevelEnum } from "@blobscan/logger";
import { z } from "@blobscan/zod";

import { createAuthedProcedure } from "../../procedures";

export const getLevel = createAuthedProcedure("admin")
.meta({
openapi: {
method: "GET",
path: "/logging/level",
summary: "Get the current logging level",
tags: ["system"],
},
})
.input(z.void())
.output(
z.object({
level: logLevelEnum,
})
)
.query(() => {
return {
level: logger.level as LoggerLevel,
};
});
8 changes: 8 additions & 0 deletions packages/api/src/routers/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { t } from "../../trpc-client";
import { getLevel } from "./getLevel";
import { updateLevel } from "./updateLevel";

export const loggingRouter = t.router({
getLevel,
updateLevel,
});
38 changes: 38 additions & 0 deletions packages/api/src/routers/logging/updateLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { LoggerLevel } from "@blobscan/logger";
import { logger, logLevelEnum } from "@blobscan/logger";
import { z } from "@blobscan/zod";

import { createAuthedProcedure } from "../../procedures";
import { toLogLevelSchema } from "../../zod-schemas";

export const updateLevel = createAuthedProcedure("admin")
.meta({
openapi: {
method: "PUT",
path: "/logging/level",
summary: "Change the logging level",
tags: ["system"],
},
})
.input(
z.object({
level: toLogLevelSchema,
})
)
.output(
z.object({
level: logLevelEnum,
previousLevel: logLevelEnum,
})
)
.mutation(({ input }) => {
const { level } = input;
const previousLevel = logger.level as LoggerLevel;

logger.level = level;

return {
level,
previousLevel,
};
});
27 changes: 16 additions & 11 deletions packages/api/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NodeHTTPRequest } from "@trpc/server/adapters/node-http";
import jwt from "jsonwebtoken";

import type { ServiceApiKeys } from "../context";
import type { ApiKeys } from "../context";

type NextHTTPRequest = CreateNextContextOptions["req"];

type HTTPRequest = NodeHTTPRequest | NextHTTPRequest;

export type ServiceClient = "indexer" | "load-network" | "blob-data";
export type ServiceClient = "indexer" | "load-network" | "blob-data" | "admin";

export function createServiceClient(
{
blobDataReadKey,
indexerServiceSecret,
loadNetworkServiceKey,
}: ServiceApiKeys,
export type AccessClient = "blob-data";

export type ApiClient = ServiceClient | AccessClient | "admin";

export function createApiClient(
apiKeys: ApiKeys,
req: HTTPRequest
): ServiceClient | undefined {
const authHeader = req.headers.authorization;
Expand All @@ -31,17 +31,22 @@ export function createServiceClient(
return;
}

const {
accesses: { blobDataRead: blobDataReadKey } = {},
services: { indexer: indexerKey, loadNetwork: loadNetworkKey } = {},
} = apiKeys;

try {
if (blobDataReadKey && blobDataReadKey === token) {
return "blob-data";
}

if (loadNetworkServiceKey && loadNetworkServiceKey === token) {
if (loadNetworkKey && loadNetworkKey === token) {
return "load-network";
}

if (indexerServiceSecret) {
const decoded = jwt.verify(token, indexerServiceSecret) as string;
if (indexerKey) {
const decoded = jwt.verify(token, indexerKey) as string;

if (decoded === token) {
return "indexer";
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/zod-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,25 @@ import {
lowercaseToUpercaseDBRollupSchema,
optimismDecodedFieldsSchema,
} from "@blobscan/db/prisma/zod-utils";
import { logLevelEnum } from "@blobscan/logger";
import { z } from "@blobscan/zod";

export const toLogLevelSchema = z.string().transform((value, ctx) => {
const result = logLevelEnum.safeParse(value);
if (!result.success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
params: {
value,
},
message: "Log level provided is invalid",
});

return z.NEVER;
}

return result.data;
});
export const toBigIntSchema = z.string().transform((value) => BigInt(value));

export const commaSeparatedValuesSchema = z
Expand Down
31 changes: 31 additions & 0 deletions packages/api/test/__snapshots__/logging.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Logging Router > updateLevel > should fail when providing an invalid log level 1`] = `
"[
{
\\"code\\": \\"custom\\",
\\"params\\": {
\\"value\\": \\"unknown-level\\"
},
\\"message\\": \\"Log level provided is invalid\\",
\\"path\\": [
\\"level\\"
]
}
]"
`;

exports[`Logging Router > updateLevel > should fail when providing an invalid log level 2`] = `
[ZodError: [
{
"code": "custom",
"params": {
"value": "unknown-level"
},
"message": "Log level provided is invalid",
"path": [
"level"
]
}
]]
`;
2 changes: 1 addition & 1 deletion packages/api/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export function runExpandsTestsSuite(
});
}

export async function unauthorizedRPCCallTest(rpcCall: () => Promise<unknown>) {
export function unauthorizedRPCCallTest(rpcCall: () => Promise<unknown>) {
it("should fail when calling procedure without auth", async () => {
await expect(rpcCall()).rejects.toThrow(
new TRPCError({ code: "UNAUTHORIZED" })
Expand Down
Loading
Loading