diff --git a/packages/convex/_generated/api.d.ts b/packages/convex/_generated/api.d.ts index 2c7501c..b468048 100644 --- a/packages/convex/_generated/api.d.ts +++ b/packages/convex/_generated/api.d.ts @@ -8,6 +8,7 @@ * @module */ +import type * as apikey from "../apikey.js"; import type * as auth from "../auth.js"; import type * as betterauth__generated_api from "../betterauth/_generated/api.js"; import type * as betterauth__generated_server from "../betterauth/_generated/server.js"; @@ -19,6 +20,7 @@ import type * as files from "../files.js"; import type * as filesystem from "../filesystem.js"; import type * as functions from "../functions.js"; import type * as http from "../http.js"; +import type * as model_apikey from "../model/apikey.js"; import type * as model_directories from "../model/directories.js"; import type * as model_files from "../model/files.js"; import type * as model_filesystem from "../model/filesystem.js"; @@ -43,6 +45,7 @@ import type { * ``` */ declare const fullApi: ApiFromModules<{ + apikey: typeof apikey; auth: typeof auth; "betterauth/_generated/api": typeof betterauth__generated_api; "betterauth/_generated/server": typeof betterauth__generated_server; @@ -54,6 +57,7 @@ declare const fullApi: ApiFromModules<{ filesystem: typeof filesystem; functions: typeof functions; http: typeof http; + "model/apikey": typeof model_apikey; "model/directories": typeof model_directories; "model/files": typeof model_files; "model/filesystem": typeof model_filesystem; diff --git a/packages/convex/apikey.ts b/packages/convex/apikey.ts new file mode 100644 index 0000000..6479cc2 --- /dev/null +++ b/packages/convex/apikey.ts @@ -0,0 +1,14 @@ +import { verifyApiKey as _verifyApiKey, parseApiKey } from "@drexa/auth" +import { WebCryptoSha256Hasher } from "@drexa/auth/hasher" +import { v } from "convex/values" +import { query } from "./_generated/server" +import * as ApiKey from "./model/apikey" + +export const verifyApiKey = query({ + args: { + unhashedKey: v.string(), + }, + handler: async (ctx, args) => { + return await ApiKey.verifyApiKey(ctx, args.unhashedKey) + }, +}) diff --git a/packages/convex/auth.ts b/packages/convex/auth.ts index 1188e96..4d2491d 100644 --- a/packages/convex/auth.ts +++ b/packages/convex/auth.ts @@ -1,8 +1,8 @@ import { createClient, type GenericCtx } from "@convex-dev/better-auth" import { convex, crossDomain } from "@convex-dev/better-auth/plugins" -import { betterAuth } from "better-auth" import { components } from "@fileone/convex/api" import type { DataModel } from "@fileone/convex/dataModel" +import { betterAuth } from "better-auth" import authSchema from "./betterauth/schema" const siteUrl = process.env.SITE_URL! diff --git a/packages/convex/functions.ts b/packages/convex/functions.ts index 25af388..0f4e0aa 100644 --- a/packages/convex/functions.ts +++ b/packages/convex/functions.ts @@ -1,18 +1,20 @@ +import type { DataModel } from "@fileone/convex/dataModel" +import type { MutationCtx, QueryCtx } from "@fileone/convex/server" +import { mutation, query } from "@fileone/convex/server" import type { DocumentByName, TableNamesInDataModel, UserIdentity, } from "convex/server" -import type { GenericId } from "convex/values" +import { type GenericId, v } from "convex/values" import { customCtx, customMutation, customQuery, } from "convex-helpers/server/customFunctions" -import type { DataModel } from "@fileone/convex/dataModel" -import type { MutationCtx, QueryCtx } from "@fileone/convex/server" -import { mutation, query } from "@fileone/convex/server" +import * as ApiKey from "./model/apikey" import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user" +import * as Err from "./shared/error" export type AuthenticatedQueryCtx = QueryCtx & { user: AuthUser @@ -50,6 +52,21 @@ export const authenticatedMutation = customMutation( }), ) +/** + * Custom mutation that requires api key authentication for a mutation. + */ +export const apiKeyAuthenticatedMutation = customMutation(mutation, { + args: { + apiKey: v.string(), + }, + input: async (ctx, args) => { + if (!(await ApiKey.verifyApiKey(ctx, args.apiKey))) { + throw Err.create(Err.Code.Unauthenticated, "Invalid API key") + } + return { ctx, args } + }, +}) + /** * Gets a document by its id and checks if the user is authorized to access it * diff --git a/packages/convex/model/apikey.ts b/packages/convex/model/apikey.ts new file mode 100644 index 0000000..f932666 --- /dev/null +++ b/packages/convex/model/apikey.ts @@ -0,0 +1,30 @@ +import { + verifyApiKey as _verifyApiKey, + parseApiKey, + WebCryptoSha256Hasher, +} from "@drexa/auth" +import type { MutationCtx, QueryCtx } from "../_generated/server" + +export async function verifyApiKey( + ctx: MutationCtx | QueryCtx, + unhashedKey: string, +): Promise { + const parsedKey = parseApiKey(unhashedKey) + if (!parsedKey) { + return false + } + + const apiKey = await ctx.db + .query("apiKeys") + .withIndex("byPublicId", (q) => q.eq("publicId", parsedKey.prefix)) + .first() + if (!apiKey) { + return false + } + + return await _verifyApiKey({ + keyToBeVerified: parsedKey.unhashedKey, + hashedKey: apiKey.hashedKey, + hasher: new WebCryptoSha256Hasher(), + }) +} diff --git a/packages/convex/package.json b/packages/convex/package.json index acc813d..207a423 100644 --- a/packages/convex/package.json +++ b/packages/convex/package.json @@ -14,7 +14,9 @@ "./shared/*": "./shared/*" }, "dependencies": { - "@fileone/path": "workspace:*" + "@drexa/auth": "workspace:*", + "@fileone/path": "workspace:*", + "hash-wasm": "^4.12.0" }, "peerDependencies": { "typescript": "^5", diff --git a/packages/convex/schema.ts b/packages/convex/schema.ts index fe7d53e..072d5cb 100644 --- a/packages/convex/schema.ts +++ b/packages/convex/schema.ts @@ -40,6 +40,13 @@ const schema = defineSchema({ "name", "deletedAt", ]), + apiKeys: defineTable({ + publicId: v.string(), + hashedKey: v.string(), + createdAt: v.number(), + updatedAt: v.number(), + expiresAt: v.optional(v.number()), + }).index("byPublicId", ["publicId"]), }) export default schema