mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
feat: create team routes and api auth functions
This commit is contained in:
@@ -38,6 +38,7 @@ yarn-error.log*
|
||||
.env
|
||||
.env*.local
|
||||
/prisma/enums.ts
|
||||
/prisma/generated/*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -52,8 +52,10 @@ yarn-error.log*
|
||||
|
||||
|
||||
/prisma/enums.ts
|
||||
/prisma/generated/*
|
||||
|
||||
/.pnpm-store
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
notes.md
|
||||
+4
-1
@@ -8,6 +8,7 @@
|
||||
"dist",
|
||||
"public/pdf.worker.min.js",
|
||||
"./prisma/enums.ts",
|
||||
"./prisma/generated/*",
|
||||
"./src/components/ui/simple-multi-select.tsx"
|
||||
]
|
||||
},
|
||||
@@ -27,6 +28,7 @@
|
||||
"dist",
|
||||
"public/pdf.worker.min.js",
|
||||
"./prisma/enums.ts",
|
||||
"./prisma/generated/*",
|
||||
"./src/components/ui/simple-multi-select.tsx"
|
||||
]
|
||||
},
|
||||
@@ -43,7 +45,8 @@
|
||||
".next",
|
||||
"dist",
|
||||
"public/pdf.worker.min.js",
|
||||
"./prisma/enums.ts"
|
||||
"./prisma/enums.ts",
|
||||
"./prisma/generated/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `secret` on the `ApiKey` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[hashedToken]` on the table `ApiKey` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `hashedToken` to the `ApiKey` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "ApiKey_secret_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ApiKey_userId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ApiKey" DROP COLUMN "secret",
|
||||
ADD COLUMN "hashedToken" TEXT NOT NULL,
|
||||
ADD COLUMN "lastUsed" TIMESTAMP(3);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiKey_hashedToken_key" ON "ApiKey"("hashedToken");
|
||||
@@ -60,7 +60,7 @@ model User {
|
||||
companies Member[]
|
||||
passkeys Passkey[]
|
||||
verificationToken VerificationToken[]
|
||||
apiKey ApiKey?
|
||||
apiKeys ApiKey[]
|
||||
|
||||
lastSignedIn DateTime @default(now())
|
||||
identityProvider String?
|
||||
@@ -1066,15 +1066,15 @@ model BillingCustomer {
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
id String @id @default(cuid())
|
||||
secret String @unique // scrypt hashed secret
|
||||
userId String
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
active Boolean @default(true)
|
||||
hashedToken String @unique // scrypt hashed token
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lastUsed DateTime?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { verifySecureHash } from "@/lib/crypto";
|
||||
import { ApiError } from "@/server/api/error";
|
||||
import { db } from "@/server/db";
|
||||
import type { Context } from "hono";
|
||||
|
||||
type Headers = Context["req"]["header"];
|
||||
|
||||
const verifyBearerToken = async (headers: Headers) => {
|
||||
const bearerToken = headers("Authorization");
|
||||
if (!bearerToken) {
|
||||
throw new ApiError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No bearer token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const token = bearerToken.split(" ")[1];
|
||||
if (!token) {
|
||||
throw new ApiError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No bearer token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const [id, hashedToken] = token.split("_");
|
||||
|
||||
if (!id || !hashedToken) {
|
||||
throw new ApiError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Invalid token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const apiKey = await db.apiKey.findUnique({
|
||||
where: { id, active: true },
|
||||
select: { id: true, hashedToken: true, user: true },
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new ApiError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Invalid token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const isValid = await verifySecureHash(apiKey.hashedToken, hashedToken);
|
||||
|
||||
if (!isValid) {
|
||||
throw new ApiError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Invalid token provided",
|
||||
});
|
||||
}
|
||||
|
||||
await db.apiKey.update({
|
||||
where: { id },
|
||||
data: { lastUsed: new Date() },
|
||||
});
|
||||
|
||||
return apiKey.user;
|
||||
};
|
||||
|
||||
export const withCompanyAuth = async (id: string, headers: Headers) => {
|
||||
const user = await verifyBearerToken(headers);
|
||||
|
||||
const member = await db.member.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
companyId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new ApiError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You do not have access to this company",
|
||||
});
|
||||
}
|
||||
|
||||
const company = await db.company.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!company) {
|
||||
throw new ApiError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Company not found",
|
||||
});
|
||||
}
|
||||
|
||||
return { user, company, member };
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ErrorResponses } from "@/server/api/error";
|
||||
import type { PublicAPI } from "@/server/api/hono";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { Context } from "hono";
|
||||
|
||||
export const RequestSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.cuid()
|
||||
.openapi({
|
||||
description: "Company ID",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
|
||||
example: "clxwbok580000i7nge8nm1ry0",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ResponseSchema = z
|
||||
.object({
|
||||
id: z.string().cuid().openapi({
|
||||
description: "Company ID",
|
||||
example: "clxwbok580000i7nge8nm1ry0",
|
||||
}),
|
||||
})
|
||||
.openapi("Company");
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "api/v1/companies/:id",
|
||||
request: { params: RequestSchema },
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ResponseSchema,
|
||||
},
|
||||
},
|
||||
description: "Get a company by ID",
|
||||
},
|
||||
|
||||
...ErrorResponses,
|
||||
},
|
||||
});
|
||||
const getOne = (app: PublicAPI) => {
|
||||
app.openapi(route, (c: Context) => {
|
||||
const params = c.req.param();
|
||||
|
||||
return c.json(
|
||||
{
|
||||
id: params.id as string,
|
||||
},
|
||||
200,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default getOne;
|
||||
export type ResponseSchemaType = z.infer<typeof ResponseSchema>;
|
||||
@@ -1,18 +1,42 @@
|
||||
import { ErrorResponses } from "@/server/api/error";
|
||||
import { withCompanyAuth } from "@/server/api/auth";
|
||||
import { ApiError, ErrorResponses } from "@/server/api/error";
|
||||
import type { PublicAPI } from "@/server/api/hono";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { Context, HonoRequest } from "hono";
|
||||
import type { Context } from "hono";
|
||||
|
||||
export const RequestSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.cuid()
|
||||
.openapi({
|
||||
description: "Company ID",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
|
||||
example: "clxwbok580000i7nge8nm1ry0",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ResponseSchema = z
|
||||
.object({
|
||||
id: z.string().cuid().openapi({
|
||||
description: "Company ID",
|
||||
example: "clxwbok580000i7nge8nm1ry0",
|
||||
}),
|
||||
})
|
||||
.openapi("Company");
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/v1/companies/:cid",
|
||||
path: "api/v1/companies/:id",
|
||||
request: { params: RequestSchema },
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
schema: ResponseSchema,
|
||||
},
|
||||
},
|
||||
description: "Get a company by ID",
|
||||
@@ -21,13 +45,29 @@ const route = createRoute({
|
||||
...ErrorResponses,
|
||||
},
|
||||
});
|
||||
|
||||
const getOne = (app: PublicAPI) => {
|
||||
app.openapi(route, async (c: Context) => {
|
||||
const req: HonoRequest = await c.req;
|
||||
console.log({ req });
|
||||
return c.json({ message: "TODO: implement this endpoint" });
|
||||
const params = c.req.param();
|
||||
const headers = c.req.header;
|
||||
const id = params.id as string;
|
||||
|
||||
const { company } = await withCompanyAuth(id, headers);
|
||||
|
||||
if (!company) {
|
||||
throw new ApiError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Company not found",
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
id: company.id,
|
||||
},
|
||||
200,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default getOne;
|
||||
export type ResponseSchemaType = z.infer<typeof ResponseSchema>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Context, HonoRequest } from "hono";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/v1/companies/:cid/teams",
|
||||
path: "/v1/companies/:id/teams",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Context, HonoRequest } from "hono";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/v1/companies/:cid/teams",
|
||||
path: "/v1/companies/:id/teams",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Context, HonoRequest } from "hono";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/v1/companies/:cid/teams/:tid",
|
||||
path: "/v1/companies/:id/teams/:teamId",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Context, HonoRequest } from "hono";
|
||||
|
||||
const route = createRoute({
|
||||
method: "put",
|
||||
path: "/v1/companies/:cid/teams/:tid",
|
||||
path: "/v1/companies/:id/teams/:teamId",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Context, HonoRequest } from "hono";
|
||||
|
||||
const route = createRoute({
|
||||
method: "put",
|
||||
path: "/v1/companies/:cid",
|
||||
path: "/v1/companies/:id",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"checkJs": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
/* Bundled projects */
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
|
||||
Reference in New Issue
Block a user