Skip to content

Commit

Permalink
Overhaul backend + tenant settings & create
Browse files Browse the repository at this point in the history
  • Loading branch information
oscartbeaumont committed Sep 18, 2024
1 parent 5524eb7 commit 4ca3081
Show file tree
Hide file tree
Showing 26 changed files with 1,343 additions and 3,232 deletions.
6 changes: 0 additions & 6 deletions apps/web/src/api/tenant/index.ts

This file was deleted.

181 changes: 34 additions & 147 deletions apps/web/src/api/trpc/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { flushResponse } from "@mattrax/trpc-server-function/server";
import { cache } from "@solidjs/router";
import { TRPCError, initTRPC } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import type { User } from "lucia";
import type { H3Event } from "vinxi/server";
import { ZodError, z } from "zod";

import { db, organisationMembers, organisations, tenants } from "~/db";
import { db, tenantMembers, tenants } from "~/db";
import { withAccount } from "../account";
import { checkAuth } from "../auth";
import { withTenant } from "../tenant";

export const createTRPCContext = (event: H3Event) => {
return {
Expand Down Expand Up @@ -47,41 +45,21 @@ export const publicProcedure = t.procedure.use(async ({ next }) => {
}
});

const isOrganisationMember = cache(async (orgPk: number, accountPk: number) => {
const isTenantMember = cache(async (tenantPk: number, accountPk: number) => {
const [org] = await db
.select({})
.from(organisations)
.where(eq(organisations.pk, orgPk))
.from(tenants)
.where(eq(tenants.pk, tenantPk))
.innerJoin(
organisationMembers,
tenantMembers,
and(
eq(organisations.pk, organisationMembers.orgPk),
eq(organisationMembers.accountPk, accountPk),
eq(tenants.pk, tenantMembers.tenantPk),
eq(tenantMembers.accountPk, accountPk),
),
);

return org !== undefined;
}, "isOrganisationMember");

export const getTenantList = cache(
(accountPk: number) =>
db
.select({
id: tenants.id,
pk: tenants.pk,
name: tenants.name,
slug: tenants.slug,
orgSlug: organisations.slug,
})
.from(tenants)
.innerJoin(organisations, eq(tenants.orgPk, organisations.pk))
.innerJoin(
organisationMembers,
eq(organisations.pk, organisationMembers.orgPk),
)
.where(eq(organisationMembers.accountPk, accountPk)),
"getTenantList",
);
}, "isTenantMember");

// Authenticated procedure
export const authedProcedure = publicProcedure.use(async ({ next }) => {
Expand All @@ -93,132 +71,41 @@ export const authedProcedure = publicProcedure.use(async ({ next }) => {
next({
ctx: {
...data,
ensureOrganisationMember: async (orgPk: number) => {
if (!isOrganisationMember(orgPk, data.account.pk))
throw new TRPCError({ code: "FORBIDDEN", message: "organisation" });
},
ensureTenantMember: async (tenantPk: number) => {
const tenantList = await getTenantList(data.account.pk);

const tenant = tenantList.find((t) => t.pk === tenantPk);
if (!tenant)
if (!isTenantMember(tenantPk, data.account.pk))
throw new TRPCError({ code: "FORBIDDEN", message: "tenant" });

return tenant;
},
},
}),
);
});

export const isSuperAdmin = (account: { email: string } | User) =>
account.email.endsWith("@otbeaumont.me") ||
account.email.endsWith("@mattrax.app");

// Authenticated procedure requiring a superadmin (Mattrax employee)
export const superAdminProcedure = authedProcedure.use((opts) => {
const { ctx } = opts;
if (!isSuperAdmin(ctx.account)) throw new TRPCError({ code: "FORBIDDEN" });

return opts.next({ ctx });
});

// const getMemberOrg = cache(async (slug: string, accountPk: number) => {
// const [org] = await db
// .select({
// pk: organisations.pk,
// slug: organisations.slug,
// name: organisations.name,
// ownerPk: organisations.ownerPk,
// })
// .from(organisations)
// .where(
// and(
// eq(organisations.slug, slug),
// eq(organisationMembers.accountPk, accountPk),
// ),
// )
// .innerJoin(
// organisationMembers,
// eq(organisations.pk, organisationMembers.orgPk),
// );

// return org;
// }, "getMemberOrg");

// export const orgProcedure = authedProcedure
// .input(z.object({ orgSlug: z.string() }))
// .use(async (opts) => {
// const { ctx, input, type } = opts;

// const org = await getMemberOrg(input.orgSlug, ctx.account.pk);

// if (!org)
// throw new TRPCError({ code: "FORBIDDEN", message: "organisation" });

// return opts.next({ ctx: { ...ctx, org } }).then((result) => {
// // TODO: Right now we invalidate everything but we will need to be more specific in the future
// // if (type === "mutation") invalidate(org.slug);
// return result;
// });
// });

// const getMemberTenant = cache(async (slug: string, accountPk: number) => {
// const [tenant] = await db
// .select({
// pk: tenants.pk,
// id: tenants.id,
// name: tenants.name,
// orgSlug: organisations.slug,
// })
// .from(tenants)
// .innerJoin(organisations, eq(tenants.orgPk, organisations.pk))
// .innerJoin(
// organisationMembers,
// eq(organisations.pk, organisationMembers.orgPk),
// )
// .where(
// and(eq(tenants.slug, slug), eq(organisationMembers.accountPk, accountPk)),
// );

// return tenant;
// }, "getMemberTenant");

// // Authenticated procedure w/ a tenant
// export const tenantProcedure = authedProcedure
// .input(z.object({ tenantSlug: z.string() }))
// .use(async (opts) => {
// const { ctx, input, type } = opts;
const getTenant = cache(async (tenantId: string, accountPk: number) => {
const [tenant] = await db
.select({
id: tenants.id,
pk: tenants.pk,
})
.from(tenants)
.where(eq(tenants.id, tenantId))
.innerJoin(
tenantMembers,
and(
eq(tenants.pk, tenantMembers.tenantPk),
eq(tenantMembers.accountPk, accountPk),
),
);

// const tenant = await getMemberTenant(input.tenantSlug, ctx.account.pk);
return tenant;
}, "isTenantMemberById");

// if (!tenant) throw new TRPCError({ code: "FORBIDDEN", message: "tenant" });
export const tenantProcedure = authedProcedure
.input(z.object({ tenantId: z.string() }))
.use(async (opts) => {
const { ctx, input, type } = opts;

// return withTenant(tenant, () =>
// opts.next({ ctx: { ...ctx, tenant } }).then((result) => {
// // TODO: Right now we invalidate everything but we will need to be more specific in the future
// // if (type === "mutation") invalidate(tenant.orgSlug, input.tenantSlug);
// return result;
// }),
// );
// });
const tenant = await getTenant(input.tenantId, ctx.account.pk);
if (!tenant) throw new TRPCError({ code: "FORBIDDEN", message: "tenant" });

export const restricted = new Set([
// Misleading names
"admin",
"administrator",
"help",
"mod",
"moderator",
"staff",
"mattrax",
"root",
"contact",
"support",
"home",
"employee",
// Reserved Mattrax routes
"enroll",
"profile",
"account",
]);
return opts.next({ ctx: { ...ctx, tenant } });
});
9 changes: 4 additions & 5 deletions apps/web/src/api/trpc/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { createTRPCContext, createTRPCRouter } from "./helpers";
import { authRouter } from "./routers/auth";
// import { deviceRouter } from "./routers/device";
import { deviceRouter } from "./routers/device";
import { metaRouter } from "./routers/meta";
// import { orgRouter } from "./routers/org";
import { tenantRouter } from "./routers/tenant/index";
// import { blueprintRouter } from "./routers/blueprint";
import { blueprintRouter } from "./routers/blueprint";

export const appRouter = createTRPCRouter({
auth: authRouter,
tenant: tenantRouter,
// blueprint: blueprintRouter,
// device: deviceRouter,
blueprint: blueprintRouter,
device: deviceRouter,
meta: metaRouter,
});

Expand Down
27 changes: 10 additions & 17 deletions apps/web/src/api/trpc/routers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { generateId, type User } from "lucia";
import { alphabet, generateRandomString } from "oslo/crypto";
import { appendResponseHeader, deleteCookie, useSession } from "vinxi/server";
import { appendResponseHeader, deleteCookie } from "vinxi/server";
import { z } from "zod";

import {
Expand All @@ -15,18 +15,11 @@ import {
} from "~/api/auth";
import { sendEmail } from "~/api/emails";
import { accountLoginCodes, accounts, db, tenants } from "~/db";
import {
authedProcedure,
createTRPCRouter,
getTenantList,
publicProcedure,
} from "../helpers";
import { authedProcedure, createTRPCRouter, publicProcedure } from "../helpers";
import { sendDiscordMessage } from "./meta";
import { env } from "~/env";

async function mapAccount(account: DatabaseUserAttributes) {
// await new Promise((resolve) => setTimeout(resolve, 10000)); // TODO

return {
id: account.id,
name: account.name,
Expand All @@ -44,7 +37,7 @@ export const authRouter = createTRPCRouter({
)
.mutation(async ({ input }) => {
const parts = input.email.split("@");
// This should be impossible due to input validation on the frontend but we guard just in case
// This should be impossible due to input validation but we guard just in case
if (parts.length !== 2) throw new Error("Invalid email provided!");

const account = await db.query.accounts.findFirst({
Expand Down Expand Up @@ -106,7 +99,10 @@ export const authRouter = createTRPCRouter({
});

if (!code)
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid code" });
throw new TRPCError({
code: "NOT_FOUND",
message: "Invalid login code",
});

const [_, [account]] = await Promise.all([
ctx.db
Expand Down Expand Up @@ -152,7 +148,7 @@ export const authRouter = createTRPCRouter({
await handleLoginSuccess(account.id);

flushResponse();
revalidate([checkAuth.key, getTenantList.key]);
revalidate(checkAuth.key);

return mapAccount(account);
}),
Expand All @@ -173,7 +169,7 @@ export const authRouter = createTRPCRouter({
}
}),

// `authedProcedure` implies `flushResponse` and we need manual control for the cookies!
// `authedProcedure` calls `flushResponse` before the handler so we can't use it here!
logout: publicProcedure.mutation(async () => {
const data = await checkAuth();
if (!data) throw new TRPCError({ code: "UNAUTHORIZED" });
Expand All @@ -182,11 +178,9 @@ export const authRouter = createTRPCRouter({
deleteCookie("isLoggedIn");

await lucia.invalidateSession(data.session.id);

flushResponse();
}),

// `authedProcedure` implies `flushResponse` and we need manual control for the cookies!
// `authedProcedure` calls `flushResponse` before the handler so we can't use it here!
delete: publicProcedure.mutation(async ({ ctx }) => {
const data = await checkAuth();
if (!data) throw new TRPCError({ code: "UNAUTHORIZED" });
Expand All @@ -202,7 +196,6 @@ export const authRouter = createTRPCRouter({
deleteCookie(lucia.sessionCookieName);
deleteCookie("isLoggedIn");
await lucia.invalidateSession(data.session.id);
flushResponse();
}),
});

Expand Down
Loading

0 comments on commit 4ca3081

Please sign in to comment.