Skip to content

Commit

Permalink
feat: basic routing for viewing
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere committed Jun 30, 2024
1 parent cf9830a commit 9d220ed
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 95 deletions.
55 changes: 46 additions & 9 deletions src/routers/app/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ServerContext>();

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(<DashboardLandingPage user={user} tenants={tenants} />);
});

app.get("/login", async (c) => {
const user = c.var.user;

Expand All @@ -18,16 +34,37 @@ app.get("/login", async (c) => {
return c.html(<LoginPage />);
});

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(<WorkspaceLandingPage tenant={tenant} services={services} />);
});

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(<HomePage user={user} services={services} />);
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;
6 changes: 0 additions & 6 deletions src/routers/app/ui/layouts/root-document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ export const RootDocument: FC<PropsWithChildren<{ title: string }>> = ({ title,
<meta charset="UTF-8"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<title>{title}</title>
<script
src="https://unpkg.com/[email protected]"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"
></script>
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 dark:bg-gray-900">{children}</body>
Expand Down
76 changes: 76 additions & 0 deletions src/routers/app/ui/pages/dashboard-landing.tsx
Original file line number Diff line number Diff line change
@@ -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<TenantRecord>;
}> = ({ user, tenants }) => {
const githubId = user.githubId;
const githubMessage =
user && githubId ? `, ${user.username} with a github id of "${githubId}"` : " user with no github-id";

return (
<RootDocument title="Simple Logging Server">
<section class="mx-auto max-w-7xl h-full grid place-items-center px-2">
<Card class="lg:max-w-2xl w-full">
<div class="p-4">
<h2 class="text-2xl lg:text-3xl font-semibold pb-2 text-gray-900">Welcome!!!</h2>
<p class="lg:text-sm pb-2 text-gray-700">{`Hello${githubMessage}`}</p>
<a class={getButtonStyles("secondary", "xs")} href="/app/logout">
Logout 👋🏼
</a>
</div>
<div class="border-t overflow-hidden">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8"
>
Name
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Created at
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{tenants.map((tenant) => (
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">
<a
href={`/app/${tenant.workspace}`}
class="text-indigo-600 hover:text-indigo-900 hover:underline"
>
{tenant.name}
</a>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{dateFormatter.humanReadable(tenant.createdAt)}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
<a href={`/app/${tenant.workspace}/edit`} class="text-indigo-600 hover:text-indigo-900">
Edit<span class="sr-only">, {tenant.name}</span>
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
</RootDocument>
);
};
77 changes: 0 additions & 77 deletions src/routers/app/ui/pages/home.tsx

This file was deleted.

79 changes: 79 additions & 0 deletions src/routers/app/ui/pages/workspace-landing.tsx
Original file line number Diff line number Diff line change
@@ -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<ServiceRecord>;
}> = ({ tenant, services }) => {
return (
<RootDocument title="Simple Logging Server">
<section class="mx-auto max-w-7xl h-full grid place-items-center px-2">
<Card class="lg:max-w-2xl w-full">
<div class="p-4">
<h2 class="text-2xl lg:text-3xl font-semibold pb-2 text-gray-900">{tenant.name}</h2>
<p class="lg:text-sm pb-2 text-gray-700">These are the services for {tenant.name}</p>
<div class="flex gap-1">
<a class={getButtonStyles("secondary", "xs")} href="/app/logout">
Logout 👋🏼
</a>
<a class={getButtonStyles("secondary", "xs")} href={`/app/${tenant.workspace}/edit`}>
Edit ✏️
</a>
</div>
</div>
<div class="border-t overflow-hidden">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8"
>
Service name
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Created at
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{services.map((service) => (
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">
<a
href={`/app/${tenant.workspace}/${service.name}`}
class="text-indigo-600 hover:text-indigo-900 hover:underline"
>
{service.name}
</a>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{dateFormatter.humanReadable(new Date(service.createdAt))}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
<a
href={`/app/${tenant.workspace}/${service.id}/edit`}
class="text-indigo-600 hover:text-indigo-900"
>
Edit<span class="sr-only">, {service.name}</span>
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
</RootDocument>
);
};
14 changes: 14 additions & 0 deletions src/routers/app/ui/utils/date.mts
Original file line number Diff line number Diff line change
@@ -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,
};
58 changes: 58 additions & 0 deletions src/routers/app/ui/utils/middleware.mts
Original file line number Diff line number Diff line change
@@ -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<ServerContext>(async (c, next) => {
const user = c.var.user;

if (!user) {
return c.redirect("/app/login");
}

return await next();
});

export const checkTenantMembership = createMiddleware<ServerContext>(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<ServerContext>(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();
});
Loading

0 comments on commit 9d220ed

Please sign in to comment.