diff --git a/src/routers/app/ui/index.tsx b/src/routers/app/ui/index.tsx index 946b197..feb338d 100644 --- a/src/routers/app/ui/index.tsx +++ b/src/routers/app/ui/index.tsx @@ -1,13 +1,29 @@ -import type { ServerContext } from "@/types/hono.mjs"; import { Hono } from "hono"; import { db } from "@/config/db/index.mjs"; -import { HomePage } from "./pages/home.js"; +import { DashboardLandingPage } from "./pages/dashboard-landing.js"; import { LoginPage } from "./pages/login.js"; +import { checkTenantMembership, checkUserAuthed, checkServiceTenantMembership } from "./utils/middleware.mjs"; +import { WorkspaceLandingPage } from "./pages/workspace-landing.js"; + +import type { ServerContext } from "@/types/hono.mjs"; const app = new Hono(); +app.get("/", checkUserAuthed, async (c) => { + const user = c.var.user!; + + const relationships = await db.query.usersToTenants.findMany({ + where: (fields, { eq }) => eq(fields.userId, user.id), + with: { tenant: true }, + }); + + const tenants = relationships.map((r) => r.tenant); + + return c.html(); +}); + app.get("/login", async (c) => { const user = c.var.user; @@ -18,16 +34,37 @@ app.get("/login", async (c) => { return c.html(); }); -app.get("/", async (c) => { - const user = c.var.user; +app.get("/:workspace", checkUserAuthed, checkTenantMembership, async (c) => { + const tenant = c.var.tenant!; - if (!user) { - return c.redirect("/app/login"); - } + const services = await db.query.services.findMany({ + where: (fields, { eq }) => eq(fields.tenantId, tenant.id), + }); + + return c.html(); +}); - const services = await db.query.services.findMany({ orderBy: (fields, { desc }) => desc(fields.createdAt) }); +app.get("/:workspace/edit", checkUserAuthed, checkTenantMembership, async (c) => { + const tenantId = c.req.param("workspace"); + return c.text(`Workspace "${tenantId}" edit page`); +}); + +app.get("/:workspace/:service_id", checkUserAuthed, checkTenantMembership, checkServiceTenantMembership, async (c) => { + const serviceId = c.req.param("service_id"); - return c.html(); + return c.text(`Service id: "${serviceId}" landing page`); }); +app.get( + "/:workspace/:service_id/edit", + checkUserAuthed, + checkTenantMembership, + checkServiceTenantMembership, + async (c) => { + const serviceId = c.req.param("service_id"); + + return c.text(`Service id: "${serviceId}" edit page`); + }, +); + export default app; diff --git a/src/routers/app/ui/layouts/root-document.tsx b/src/routers/app/ui/layouts/root-document.tsx index 9a9bb9a..f618731 100644 --- a/src/routers/app/ui/layouts/root-document.tsx +++ b/src/routers/app/ui/layouts/root-document.tsx @@ -7,12 +7,6 @@ export const RootDocument: FC> = ({ title, {title} - - {children} diff --git a/src/routers/app/ui/pages/dashboard-landing.tsx b/src/routers/app/ui/pages/dashboard-landing.tsx new file mode 100644 index 0000000..1292ff3 --- /dev/null +++ b/src/routers/app/ui/pages/dashboard-landing.tsx @@ -0,0 +1,76 @@ +import type { FC } from "hono/jsx"; +import type { User } from "lucia"; + +import { RootDocument } from "../layouts/root-document.js"; +import { Card } from "../components/card.js"; +import { getButtonStyles } from "../components/button.js"; +import { dateFormatter } from "../utils/date.mjs"; + +import type { TenantRecord } from "@/types/db.mjs"; + +export const DashboardLandingPage: FC<{ + user: User; + tenants: Array; +}> = ({ user, tenants }) => { + const githubId = user.githubId; + const githubMessage = + user && githubId ? `, ${user.username} with a github id of "${githubId}"` : " user with no github-id"; + + return ( + +
+ +
+

Welcome!!!

+

{`Hello${githubMessage}`}

+ + Logout 👋🏼 + +
+
+ + + + + + + + + + {tenants.map((tenant) => ( + + + + + + ))} + +
+ Name + + Created at + + Edit +
+ + {tenant.name} + + + {dateFormatter.humanReadable(tenant.createdAt)} + + + Edit, {tenant.name} + +
+
+
+
+
+ ); +}; diff --git a/src/routers/app/ui/pages/home.tsx b/src/routers/app/ui/pages/home.tsx deleted file mode 100644 index b00d640..0000000 --- a/src/routers/app/ui/pages/home.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { FC } from "hono/jsx"; -import { services } from "@/config/db/schema.mjs"; - -import { RootDocument } from "../layouts/root-document.js"; -import { Card } from "../components/card.js"; -import { getButtonStyles } from "../components/button.js"; - -import type { ServerContext } from "@/types/hono.mjs"; - -type ServiceItem = typeof services.$inferSelect; - -function intlDateFormatter(date: string) { - return new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }).format(new Date(date)); -} - -export const HomePage: FC<{ user: NonNullable; services: Array }> = ({ - user, - services, -}) => { - const githubId = user.githubId; - const githubMessage = - user && githubId ? `, ${user.username} with a github id of "${githubId}"` : " user with no github-id"; - - return ( - -
- -
-

Welcome!!!

-

{`Hello${githubMessage}`}

- - Logout 👋🏼 - -
- {user ? ( -
- - - - - - - - - {services.map((service) => ( - - - - - ))} - -
- Name - - Created at -
- {service.name} - - {intlDateFormatter(service.createdAt)} -
-
- ) : null} -
-
-
- ); -}; diff --git a/src/routers/app/ui/pages/workspace-landing.tsx b/src/routers/app/ui/pages/workspace-landing.tsx new file mode 100644 index 0000000..93b3f51 --- /dev/null +++ b/src/routers/app/ui/pages/workspace-landing.tsx @@ -0,0 +1,79 @@ +import type { FC } from "hono/jsx"; + +import { RootDocument } from "../layouts/root-document.js"; +import { Card } from "../components/card.js"; +import { getButtonStyles } from "../components/button.js"; +import { dateFormatter } from "../utils/date.mjs"; + +import type { ServiceRecord, TenantRecord } from "@/types/db.mjs"; + +export const WorkspaceLandingPage: FC<{ + tenant: TenantRecord; + services: Array; +}> = ({ tenant, services }) => { + return ( + +
+ +
+

{tenant.name}

+

These are the services for {tenant.name}

+ +
+
+ + + + + + + + + + {services.map((service) => ( + + + + + + ))} + +
+ Service name + + Created at + + Edit +
+ + {service.name} + + + {dateFormatter.humanReadable(new Date(service.createdAt))} + + + Edit, {service.name} + +
+
+
+
+
+ ); +}; diff --git a/src/routers/app/ui/utils/date.mts b/src/routers/app/ui/utils/date.mts new file mode 100644 index 0000000..aa281bf --- /dev/null +++ b/src/routers/app/ui/utils/date.mts @@ -0,0 +1,14 @@ +function intlDateFormatter(date: Date) { + return new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }).format(date); +} + +export const dateFormatter = { + humanReadable: intlDateFormatter, +}; diff --git a/src/routers/app/ui/utils/middleware.mts b/src/routers/app/ui/utils/middleware.mts new file mode 100644 index 0000000..2f8fa08 --- /dev/null +++ b/src/routers/app/ui/utils/middleware.mts @@ -0,0 +1,58 @@ +import { createMiddleware } from "hono/factory"; +import { HTTPException } from "hono/http-exception"; + +import { db } from "@/config/db/index.mjs"; + +import type { ServerContext } from "@/types/hono.mjs"; + +export const checkUserAuthed = createMiddleware(async (c, next) => { + const user = c.var.user; + + if (!user) { + return c.redirect("/app/login"); + } + + return await next(); +}); + +export const checkTenantMembership = createMiddleware(async (c, next) => { + const userId = c.var.user!.id; + const workspace = c.req.param("workspace"); + + const tenant = await db.query.tenants.findFirst({ + where: (fields, { eq }) => eq(fields.workspace, workspace), + }); + + if (!tenant) { + throw new HTTPException(404, { message: "Workspace not found." }); + } + + const relationship = await db.query.usersToTenants.findFirst({ + where: (fields, { and, eq }) => and(eq(fields.userId, userId), eq(fields.tenantId, tenant.id)), + }); + + if (!relationship) { + throw new HTTPException(403, { message: "You do not have access to this workspace." }); + } + + c.set("tenant", tenant); + + return await next(); +}); + +export const checkServiceTenantMembership = createMiddleware(async (c, next) => { + const tenant = c.var.tenant!; + const serviceId = c.req.param("service_id"); + + const service = await db.query.services.findFirst({ + where: (fields, { and, eq }) => and(eq(fields.id, serviceId), eq(fields.tenantId, tenant.id)), + }); + + if (!service) { + throw new HTTPException(404, { message: "Service not found." }); + } + + c.set("service", service); + + return await next(); +}); diff --git a/src/types/db.mts b/src/types/db.mts new file mode 100644 index 0000000..9a6a48a --- /dev/null +++ b/src/types/db.mts @@ -0,0 +1,5 @@ +import type { services, tenants, users } from "@/config/db/schema.mjs"; + +export type ServiceRecord = typeof services.$inferSelect; +export type UserRecord = typeof users.$inferSelect; +export type TenantRecord = typeof tenants.$inferSelect; diff --git a/src/types/hono.mts b/src/types/hono.mts index bc68bb3..d33032c 100644 --- a/src/types/hono.mts +++ b/src/types/hono.mts @@ -1,13 +1,13 @@ import type { HttpBindings } from "@hono/node-server"; import type { Session, User } from "lucia"; -import type { services as servicesTable } from "@/config/db/schema.mjs"; -type Service = typeof servicesTable.$inferSelect; +import type { ServiceRecord, TenantRecord } from "./db.mjs"; type Bindings = HttpBindings & {}; type Variables = { - service: Service | null; + service: ServiceRecord | null; + tenant: TenantRecord | null; user: User | null; session: Session | null; };