feat(dashboard): setting up layout

This commit is contained in:
2025-11-12 16:51:59 +02:00
parent b8b2a15ee9
commit a3f46b6b38
61 changed files with 2957 additions and 123 deletions
+1
View File
@@ -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"),
},
+11
View File
@@ -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: {
+69
View File
@@ -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,
});
+67
View File
@@ -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,
},
});
});
+36
View File
@@ -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,
},
});
};
+23
View File
@@ -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;
};
+13
View File
@@ -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>;
+20
View File
@@ -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 });
}),
});
+40
View File
@@ -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;
}
}