feat[convex]: api key auth support

This commit is contained in:
2025-10-20 00:17:41 +00:00
parent a4544a3f09
commit d0893e13be
7 changed files with 80 additions and 6 deletions

View File

@@ -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;

14
packages/convex/apikey.ts Normal file
View File

@@ -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)
},
})

View File

@@ -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!

View File

@@ -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
*

View File

@@ -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<boolean> {
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(),
})
}

View File

@@ -14,7 +14,9 @@
"./shared/*": "./shared/*"
},
"dependencies": {
"@fileone/path": "workspace:*"
"@drexa/auth": "workspace:*",
"@fileone/path": "workspace:*",
"hash-wasm": "^4.12.0"
},
"peerDependencies": {
"typescript": "^5",

View File

@@ -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