diff --git a/bun.lockb b/bun.lockb index 1872bc5..c7a287a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0000_uneven_professor_monster.sql b/drizzle/0000_ambitious_jocasta.sql similarity index 92% rename from drizzle/0000_uneven_professor_monster.sql rename to drizzle/0000_ambitious_jocasta.sql index 776ae33..e532564 100644 --- a/drizzle/0000_uneven_professor_monster.sql +++ b/drizzle/0000_ambitious_jocasta.sql @@ -16,6 +16,13 @@ CREATE TABLE "auth"."account" ( "updated_at" timestamp NOT NULL ); --> statement-breakpoint +CREATE TABLE "auth"."jwks" ( + "id" text PRIMARY KEY NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "created_at" timestamp NOT NULL +); +--> statement-breakpoint CREATE TABLE "auth"."rate_limit" ( "id" text PRIMARY KEY NOT NULL, "key" text, diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index e268da5..5e1d8bc 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "50bd2c27-8d45-478f-a894-b2f7cd4a718b", + "id": "38b37b1d-93e4-4f9a-8d7e-a77b0632afb4", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -110,6 +110,43 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "auth.jwks": { + "name": "jwks", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "auth.rate_limit": { "name": "rate_limit", "schema": "auth", diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 729beee..9fdfc2b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1737019435130, - "tag": "0000_uneven_professor_monster", + "when": 1737386379330, + "tag": "0000_ambitious_jocasta", "breakpoints": true } ] diff --git a/package.json b/package.json index d9984d9..be978da 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "app", "version": "1.0.50", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "bun test api", "dev": "bun run --watch src/index.ts", "email": "email dev --dir src/emails", "auth:generate": "bun x @better-auth/cli generate --config src/lib/auth/auth.ts --output src/db/schema/auth.ts && drizzle-kit migrate", @@ -16,14 +16,15 @@ }, "dependencies": { "@elysiajs/cors": "^1.2.0", + "@elysiajs/eden": "^1.2.0", "@elysiajs/opentelemetry": "^1.2.0", "@elysiajs/server-timing": "^1.2.0", "@elysiajs/swagger": "^1.2.0", "@paralleldrive/cuid2": "^2.2.2", - "@react-email/components": "^0.0.31", - "better-auth": "^1.1.10", + "@react-email/components": "^0.0.32", + "better-auth": "^1.1.14", "dotenv": "^16.4.7", - "drizzle-orm": "^0.38.3", + "drizzle-orm": "^0.38.4", "drizzle-typebox": "^0.2.1", "elysia": "latest", "minio": "^8.0.3", @@ -38,11 +39,11 @@ "devDependencies": { "@types/nodemailer": "^6.4.17", "@types/pg": "^8.11.10", - "@types/react": "^19.0.3", - "@types/react-dom": "^19.0.2", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", "bun-types": "latest", - "drizzle-kit": "^0.30.1", - "react-email": "^3.0.4", + "drizzle-kit": "^0.30.2", + "react-email": "^3.0.6", "tsx": "^4.19.2" }, "module": "src/index.js" diff --git a/src/api/routes/note/note.controller.ts b/src/api/routes/note/note.controller.ts index 50b75a4..e0b1060 100644 --- a/src/api/routes/note/note.controller.ts +++ b/src/api/routes/note/note.controller.ts @@ -64,8 +64,12 @@ export class NoteController { ) ) .execute(); + let successStatus = true; + if(result.length===0){ + successStatus = false + }; return { - success: true, + success: successStatus, data: result, message: "", error: null, diff --git a/src/api/routes/note/note.route.ts b/src/api/routes/note/note.route.ts index 3863e6d..839dacf 100644 --- a/src/api/routes/note/note.route.ts +++ b/src/api/routes/note/note.route.ts @@ -44,7 +44,7 @@ export const noteRouter = new Elysia({ } ) .get( - ":id", + "/:id", async ({ note, user, params:{id} }) => { return await note.getNoteById(id, user.id); }, @@ -73,7 +73,7 @@ export const noteRouter = new Elysia({ } } ).patch( - ":id", + "/:id", async ({ body, note, user, params:{id} }) => { return await note.updateNoteById(id, body, user.id); }, @@ -89,7 +89,7 @@ export const noteRouter = new Elysia({ } } ).delete( - ":id", + "/:id", async ({ note, user, params:{id} }) => { return await note.deleteNoteById(id, user.id); }, diff --git a/src/api/routes/note/note.test.ts b/src/api/routes/note/note.test.ts new file mode 100644 index 0000000..304d241 --- /dev/null +++ b/src/api/routes/note/note.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'bun:test' +import { testClientApp } from '../../../../test/client'; + +let noteId: string; + +describe('Note', () => { + // Create a note before tests + it('Create Note', async () => { + const { data } = await testClientApp.api.note.post({ + "title": "test note", + "content": "description", + }) + if (!data?.data) { + throw new Error('create note api did not return data'); + } + noteId = data.data[0].id + expect(data.data[0].title).toBe('test note') + }) + + // Get all notes + it('Get All Notes', async () => { + const { data } = await testClientApp.api.note.get({ + query: { limit: 1, offset: 0 } + }) + expect(data?.data[0].id).toBe(noteId) + }) + + // Get single note + it('Get Created Note', async () => { + const { data, error } = await testClientApp.api.note({ id: noteId }).get() + expect(data?.data[0].id).toBe(noteId) + }) + + // Update note + it('Update Note', async () => { + const updatedTitle = "updated test note" + const updatedContent = "updated description" + + const { data } = await testClientApp.api.note({ id: noteId }).patch({ + title: updatedTitle, + content: updatedContent, + }) + + expect(data?.success).toBe(true) + expect(data?.data[0].title).toBe(updatedTitle) + expect(data?.data[0].content).toBe(updatedContent) + }) + + // Delete single note + it('Delete Single Note', async () => { + // First create a new note to delete + const { data: createData } = await testClientApp.api.note.post({ + "title": "note to delete", + "content": "this note will be deleted", + }) + const deleteNoteId = createData?.data[0].id + + + if (!deleteNoteId) { + throw new Error('Failed to receive noteId in delete note test'); + } + + // Delete the note + const { data: deleteData } = await testClientApp.api.note({ id: deleteNoteId }).delete() + expect(deleteData?.success).toBe(true) + + // Verify note is deleted by trying to fetch it + const { data: verifyData } = await testClientApp.api.note({ id: deleteNoteId }).get() + expect(verifyData?.data).toHaveLength(0) + }) + + // Delete all notes + it('Delete All Notes', async () => { + // First create multiple notes + await testClientApp.api.note.post({ + "title": "note 1", + "content": "content 1", + }) + await testClientApp.api.note.post({ + "title": "note 2", + "content": "content 2", + }) + + // Delete all notes + const { data: deleteData } = await testClientApp.api.note.delete() + expect(deleteData?.success).toBe(true) + + // Verify all notes are deleted + const { data: verifyData } = await testClientApp.api.note.get({ + query: { limit: 10, offset: 0 } + }) + expect(verifyData?.data).toHaveLength(0) + }) + + // Error cases + it('Should handle invalid note ID', async () => { + const invalidId = 'invalid-id' + const { data } = await testClientApp.api.note({ id: invalidId }).get() + expect(data?.success).toBe(false) + expect(data?.data).toHaveLength(0) + }) +}) \ No newline at end of file diff --git a/src/db/schema/auth.ts b/src/db/schema/auth.ts index c68d842..bac513a 100644 --- a/src/db/schema/auth.ts +++ b/src/db/schema/auth.ts @@ -54,3 +54,10 @@ export const rateLimit = authSchema.table("rate_limit", { count: integer('count'), lastRequest: integer('last_request') }); + +export const jwks = authSchema.table("jwks", { + id: text("id").primaryKey(), + publicKey: text("public_key").notNull(), + privateKey: text("private_key").notNull(), + createdAt: timestamp("created_at").notNull(), +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9c3cb33..98bccc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,8 @@ import { api } from "./api"; const baseConfig = getBaseConfig(); validateEnv(); -const app = new Elysia() + +export const app = new Elysia() .use(cors()) .use( opentelemetry({ diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index bb5a527..2bd5698 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -1,8 +1,8 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "../../db/index"; -import { openAPI } from "better-auth/plugins" -import { user, account, verification, session, rateLimit } from "../../db/schema/auth"; +import { jwt, openAPI } from "better-auth/plugins" +import { user, account, verification, session, rateLimit, jwks } from "../../db/schema/auth"; import { sendMail } from "../mail/mail"; import { renderToStaticMarkup } from "react-dom/server"; import { createElement } from "react"; @@ -17,8 +17,14 @@ export const auth = betterAuth({ account: account, verification: verification, rateLimit: rateLimit, + jwks: jwks } }), + user: { + deleteUser: { + enabled: true // [!Code Highlight] + } + }, rateLimit: { window: 60, max: 100, @@ -67,6 +73,7 @@ export const auth = betterAuth({ openAPI({ path: "/docs", }), + jwt() ], socialProviders: { /* diff --git a/test/client.ts b/test/client.ts new file mode 100644 index 0000000..72966db --- /dev/null +++ b/test/client.ts @@ -0,0 +1,27 @@ +import { treaty } from '@elysiajs/eden' +import { app } from '../src' +import { getAuthConfig } from '../src/lib/utils/env'; + +async function getAuthToken() { + const authUrl = getAuthConfig().BETTER_AUTH_URL + const response = await fetch(`${authUrl}/api/auth/sign-in/email`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: "test@test.com", + password: "testpass123" + }) + }); + const cookies = response.headers.getSetCookie()[0]; + const sessionToken = cookies.split(";")[0].split("=")[1] + return sessionToken; +} + +const token = await getAuthToken(); +export const testClientApp = treaty(app,{ + headers: { + Cookie: `better-auth.session_token=${token}` + } +}) \ No newline at end of file