mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
feat[convex]: api key auth support
This commit is contained in:
4
packages/convex/_generated/api.d.ts
vendored
4
packages/convex/_generated/api.d.ts
vendored
@@ -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
14
packages/convex/apikey.ts
Normal 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)
|
||||
},
|
||||
})
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
30
packages/convex/model/apikey.ts
Normal file
30
packages/convex/model/apikey.ts
Normal 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(),
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
"./shared/*": "./shared/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fileone/path": "workspace:*"
|
||||
"@drexa/auth": "workspace:*",
|
||||
"@fileone/path": "workspace:*",
|
||||
"hash-wasm": "^4.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user