feat(dashboard): setting up layout
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
||||
NODE_ENV=development
|
||||
BASANGO_API_HOST=localhost
|
||||
BASANGO_API_PORT=3000
|
||||
BASANGO_API_PORT=3080
|
||||
BASANGO_API_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
BASANGO_API_KEY=your_api_key_here
|
||||
BASANGO_CRAWLER_KEY=dev
|
||||
BASANGO_JWT_SECRET=your_jwt_secret_here
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"@basango/encryption": "workspace:*",
|
||||
"@basango/logger": "workspace:*",
|
||||
"@hono/node-server": "^1.19.6",
|
||||
"@hono/trpc-server": "^0.4.0",
|
||||
"@hono/zod-openapi": "^1.1.4",
|
||||
"@scalar/hono-api-reference": "^0.9.24",
|
||||
"@trpc/server": "^11.7.1",
|
||||
@@ -16,6 +17,9 @@
|
||||
"zod": "catalog:",
|
||||
"zod-openapi": "^5.4.3"
|
||||
},
|
||||
"exports": {
|
||||
"./trpc/routers/_app": "./src/trpc/routers/_app.ts"
|
||||
},
|
||||
"name": "@basango/api",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -28,6 +28,7 @@ export const { env, config } = defineConfig({
|
||||
"BASANGO_API_ALLOWED_ORIGINS",
|
||||
"BASANGO_API_KEY",
|
||||
"BASANGO_CRAWLER_KEY",
|
||||
"BASANGO_JWT_SECRET",
|
||||
],
|
||||
path: path.join(PROJECT_DIR, ".env"),
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { trpcServer } from "@hono/trpc-server";
|
||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import { cors } from "hono/cors";
|
||||
@@ -5,6 +6,8 @@ import { secureHeaders } from "hono/secure-headers";
|
||||
|
||||
import { config, env } from "@/config";
|
||||
import { routers } from "@/rest/routers";
|
||||
import { createTRPCContext } from "@/trpc/init";
|
||||
import { appRouter } from "@/trpc/routers/_app";
|
||||
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
@@ -21,6 +24,14 @@ app.use(
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(
|
||||
"/trpc/*",
|
||||
trpcServer({
|
||||
createContext: createTRPCContext,
|
||||
router: appRouter,
|
||||
}),
|
||||
);
|
||||
|
||||
app.doc("/openapi", {
|
||||
info: {
|
||||
contact: {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const idSchema = z.uuid().openapi({
|
||||
description: "The unique identifier of the source.",
|
||||
example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g",
|
||||
});
|
||||
|
||||
const biasSchema = z.enum(["neutral", "slightly", "partisan", "extreme"]).openapi({
|
||||
description: "The bias level of the source.",
|
||||
example: "neutral",
|
||||
});
|
||||
const reliabilitySchema = z
|
||||
.enum(["trusted", "reliable", "average", "low_trust", "unreliable"])
|
||||
.openapi({
|
||||
description: "The reliability level of the source.",
|
||||
example: "trusted",
|
||||
});
|
||||
|
||||
const transparencySchema = z.enum(["high", "medium", "low"]).openapi({
|
||||
description: "The transparency level of the source.",
|
||||
example: "high",
|
||||
});
|
||||
|
||||
const credibilitySchema = z
|
||||
.object({
|
||||
bias: biasSchema.default("neutral"),
|
||||
reliability: reliabilitySchema.default("average"),
|
||||
transparency: transparencySchema.default("medium"),
|
||||
})
|
||||
.openapi({
|
||||
description: "Credibility information about the source.",
|
||||
});
|
||||
|
||||
export const createSourceSchema = z.object({
|
||||
credibility: credibilitySchema.optional(),
|
||||
description: z.string().max(1024).optional().openapi({
|
||||
description: "A brief description of the source.",
|
||||
example: "Radio Okapi is a Congolese radio station that provides news and information.",
|
||||
}),
|
||||
displayName: z.string().min(1).max(255).optional().openapi({
|
||||
description: "The display name of the source.",
|
||||
example: "Radio Okapi",
|
||||
}),
|
||||
name: z.string().min(1).max(255).openapi({
|
||||
description: "The name of the source.",
|
||||
example: "radiookapi.com",
|
||||
}),
|
||||
url: z.url().openapi({
|
||||
description: "The URL of the source.",
|
||||
example: "https://techcrunch.com",
|
||||
}),
|
||||
});
|
||||
|
||||
export const getSourceSchema = z.object({
|
||||
id: idSchema,
|
||||
});
|
||||
|
||||
export const updateSourceSchema = z.object({
|
||||
credibility: credibilitySchema.optional(),
|
||||
description: createSourceSchema.shape.description,
|
||||
displayName: createSourceSchema.shape.displayName,
|
||||
id: idSchema,
|
||||
name: createSourceSchema.shape.name.optional(),
|
||||
});
|
||||
|
||||
export const createSourceResponseSchema = z.object({
|
||||
id: idSchema,
|
||||
...createSourceSchema.shape,
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Database, db } from "@basango/db/client";
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import type { Context } from "hono";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { withAuthentication } from "@/trpc/middlewares/auth";
|
||||
import { withDatabase } from "@/trpc/middlewares/db";
|
||||
import { Session, verifyAccessToken } from "@/utils/auth";
|
||||
import { getGeoContext } from "@/utils/geo";
|
||||
|
||||
type TRPCContext = {
|
||||
session: Session | null;
|
||||
db: Database;
|
||||
geo: ReturnType<typeof getGeoContext>;
|
||||
};
|
||||
|
||||
export const createTRPCContext = async (_: unknown, c: Context): Promise<TRPCContext> => {
|
||||
const accessToken = c.req.header("Authorization")?.split(" ")[1];
|
||||
const session = await verifyAccessToken(accessToken);
|
||||
const geo = getGeoContext(c.req);
|
||||
|
||||
return {
|
||||
db,
|
||||
geo,
|
||||
session,
|
||||
};
|
||||
};
|
||||
|
||||
const t = initTRPC.context<TRPCContext>().create({
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
export const createTRPCRouter = t.router;
|
||||
export const createCallerFactory = t.createCallerFactory;
|
||||
|
||||
const withDatabaseMiddleware = t.middleware(async (opts) => {
|
||||
return withDatabase({
|
||||
ctx: opts.ctx,
|
||||
next: opts.next,
|
||||
});
|
||||
});
|
||||
|
||||
const withAutenticationMiddleware = t.middleware(async (opts) => {
|
||||
return withAuthentication({
|
||||
ctx: opts.ctx,
|
||||
next: opts.next,
|
||||
});
|
||||
});
|
||||
|
||||
export const publicProcedure = t.procedure.use(withDatabaseMiddleware);
|
||||
|
||||
export const protectedProcedure = t.procedure
|
||||
.use(withDatabaseMiddleware)
|
||||
.use(withAutenticationMiddleware) // NOTE: This is needed to ensure that the teamId is set in the context
|
||||
.use(async (opts) => {
|
||||
const { session } = opts.ctx;
|
||||
|
||||
// if (!session) {
|
||||
// throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
// }
|
||||
|
||||
return opts.next({
|
||||
ctx: {
|
||||
session,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Database } from "@basango/db/client";
|
||||
|
||||
// import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { Session } from "@/utils/auth";
|
||||
|
||||
export const withAuthentication = async <TReturn>(opts: {
|
||||
ctx: {
|
||||
session?: Session | null;
|
||||
db: Database;
|
||||
};
|
||||
next: (opts: {
|
||||
ctx: {
|
||||
session?: Session | null;
|
||||
db: Database;
|
||||
};
|
||||
}) => Promise<TReturn>;
|
||||
}) => {
|
||||
const { ctx, next } = opts;
|
||||
|
||||
// const userId = ctx.session?.user?.id;
|
||||
|
||||
// if (!userId) {
|
||||
// throw new TRPCError({
|
||||
// code: "UNAUTHORIZED",
|
||||
// message: "No permission to access",
|
||||
// });
|
||||
// }
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
db: ctx.db,
|
||||
session: ctx.session,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { type Database, db } from "@basango/db/client";
|
||||
|
||||
import type { Session } from "@/utils/auth";
|
||||
|
||||
export const withDatabase = async <TReturn>(opts: {
|
||||
ctx: {
|
||||
session?: Session | null;
|
||||
db: Database;
|
||||
};
|
||||
next: (opts: {
|
||||
ctx: {
|
||||
session?: Session | null;
|
||||
db: Database;
|
||||
};
|
||||
}) => Promise<TReturn>;
|
||||
}) => {
|
||||
const { ctx, next } = opts;
|
||||
|
||||
ctx.db = db;
|
||||
const result = await next({ ctx });
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
|
||||
import { createTRPCRouter } from "@/trpc/init";
|
||||
import { sourcesRouter } from "@/trpc/routers/sources";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
sources: sourcesRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createSource, getSourceById, getSources, updateSource } from "@basango/db/queries";
|
||||
|
||||
import { createSourceSchema, getSourceSchema, updateSourceSchema } from "@/schemas/sources";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/trpc/init";
|
||||
|
||||
export const sourcesRouter = createTRPCRouter({
|
||||
create: protectedProcedure.input(createSourceSchema).mutation(async ({ ctx, input }) => {
|
||||
return createSource(ctx.db, { ...input });
|
||||
}),
|
||||
|
||||
get: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)),
|
||||
|
||||
getById: protectedProcedure.input(getSourceSchema).query(async ({ ctx, input }) => {
|
||||
return getSourceById(ctx.db, { ...input });
|
||||
}),
|
||||
|
||||
update: protectedProcedure.input(updateSourceSchema).mutation(async ({ ctx, input }) => {
|
||||
return updateSource(ctx.db, { ...input });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type JWTPayload, jwtVerify } from "jose";
|
||||
|
||||
import { env } from "@/config";
|
||||
|
||||
export type Session = {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type VerifiedJWTPayload = JWTPayload & {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function verifyAccessToken(accessToken?: string): Promise<Session | null> {
|
||||
if (!accessToken) return null;
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify<VerifiedJWTPayload>(
|
||||
accessToken,
|
||||
new TextEncoder().encode(env("BASANGO_JWT_SECRET")),
|
||||
);
|
||||
|
||||
return {
|
||||
user: {
|
||||
email: payload.user.email,
|
||||
full_name: payload.user.full_name,
|
||||
id: payload.user.id,
|
||||
},
|
||||
};
|
||||
} catch (_error: unknown) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user