diff --git a/.changeset/perfect-wolves-fix.md b/.changeset/perfect-wolves-fix.md deleted file mode 100644 index 3837d45..0000000 --- a/.changeset/perfect-wolves-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"authhero": minor ---- - -Add prompts endpoint diff --git a/packages/authhero/CHANGELOG.md b/packages/authhero/CHANGELOG.md index d89bc28..a8bd2c0 100644 --- a/packages/authhero/CHANGELOG.md +++ b/packages/authhero/CHANGELOG.md @@ -1,5 +1,12 @@ # authhero +## 0.5.0 + +### Minor Changes + +- Store hook booleans as integers +- bb18986: Add prompts endpoint + ## 0.4.0 ### Minor Changes diff --git a/packages/authhero/package.json b/packages/authhero/package.json index 90650fa..ab44ee6 100644 --- a/packages/authhero/package.json +++ b/packages/authhero/package.json @@ -1,6 +1,6 @@ { "name": "authhero", - "version": "0.4.0", + "version": "0.5.0", "files": [ "dist" ], diff --git a/packages/authhero/src/management-app.ts b/packages/authhero/src/management-app.ts index 4e878fa..bb6cbbb 100644 --- a/packages/authhero/src/management-app.ts +++ b/packages/authhero/src/management-app.ts @@ -9,7 +9,7 @@ import { usersByEmailRoutes } from "./routes/management-api/users-by-email"; import { clientRoutes } from "./routes/management-api/clients"; import { tenantRoutes } from "./routes/management-api/tenants"; import { logRoutes } from "./routes/management-api/logs"; -// import { hooksRoutes } from "./routes/management-api/hooks"; +import { hooksRoutes } from "./routes/management-api/hooks"; import { connectionRoutes } from "./routes/management-api/connections"; import { promptsRoutes } from "./routes/management-api/prompts"; import { registerComponent } from "./middlewares/register-component"; @@ -39,7 +39,7 @@ export default function create(params: CreateAuthParams) { .route("/api/v2/clients", clientRoutes) .route("/api/v2/tenants", tenantRoutes) .route("/api/v2/logs", logRoutes) - // .route("/api/v2/hooks", hooksRoutes) + .route("/api/v2/hooks", hooksRoutes) .route("/api/v2/connections", connectionRoutes) .route("/api/v2/prompts", promptsRoutes); diff --git a/packages/authhero/src/routes/management-api/branding.ts b/packages/authhero/src/routes/management-api/branding.ts index de0d91f..9ca11f1 100644 --- a/packages/authhero/src/routes/management-api/branding.ts +++ b/packages/authhero/src/routes/management-api/branding.ts @@ -18,11 +18,11 @@ export const brandingRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -67,11 +67,11 @@ export const brandingRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }, }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { description: "Branding settings", diff --git a/packages/authhero/src/routes/management-api/connections.ts b/packages/authhero/src/routes/management-api/connections.ts index 3ffc69f..114497b 100644 --- a/packages/authhero/src/routes/management-api/connections.ts +++ b/packages/authhero/src/routes/management-api/connections.ts @@ -30,11 +30,11 @@ export const connectionRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -92,11 +92,11 @@ export const connectionRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -138,11 +138,11 @@ export const connectionRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { description: "Status", @@ -187,11 +187,11 @@ export const connectionRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { content: { @@ -247,11 +247,11 @@ export const connectionRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 201: { content: { diff --git a/packages/authhero/src/routes/management-api/hooks.ts b/packages/authhero/src/routes/management-api/hooks.ts new file mode 100644 index 0000000..275aaaf --- /dev/null +++ b/packages/authhero/src/routes/management-api/hooks.ts @@ -0,0 +1,262 @@ +import { Bindings } from "../../types"; +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +// import authenticationMiddleware from "../../middlewares/authentication"; +import { + hookInsertSchema, + hookSchema, + totalsSchema, +} from "@authhero/adapter-interfaces"; +import { querySchema } from "../../types/auth0/Query"; +import { parseSort } from "../../helpers/sort"; +import { HTTPException } from "hono/http-exception"; + +const hopoksWithTotalsSchema = totalsSchema.extend({ + hooks: z.array(hookSchema), +}); + +export const hooksRoutes = new OpenAPIHono<{ Bindings: Bindings }>() + // -------------------------------- + // GET /api/v2/hooks + // -------------------------------- + .openapi( + createRoute({ + tags: ["hooks"], + method: "get", + path: "/", + request: { + query: querySchema, + headers: z.object({ + "tenant-id": z.string(), + }), + }, + // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], + security: [ + { + Bearer: ["auth:read"], + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: z.union([z.array(hookSchema), hopoksWithTotalsSchema]), + }, + }, + description: "List of hooks", + }, + }, + }), + async (ctx) => { + const { "tenant-id": tenant_id } = ctx.req.valid("header"); + + const { page, per_page, include_totals, sort, q } = + ctx.req.valid("query"); + + const hooks = await ctx.env.data.hooks.list(tenant_id, { + page, + per_page, + include_totals, + sort: parseSort(sort), + q, + }); + + return ctx.json(hooks); + }, + ) + // -------------------------------- + // POST /api/v2/hooks + // -------------------------------- + .openapi( + createRoute({ + tags: ["hooks"], + method: "post", + path: "/", + request: { + headers: z.object({ + "tenant-id": z.string(), + }), + body: { + content: { + "application/json": { + schema: z.object(hookInsertSchema.shape), + }, + }, + }, + }, + // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], + security: [ + { + Bearer: ["auth:write"], + }, + ], + responses: { + 201: { + content: { + "application/json": { + schema: hookSchema, + }, + }, + description: "The created hook", + }, + }, + }), + async (ctx) => { + const { "tenant-id": tenant_id } = ctx.req.valid("header"); + const hook = ctx.req.valid("json"); + + const hooks = await ctx.env.data.hooks.create(tenant_id, hook); + + return ctx.json(hooks, { status: 201 }); + }, + ) + // -------------------------------- + // PATCH /api/v2/hooks/:hook_id + // -------------------------------- + .openapi( + createRoute({ + tags: ["hooks"], + method: "patch", + path: "/{hook_id}", + request: { + headers: z.object({ + "tenant-id": z.string(), + }), + params: z.object({ + hook_id: z.string(), + }), + body: { + content: { + "application/json": { + schema: z + .object(hookInsertSchema.shape) + .omit({ hook_id: true }) + .partial(), + }, + }, + }, + }, + // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], + security: [ + { + Bearer: ["auth:write"], + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: hookSchema.shape, + }, + }, + description: "The updated hook", + }, + 404: { + description: "Hook not found", + }, + }, + }), + async (ctx) => { + const { "tenant-id": tenant_id } = ctx.req.valid("header"); + const { hook_id } = ctx.req.valid("param"); + const hook = ctx.req.valid("json"); + + await ctx.env.data.hooks.update(tenant_id, hook_id, hook); + const result = await ctx.env.data.hooks.get(tenant_id, hook_id); + + if (!result) { + throw new HTTPException(404, { message: "Hook not found" }); + } + + return ctx.json(result); + }, + ) + // -------------------------------- + // GET /api/v2/hooks/:hook_id + // -------------------------------- + .openapi( + createRoute({ + tags: ["hooks"], + method: "get", + path: "/{hook_id}", + request: { + headers: z.object({ + "tenant-id": z.string(), + }), + params: z.object({ + hook_id: z.string(), + }), + }, + // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], + security: [ + { + Bearer: ["auth:read"], + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: hookSchema, + }, + }, + description: "A hook", + }, + 404: { + description: "Hook not found", + }, + }, + }), + async (ctx) => { + const { "tenant-id": tenant_id } = ctx.req.valid("header"); + const { hook_id } = ctx.req.valid("param"); + + const hook = await ctx.env.data.hooks.get(tenant_id, hook_id); + + if (!hook) { + throw new HTTPException(404, { message: "Hook not found" }); + } + + return ctx.json(hook); + }, + ) + // -------------------------------- + // DELETE /api/v2/hooks/:hook_id + // -------------------------------- + .openapi( + createRoute({ + tags: ["hooks"], + method: "delete", + path: "/{hook_id}", + request: { + headers: z.object({ + "tenant-id": z.string(), + }), + params: z.object({ + hook_id: z.string(), + }), + }, + // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], + security: [ + { + Bearer: ["auth:read"], + }, + ], + responses: { + 200: { + description: "A hook", + }, + }, + }), + async (ctx) => { + const { "tenant-id": tenant_id } = ctx.req.valid("header"); + const { hook_id } = ctx.req.valid("param"); + + const result = await ctx.env.data.hooks.remove(tenant_id, hook_id); + + if (!result) { + throw new HTTPException(404, { message: "Hook not found" }); + } + + return ctx.text("OK"); + }, + ); diff --git a/packages/authhero/src/routes/management-api/logs.ts b/packages/authhero/src/routes/management-api/logs.ts index 158f2dc..1fd50db 100644 --- a/packages/authhero/src/routes/management-api/logs.ts +++ b/packages/authhero/src/routes/management-api/logs.ts @@ -26,11 +26,11 @@ export const logRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -79,11 +79,11 @@ export const logRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { diff --git a/packages/authhero/src/routes/management-api/prompts.ts b/packages/authhero/src/routes/management-api/prompts.ts index 96d0edd..b443c04 100644 --- a/packages/authhero/src/routes/management-api/prompts.ts +++ b/packages/authhero/src/routes/management-api/prompts.ts @@ -68,11 +68,11 @@ export const promptsRoutes = new OpenAPIHono<{ Bindings: Bindings }>() }, }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { description: "Prompts settings", diff --git a/packages/authhero/src/routes/management-api/users-by-email.ts b/packages/authhero/src/routes/management-api/users-by-email.ts index 5c02fcf..e0c0390 100644 --- a/packages/authhero/src/routes/management-api/users-by-email.ts +++ b/packages/authhero/src/routes/management-api/users-by-email.ts @@ -25,11 +25,11 @@ export const usersByEmailRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { diff --git a/packages/authhero/src/routes/management-api/users.ts b/packages/authhero/src/routes/management-api/users.ts index 8e54c70..93b4bd6 100644 --- a/packages/authhero/src/routes/management-api/users.ts +++ b/packages/authhero/src/routes/management-api/users.ts @@ -39,11 +39,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -147,11 +147,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { content: { @@ -199,11 +199,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { description: "Status", @@ -244,11 +244,11 @@ export const userRoutes = new OpenAPIHono<{ }, }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { content: { @@ -356,11 +356,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { description: "Status", @@ -467,11 +467,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { // TODO: check why this fails @@ -545,11 +545,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:write"] })], - // security: [ - // { - // Bearer: ["auth:write"], - // }, - // ], + security: [ + { + Bearer: ["auth:write"], + }, + ], responses: { 200: { content: { @@ -596,11 +596,11 @@ export const userRoutes = new OpenAPIHono<{ }), }, // middleware: [authenticationMiddleware({ scopes: ["auth:read"] })], - // security: [ - // { - // Bearer: ["auth:read"], - // }, - // ], + security: [ + { + Bearer: ["auth:read"], + }, + ], responses: { 200: { // TODO: will be exposed in next version for the adapter interfaces diff --git a/packages/authhero/test/routes/management-api/hooks.spec.ts b/packages/authhero/test/routes/management-api/hooks.spec.ts new file mode 100644 index 0000000..8f7d630 --- /dev/null +++ b/packages/authhero/test/routes/management-api/hooks.spec.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from "vitest"; +import { testClient } from "hono/testing"; +import { getAdminToken } from "../../helpers/token"; +import { getTestServer } from "../../helpers/test-server"; +import { Hook } from "@authhero/adapter-interfaces"; + +describe("hooks", () => { + it("should support crud", async () => { + const { managementApp, env } = await getTestServer(); + const managementClient = testClient(managementApp, env); + + const token = await getAdminToken(); + + // -------------------------------------------- + // POST + // -------------------------------------------- + const createHooksResponse = await managementClient.api.v2.hooks.$post( + { + json: { + url: "https://example.com/hook", + trigger_id: "pre-user-signup", + }, + header: { + "tenant-id": "tenantId", + }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + + if (createHooksResponse.status !== 201) { + const message = await createHooksResponse.text(); + console.log(message); + } + + expect(createHooksResponse.status).toBe(201); + const createdHook = await createHooksResponse.json(); + + const { created_at, updated_at, hook_id, ...rest } = createdHook; + + expect(rest).toEqual({ + url: "https://example.com/hook", + trigger_id: "pre-user-signup", + }); + expect(created_at).toBeTypeOf("string"); + expect(updated_at).toBeTypeOf("string"); + expect(hook_id).toBeTypeOf("string"); + + // -------------------------------------------- + // PATCH + // -------------------------------------------- + const updateHookResponse = await managementClient.api.v2.hooks[ + ":id" + ].$patch( + { + param: { + id: hook_id!, + }, + json: { + url: "https://example.com/hook2", + }, + header: { + "tenant-id": "tenantId", + }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + + expect(updateHookResponse.status).toBe(200); + const updatedHook = (await updateHookResponse.json()) as Hook; + expect(updatedHook.url).toEqual("https://example.com/hook2"); + + // -------------------------------------------- + // DELETE + // -------------------------------------------- + const deleteHookResponse = await managementClient.api.v2.hooks[ + ":id" + ].$delete( + { + param: { + id: hook_id!, + }, + header: { + "tenant-id": "tenantId", + }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + + expect(deleteHookResponse.status).toBe(200); + + // -------------------------------------------- + // LIST + // -------------------------------------------- + const listHooksResponse = await managementClient.api.v2.hooks.$get( + { + query: {}, + header: { + "tenant-id": "tenantId", + }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + + expect(listHooksResponse.status).toBe(200); + const hooks = await listHooksResponse.json(); + expect(hooks).toEqual([]); + }); +}); diff --git a/packages/authhero/tsconfig.json b/packages/authhero/tsconfig.json index 3d52423..83f4fb9 100644 --- a/packages/authhero/tsconfig.json +++ b/packages/authhero/tsconfig.json @@ -6,6 +6,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": ["src"], + "include": ["src", "test/routes/management-api/hooks.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/kysely/CHANGELOG.md b/packages/kysely/CHANGELOG.md index 63f3a4a..9b45db4 100644 --- a/packages/kysely/CHANGELOG.md +++ b/packages/kysely/CHANGELOG.md @@ -1,5 +1,11 @@ # @authhero/kysely-adapter +## 0.16.0 + +### Minor Changes + +- Store hook booleans as integers + ## 0.15.0 ### Minor Changes diff --git a/packages/kysely/package.json b/packages/kysely/package.json index 727e6fe..e5f934c 100644 --- a/packages/kysely/package.json +++ b/packages/kysely/package.json @@ -11,7 +11,7 @@ "type": "git", "url": "https://github.com/markusahlstrand/authhero" }, - "version": "0.15.0", + "version": "0.16.0", "files": [ "dist" ], diff --git a/packages/kysely/src/db.ts b/packages/kysely/src/db.ts index 0c66d00..4ca3cde 100644 --- a/packages/kysely/src/db.ts +++ b/packages/kysely/src/db.ts @@ -5,7 +5,7 @@ import { Code, connectionSchema, Domain, - Hook, + hookSchema, loginSchema, Password, promptSettingSchema, @@ -61,13 +61,20 @@ const sqlUserSchema = z.object({ tenant_id: z.string(), }); +const sqlHookSchema = z.object({ + ...hookSchema.shape, + tenant_id: z.string(), + enabled: z.number(), + synchronous: z.number(), +}); + export interface Database { applications: z.infer; branding: SqlBranding; codes: Code & { tenant_id: string }; connections: z.infer; domains: Domain & { tenant_id: string }; - hooks: Hook & { tenant_id: string }; + hooks: z.infer; keys: SigningKey & { created_at: string }; logins: z.infer; logs: SqlLog; diff --git a/packages/kysely/src/hooks/create.ts b/packages/kysely/src/hooks/create.ts index 5b3ffe9..53ca4c2 100644 --- a/packages/kysely/src/hooks/create.ts +++ b/packages/kysely/src/hooks/create.ts @@ -5,7 +5,7 @@ import { Database } from "../db"; export function create(db: Kysely) { return async (tenant_id: string, hook: HookInsert): Promise => { - const createdHook = { + const createdHook: Hook = { hook_id: nanoid(), ...hook, created_at: new Date().toISOString(), @@ -17,6 +17,8 @@ export function create(db: Kysely) { .values({ ...createdHook, tenant_id, + enabled: hook.enabled ? 1 : 0, + synchronous: hook.synchronous ? 1 : 0, }) .execute(); diff --git a/packages/kysely/src/hooks/get.ts b/packages/kysely/src/hooks/get.ts index ef3c0dc..bdb14b7 100644 --- a/packages/kysely/src/hooks/get.ts +++ b/packages/kysely/src/hooks/get.ts @@ -16,9 +16,10 @@ export function get(db: Kysely) { return null; } - hook.enabled = !!hook.enabled; - hook.synchronous = !!hook.synchronous; - - return removeNullProperties(hook); + return removeNullProperties({ + ...hook, + enabled: !!hook.enabled, + synchronous: !!hook.synchronous, + }); }; } diff --git a/packages/kysely/src/hooks/update.ts b/packages/kysely/src/hooks/update.ts index faa3696..d5e2547 100644 --- a/packages/kysely/src/hooks/update.ts +++ b/packages/kysely/src/hooks/update.ts @@ -11,6 +11,9 @@ export function update(db: Kysely) { const sqlHook = { ...hook, updated_at: new Date().toISOString(), + enabled: hook.enabled !== undefined ? (hook.enabled ? 1 : 0) : undefined, + synchronous: + hook.enabled !== undefined ? (hook.synchronous ? 1 : 0) : undefined, }; await db