From d75249ae137d3df4aa68122c05b741101fa4ff45 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:44:25 +0530 Subject: [PATCH 01/24] added new column (price) in subscriptions and in users table added not NUll constraint with default value USER --- app/src/db/schema.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/db/schema.ts b/app/src/db/schema.ts index 8567d10..23ac0d4 100644 --- a/app/src/db/schema.ts +++ b/app/src/db/schema.ts @@ -1,3 +1,4 @@ +import { ROLES } from "@/constants/roles"; import { segment } from "@/types/transcriptions"; import { timestamp, @@ -43,7 +44,7 @@ export const userTable = pgTable("user", { subscriptionId: text("subscriptionId") .notNull() .references(() => subscriptionTable.id), - role: text("role"), + role: text("role").notNull().default(ROLES.USER), createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), }); @@ -83,6 +84,7 @@ export const subscriptionTable = pgTable("subscriptions", { recordingCount: integer("recordingCount").notNull(), fileSizeLimitMB: integer("fileSizeLimitMB").notNull(), // Store all sizes in MB durationDays: integer("durationDays").notNull(), // Validity in days + price: integer("price").notNull().default(0), createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), }); From 9eac172290ef7563b375c885ff186efcda406de4 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:46:31 +0530 Subject: [PATCH 02/24] added roles constant and type to use in BE/FE .. added in schema --- app/src/constants/roles.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/src/constants/roles.ts diff --git a/app/src/constants/roles.ts b/app/src/constants/roles.ts new file mode 100644 index 0000000..fd5ee3f --- /dev/null +++ b/app/src/constants/roles.ts @@ -0,0 +1,7 @@ +// roles.ts +export const ROLES = { + USER: "USER", + ADMIN: "ADMIN", +} as const; + +export type Role = (typeof ROLES)[keyof typeof ROLES]; From e851aa43aca596805299d7498e7bbae5c4be6b16 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:48:01 +0530 Subject: [PATCH 03/24] added role in validate request to check in apis authorization --- app/src/auth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/auth.ts b/app/src/auth.ts index a4f1d58..c067cfe 100644 --- a/app/src/auth.ts +++ b/app/src/auth.ts @@ -20,6 +20,7 @@ export const lucia = new Lucia(adapter, { return { // attributes has the type of DatabaseUserAttributes username: attributes.username, + role: attributes.role, }; }, }); @@ -87,4 +88,5 @@ declare module "lucia" { interface DatabaseUserAttributes { username: string; + role: string; } From 73a603c7f4bbfc40f6ba03c15e7d108758096a59 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:49:15 +0530 Subject: [PATCH 04/24] created migration scripts for schema changes and added default values in columns --- app/migrations/0011_sour_maria_hill.sql | 11 + app/migrations/meta/0011_snapshot.json | 424 ++++++++++++++++++++++++ app/migrations/meta/_journal.json | 7 + 3 files changed, 442 insertions(+) create mode 100644 app/migrations/0011_sour_maria_hill.sql create mode 100644 app/migrations/meta/0011_snapshot.json diff --git a/app/migrations/0011_sour_maria_hill.sql b/app/migrations/0011_sour_maria_hill.sql new file mode 100644 index 0000000..08c6cea --- /dev/null +++ b/app/migrations/0011_sour_maria_hill.sql @@ -0,0 +1,11 @@ +-- Step 1: Set default if not already set +ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'USER'; + +-- Step 2: Fix existing data +UPDATE "user" SET role = 'USER' WHERE role IS NULL; + +-- Step 3: Set NOT NULL constraint +ALTER TABLE "user" ALTER COLUMN "role" SET NOT NULL; + +-- Step 4: New Column added and Set NOT NULL constraint with default value +ALTER TABLE "subscriptions" ADD COLUMN "price" integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/app/migrations/meta/0011_snapshot.json b/app/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..8d2d584 --- /dev/null +++ b/app/migrations/meta/0011_snapshot.json @@ -0,0 +1,424 @@ +{ + "id": "35e9d28c-c5e6-4839-af7a-89cd21d439d9", + "prevId": "3735879d-ed33-4d4e-bc88-a1a2865bebfd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.bot": { + "name": "bot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "botName": { + "name": "botName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botEmail": { + "name": "botEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botHd": { + "name": "botHd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botPicture": { + "name": "botPicture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bot_user_id_user_id_fk": { + "name": "bot_user_id_user_id_fk", + "tableFrom": "bot", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userEmail": { + "name": "userEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recordingCount": { + "name": "recordingCount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fileSizeLimitMB": { + "name": "fileSizeLimitMB", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "durationDays": { + "name": "durationDays", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_name_unique": { + "name": "subscriptions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + } + }, + "public.transcriptions": { + "name": "transcriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "translation": { + "name": "translation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "documentUrl": { + "name": "documentUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentName": { + "name": "documentName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "audioDuration": { + "name": "audioDuration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "transcriptions_user_id_user_id_fk": { + "name": "transcriptions_user_id_user_id_fk", + "tableFrom": "transcriptions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactNumber": { + "name": "contactNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscriptionId": { + "name": "subscriptionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USER'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_subscriptionId_subscriptions_id_fk": { + "name": "user_subscriptionId_subscriptions_id_fk", + "tableFrom": "user", + "tableTo": "subscriptions", + "columnsFrom": [ + "subscriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/app/migrations/meta/_journal.json b/app/migrations/meta/_journal.json index 2d8786f..c87d851 100644 --- a/app/migrations/meta/_journal.json +++ b/app/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1750831412800, "tag": "0010_brainy_pixie", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1750919306282, + "tag": "0011_sour_maria_hill", + "breakpoints": true } ] } \ No newline at end of file From 3a8646e40b95b41486cc360e4c99a882d6c50a6e Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:53:50 +0530 Subject: [PATCH 05/24] added admin auththorization wrapper function for admin routes only --- app/src/lib/withAdmin.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/lib/withAdmin.ts diff --git a/app/src/lib/withAdmin.ts b/app/src/lib/withAdmin.ts new file mode 100644 index 0000000..905501a --- /dev/null +++ b/app/src/lib/withAdmin.ts @@ -0,0 +1,17 @@ +// lib/withAdmin.ts +import { validateRequest } from "@/auth"; +import { NextResponse } from "next/server"; + +export function withAdmin Promise>( + handler: T +) { + return async (...args: Parameters): Promise => { + const { user } = await validateRequest(); + + if (!user || user.role !== "ADMIN") { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + return handler(...args); + }; +} From 057d9d443acf1b6886ffb4ce40a856e96b8fdbd0 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 12:54:46 +0530 Subject: [PATCH 06/24] added users , subscriptions get and update APIs --- app/src/app/api/admin/subscriptions/route.ts | 9 ++++ .../api/admin/subscriptions/update/route.ts | 23 ++++++++ app/src/app/api/admin/users/route.ts | 53 +++++++++++++++++++ .../admin/users/update-subscription/route.ts | 20 +++++++ 4 files changed, 105 insertions(+) create mode 100644 app/src/app/api/admin/subscriptions/route.ts create mode 100644 app/src/app/api/admin/subscriptions/update/route.ts create mode 100644 app/src/app/api/admin/users/route.ts create mode 100644 app/src/app/api/admin/users/update-subscription/route.ts diff --git a/app/src/app/api/admin/subscriptions/route.ts b/app/src/app/api/admin/subscriptions/route.ts new file mode 100644 index 0000000..b599d94 --- /dev/null +++ b/app/src/app/api/admin/subscriptions/route.ts @@ -0,0 +1,9 @@ +import { db } from "@/db"; +import { subscriptionTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { NextResponse } from "next/server"; + +export const GET = withAdmin(async function () { + const subscriptions = await db.select().from(subscriptionTable); + return NextResponse.json({ subscriptions }); +}); diff --git a/app/src/app/api/admin/subscriptions/update/route.ts b/app/src/app/api/admin/subscriptions/update/route.ts new file mode 100644 index 0000000..5fa7873 --- /dev/null +++ b/app/src/app/api/admin/subscriptions/update/route.ts @@ -0,0 +1,23 @@ +import { db } from "@/db"; +import { subscriptionTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const PATCH = withAdmin(async function (req: NextRequest) { + const { id, ...data } = await req.json(); + + if (!id) { + return NextResponse.json( + { error: "Subscription ID is required" }, + { status: 400 } + ); + } + + await db + .update(subscriptionTable) + .set(data) + .where(eq(subscriptionTable.id, id)); + + return NextResponse.json({ success: true }); +}); diff --git a/app/src/app/api/admin/users/route.ts b/app/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..28b0bc9 --- /dev/null +++ b/app/src/app/api/admin/users/route.ts @@ -0,0 +1,53 @@ +import { validateRequest } from "@/auth"; +import { db } from "@/db"; +import { userTable, subscriptionTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { ilike, eq, sql, or } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = withAdmin(async function (req: NextRequest) { + const { user } = await validateRequest(); + + if (!user || user.role !== "ADMIN") { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(req.url); + const page = parseInt(searchParams.get("page") || "1"); + const limit = parseInt(searchParams.get("limit") || "10"); + const search = searchParams.get("search")?.trim().toLowerCase() || ""; + + const offset = (page - 1) * limit; + + const whereClause = search + ? or( + ilike(userTable.username, `%${search}%`), + ilike(userTable.name, `%${search}%`), + ilike(userTable.contactNumber, `%${search}%`) + ) + : undefined; + + const users = await db + .select({ + id: userTable.id, + username: userTable.username, + name: userTable.name, + contactNumber: userTable.contactNumber, + role: userTable.role, + subscriptionName: subscriptionTable.name, + }) + .from(userTable) + .leftJoin( + subscriptionTable, + eq(userTable.subscriptionId, subscriptionTable.id) + ) + .where(whereClause) + .limit(limit) + .offset(offset); + + const [{ count }] = await db + .select({ count: sql`COUNT(*)` }) + .from(userTable) + .where(whereClause); + + return NextResponse.json({ users, total: count, page, limit }); +}); diff --git a/app/src/app/api/admin/users/update-subscription/route.ts b/app/src/app/api/admin/users/update-subscription/route.ts new file mode 100644 index 0000000..2f14af9 --- /dev/null +++ b/app/src/app/api/admin/users/update-subscription/route.ts @@ -0,0 +1,20 @@ +import { db } from "@/db"; +import { userTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const PATCH = withAdmin(async function (req: NextRequest) { + const { userId, subscriptionId } = await req.json(); + + if (!userId || !subscriptionId) { + return NextResponse.json({ error: "Missing parameters" }, { status: 400 }); + } + + await db + .update(userTable) + .set({ subscriptionId }) + .where(eq(userTable.id, userId)); + + return NextResponse.json({ success: true }); +}); From 2e644e9f21f4b230bd5dbbf4f284760983fe169f Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:13:38 +0530 Subject: [PATCH 07/24] changed user response --- app/src/app/api/admin/users/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/app/api/admin/users/route.ts b/app/src/app/api/admin/users/route.ts index 28b0bc9..e5cfbf9 100644 --- a/app/src/app/api/admin/users/route.ts +++ b/app/src/app/api/admin/users/route.ts @@ -33,7 +33,12 @@ export const GET = withAdmin(async function (req: NextRequest) { name: userTable.name, contactNumber: userTable.contactNumber, role: userTable.role, - subscriptionName: subscriptionTable.name, + subscription: { + name: subscriptionTable.name, + recordingCount: subscriptionTable.recordingCount, + fileSizeLimitMB: subscriptionTable.fileSizeLimitMB, + durationDays: subscriptionTable.durationDays, + }, }) .from(userTable) .leftJoin( From 4d169e257597ddba83feea91e2f711df6a67929b Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:14:27 +0530 Subject: [PATCH 08/24] removed reparate endpoint of update ... handled in by id route --- .../api/admin/subscriptions/update/route.ts | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 app/src/app/api/admin/subscriptions/update/route.ts diff --git a/app/src/app/api/admin/subscriptions/update/route.ts b/app/src/app/api/admin/subscriptions/update/route.ts deleted file mode 100644 index 5fa7873..0000000 --- a/app/src/app/api/admin/subscriptions/update/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from "@/db"; -import { subscriptionTable } from "@/db/schema"; -import { withAdmin } from "@/lib/withAdmin"; -import { eq } from "drizzle-orm"; -import { NextRequest, NextResponse } from "next/server"; - -export const PATCH = withAdmin(async function (req: NextRequest) { - const { id, ...data } = await req.json(); - - if (!id) { - return NextResponse.json( - { error: "Subscription ID is required" }, - { status: 400 } - ); - } - - await db - .update(subscriptionTable) - .set(data) - .where(eq(subscriptionTable.id, id)); - - return NextResponse.json({ success: true }); -}); From f8f0081168d0f2877d0d6f7450bf25e45ab96dfa Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:15:14 +0530 Subject: [PATCH 09/24] sending updated user when subscription changed --- .../api/admin/users/update-subscription/route.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/app/api/admin/users/update-subscription/route.ts b/app/src/app/api/admin/users/update-subscription/route.ts index 2f14af9..d26c164 100644 --- a/app/src/app/api/admin/users/update-subscription/route.ts +++ b/app/src/app/api/admin/users/update-subscription/route.ts @@ -11,10 +11,18 @@ export const PATCH = withAdmin(async function (req: NextRequest) { return NextResponse.json({ error: "Missing parameters" }, { status: 400 }); } - await db + const [updatedUser] = await db .update(userTable) .set({ subscriptionId }) - .where(eq(userTable.id, userId)); + .where(eq(userTable.id, userId)) + .returning(); - return NextResponse.json({ success: true }); + if (!updatedUser) { + return NextResponse.json( + { error: "User not found or not updated" }, + { status: 404 } + ); + } + + return NextResponse.json({ user: updatedUser }); }); From bc9930a47d8c389ee9bc36dae78ae302f5ca4a3f Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:16:36 +0530 Subject: [PATCH 10/24] added GET by id and PATCH by id on same route ..to update subscription details --- .../app/api/admin/subscriptions/[id]/route.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 app/src/app/api/admin/subscriptions/[id]/route.ts diff --git a/app/src/app/api/admin/subscriptions/[id]/route.ts b/app/src/app/api/admin/subscriptions/[id]/route.ts new file mode 100644 index 0000000..d44214a --- /dev/null +++ b/app/src/app/api/admin/subscriptions/[id]/route.ts @@ -0,0 +1,59 @@ +import { db } from "@/db"; +import { subscriptionTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +// GET /api/admin/subscriptions/:id +export const GET = withAdmin(async function ( + _req: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + + const subscription = await db + .select() + .from(subscriptionTable) + .where(eq(subscriptionTable.id, id)) + .then((res) => res[0]); + + if (!subscription) { + return NextResponse.json( + { error: "Subscription not found" }, + { status: 404 } + ); + } + + return NextResponse.json(subscription); +}); + +// PATCH /api/admin/subscriptions/:id +export const PATCH = withAdmin(async function ( + req: NextRequest, + { params }: { params: { id: string } } +) { + const data = await req.json(); + const { id } = params; + + if (!id) { + return NextResponse.json( + { error: "Subscription ID is required" }, + { status: 400 } + ); + } + + const [updated] = await db + .update(subscriptionTable) + .set(data) + .where(eq(subscriptionTable.id, id)) + .returning(); + + if (!updated) { + return NextResponse.json( + { error: "Subscription not found" }, + { status: 404 } + ); + } + + return NextResponse.json(updated); +}); From 9a28558813f5f3542426219cd2d17239f5967db0 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:17:55 +0530 Subject: [PATCH 11/24] added GET by user and PATCH user By id route on same endpoint --- app/src/app/api/admin/users/[id]/route.ts | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 app/src/app/api/admin/users/[id]/route.ts diff --git a/app/src/app/api/admin/users/[id]/route.ts b/app/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..613ed5f --- /dev/null +++ b/app/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,67 @@ +import { db } from "@/db"; +import { subscriptionTable, userTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { ROLES } from "@/constants/roles"; + +// GET /api/admin/users/:id +export const GET = withAdmin(async function ( + _req: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + const user = await db + .select({ + id: userTable.id, + username: userTable.username, + name: userTable.name, + contactNumber: userTable.contactNumber, + role: userTable.role, + subscription: { + name: subscriptionTable.name, + recordingCount: subscriptionTable.recordingCount, + fileSizeLimitMB: subscriptionTable.fileSizeLimitMB, + durationDays: subscriptionTable.durationDays, + }, + }) + .from(userTable) + .leftJoin( + subscriptionTable, + eq(userTable.subscriptionId, subscriptionTable.id) + ) + .where(eq(userTable.id, id)) + .then((res) => res[0]); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json(user); +}); + +// PATCH /api/admin/users/:id +export const PATCH = withAdmin(async function ( + req: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + const data = await req.json(); + + // Optional: Validate `role` if it's present in the payload + if (data.role && ![ROLES.ADMIN, ROLES.USER].includes(data.role)) { + return NextResponse.json({ error: "Invalid role" }, { status: 400 }); + } + + const [updated] = await db + .update(userTable) + .set(data) + .where(eq(userTable.id, id)) + .returning(); + + if (!updated) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + const { password_hash, ...user } = updated; // removed password hash from response + return NextResponse.json(user); +}); From 120711c914f32d400deeed73e76647adbd23c3e3 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 15:19:21 +0530 Subject: [PATCH 12/24] added route to change role of the specific user --- .../app/api/admin/users/update-role/route.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/src/app/api/admin/users/update-role/route.ts diff --git a/app/src/app/api/admin/users/update-role/route.ts b/app/src/app/api/admin/users/update-role/route.ts new file mode 100644 index 0000000..165de7c --- /dev/null +++ b/app/src/app/api/admin/users/update-role/route.ts @@ -0,0 +1,40 @@ +import { ROLES } from "@/constants/roles"; +import { db } from "@/db"; +import { userTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const PATCH = withAdmin(async function (req: NextRequest) { + const { userId, role } = await req.json(); + + const roles = [ROLES.ADMIN, ROLES.USER] as never as [string]; + if ( + typeof userId !== "string" || + typeof role !== "string" || + !roles.includes(role) + ) { + return NextResponse.json( + { error: "Invalid or missing parameters" }, + { status: 400 } + ); + } + + const [updatedUser] = await db + .update(userTable) + .set({ role }) + .where(eq(userTable.id, userId)) + .returning({ + id: userTable.id, + username: userTable.username, + name: userTable.name, + contactNumber: userTable.contactNumber, + role: userTable.role, + }); + + if (!updatedUser) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json({ user: updatedUser }); +}); From 5c9a4147ba250cce10eda7e4b88eb6ab6e646e63 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 16:00:22 +0530 Subject: [PATCH 13/24] added role in login requst --- app/src/app/api/signin/route.ts | 111 +++++++++++++++++--------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/app/src/app/api/signin/route.ts b/app/src/app/api/signin/route.ts index 3c9c1aa..c8b31fc 100644 --- a/app/src/app/api/signin/route.ts +++ b/app/src/app/api/signin/route.ts @@ -8,67 +8,74 @@ import { cookies } from "next/headers"; import { z } from "zod"; export async function POST(req: Request) { - try { - const body = await req.json(); - const { password, userEmail } = signinUserSchema.parse(body); + try { + const body = await req.json(); + const { password, userEmail } = signinUserSchema.parse(body); - // Check if user exists - const existingUser = await db - .select() - .from(userTable) - .where(eq(userTable.username, userEmail)); + // Check if user exists + const existingUser = await db + .select() + .from(userTable) + .where(eq(userTable.username, userEmail)); - if (!existingUser[0]) { - return new Response("User not found", { - status: 404, - }); - } - - const user = existingUser[0]; - - // Verify password - const validPassword = await verify(user.password_hash, password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); + if (!existingUser[0]) { + return new Response("User not found", { + status: 404, + }); + } - if (!validPassword) { - return new Response("Incorrect username or password", { status: 401 }); - } + const user = existingUser[0]; - // Create session and set cookie - const session = await lucia.createSession(user.id, {}); - const sessionCookie = lucia.createSessionCookie(session.id); - (await cookies()).set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + // Verify password + const validPassword = await verify(user.password_hash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); - // Check if bot is added - const bot = await db - .select() - .from(botTable) - .where(eq(botTable.userId, user.id)); + if (!validPassword) { + return new Response("Incorrect username or password", { status: 401 }); + } - const isBotAdded = !!bot[0]; + // Create session and set cookie + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + (await cookies()).set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); - // Respond with success and isBotAdded flag - return new Response(JSON.stringify({ - message: "User Logged In", - data: { - isBotAdded - } - }), { - status: 200, - headers: { "Content-Type": "application/json" } - }); + // Check if bot is added + const bot = await db + .select() + .from(botTable) + .where(eq(botTable.userId, user.id)); - } catch (error) { - console.error(error); + const isBotAdded = !!bot[0]; - if (error instanceof z.ZodError) { - return new Response(error.message, { status: 422 }); - } + // Respond with success and isBotAdded flag + return new Response( + JSON.stringify({ + message: "User Logged In", + data: { + isBotAdded, + role: user.role || null, + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error(error); - return new Response("Failed to login user", { status: 500 }); + if (error instanceof z.ZodError) { + return new Response(error.message, { status: 422 }); } + + return new Response("Failed to login user", { status: 500 }); + } } From 93380de9e212225ad8658f0585ded8628f4db8c6 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 16:01:38 +0530 Subject: [PATCH 14/24] navigation changes if role is admin --- app/src/hooks/useUser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/hooks/useUser.ts b/app/src/hooks/useUser.ts index 8a55250..cb7beff 100644 --- a/app/src/hooks/useUser.ts +++ b/app/src/hooks/useUser.ts @@ -7,6 +7,7 @@ import { useRouter } from "next/navigation"; import { SigninUserRequest, SignupUserRequest } from "@/Validators/register"; import { useState } from "react"; import Cookies from "js-cookie"; +import { ROLES } from "@/constants/roles"; export const useUser = () => { const router = useRouter(); @@ -76,7 +77,7 @@ export const useUser = () => { // Wait for the toast to be shown a bit before redirect setTimeout(() => { - router.push("/new"); // Navigate to /new + router.push(res.data.role === ROLES.USER ? "/new" : "/admin/users"); // Navigate to /new if role is USER else navigate to admin route router.refresh(); // Force a layout/server refresh }, 100); // Adjust timing if needed }, From 12acd4863b20c75b6d76e2131e7d660bb7bcb3e7 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 16:13:36 +0530 Subject: [PATCH 15/24] added common axios client --- app/src/lib/axios.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/lib/axios.ts diff --git a/app/src/lib/axios.ts b/app/src/lib/axios.ts new file mode 100644 index 0000000..892c487 --- /dev/null +++ b/app/src/lib/axios.ts @@ -0,0 +1,9 @@ +// Creating common axios client for api calls +import axios from "axios"; + +export const API = axios.create({ + baseURL: "/api", + headers: { + "Content-Type": "application/json", + }, +}); From 19dc599b5fba5989af619a3dd2ed23c5be005450 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Thu, 26 Jun 2025 16:15:13 +0530 Subject: [PATCH 16/24] added admin route and basic functionality --- app/src/app/admin/layout.tsx | 30 +++++++++ app/src/app/admin/page.tsx | 0 app/src/app/admin/subscriptions/page.tsx | 86 ++++++++++++++++++++++++ app/src/app/admin/users/page.tsx | 85 +++++++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 app/src/app/admin/layout.tsx create mode 100644 app/src/app/admin/page.tsx create mode 100644 app/src/app/admin/subscriptions/page.tsx create mode 100644 app/src/app/admin/users/page.tsx diff --git a/app/src/app/admin/layout.tsx b/app/src/app/admin/layout.tsx new file mode 100644 index 0000000..825366f --- /dev/null +++ b/app/src/app/admin/layout.tsx @@ -0,0 +1,30 @@ +// app/admin/layout.tsx +import Link from "next/link"; +import { validateRequest } from "@/auth"; +import { redirect } from "next/navigation"; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const { user } = await validateRequest(); + if (!user || user.role !== "ADMIN") redirect("/"); + + return ( +
+ +
{children}
+
+ ); +} diff --git a/app/src/app/admin/page.tsx b/app/src/app/admin/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/src/app/admin/subscriptions/page.tsx b/app/src/app/admin/subscriptions/page.tsx new file mode 100644 index 0000000..9a186a6 --- /dev/null +++ b/app/src/app/admin/subscriptions/page.tsx @@ -0,0 +1,86 @@ +// app/admin/subscriptions/page.tsx +"use client"; + +import { useEffect, useState } from "react"; +import { API } from "@/lib/axios"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export default function SubscriptionsPage() { + const [subs, setSubs] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchSubscriptions = async () => { + try { + setLoading(true); + const { data } = await API.get("/admin/subscriptions"); + setSubs(data.subscriptions); + } catch (err) { + console.error("Failed to fetch subscriptions:", err); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + try { + // await API.delete(`/admin/subscriptions/${id}`); + setSubs((prev) => prev.filter((s) => s.id !== id)); + } catch (err) { + console.error("Delete failed:", err); + } + }; + + useEffect(() => { + fetchSubscriptions(); + }, []); + + return ( + + +

Subscriptions

+ {loading ? ( +

Loading...

+ ) : ( + + + + Name + Recording Count + File Size Limit (MB) + Duration (Days) + Actions + + + + {subs.map((s) => ( + + {s.name} + {s.recordingCount} + {s.fileSizeLimitMB} + {s.durationDays} + + + + + ))} + +
+ )} +
+
+ ); +} diff --git a/app/src/app/admin/users/page.tsx b/app/src/app/admin/users/page.tsx new file mode 100644 index 0000000..54f4527 --- /dev/null +++ b/app/src/app/admin/users/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { API } from "@/lib/axios"; // Capitalized Axios instance +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export default function UsersPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchUsers = async () => { + try { + setLoading(true); + const { data } = await API.get("/admin/users"); // Capitalized route path + setUsers(data.users); + } catch (err) { + console.error("Failed to fetch users:", err); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + try { + // await API.delete(`/admin/users/${id}`); + setUsers((prev) => prev.filter((u) => u.id !== id)); + } catch (err) { + console.error("Delete failed:", err); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + return ( + + +

Users

+ {loading ? ( +

Loading...

+ ) : ( + + + + Username + Name + Role + Subscription + Actions + + + + {users.map((u) => ( + + {u.username} + {u.name} + {u.role} + {u.subscription.name} + + + + + ))} + +
+ )} +
+
+ ); +} From cfc05819bd65336d88b5fefbed9ec4198f71991e Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Fri, 27 Jun 2025 15:42:14 +0530 Subject: [PATCH 17/24] added post api for the subscription add --- app/src/app/api/admin/subscriptions/route.ts | 59 +++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/app/src/app/api/admin/subscriptions/route.ts b/app/src/app/api/admin/subscriptions/route.ts index b599d94..2148028 100644 --- a/app/src/app/api/admin/subscriptions/route.ts +++ b/app/src/app/api/admin/subscriptions/route.ts @@ -1,9 +1,66 @@ +// src/app/api/admin/subscriptions/route.ts + import { db } from "@/db"; import { subscriptionTable } from "@/db/schema"; import { withAdmin } from "@/lib/withAdmin"; -import { NextResponse } from "next/server"; +import { ilike } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +// Define schema for request validation +const subscriptionSchema = z.object({ + name: z.string().min(1), + recordingCount: z.number().int().min(0), + fileSizeLimitMB: z.number().int().min(1), + durationDays: z.number().int().min(1), + price: z.number().int().min(0).default(0), +}); +// GET: Fetch all subscriptions export const GET = withAdmin(async function () { const subscriptions = await db.select().from(subscriptionTable); return NextResponse.json({ subscriptions }); }); + +// POST: Create a new subscription + +export const POST = withAdmin(async function (req: NextRequest) { + try { + const json = await req.json(); + const data = subscriptionSchema.parse(json); + + // 🔍 Check if a subscription with the same name (case-insensitive) exists + const existing = await db + .select() + .from(subscriptionTable) + .where(ilike(subscriptionTable.name, data.name)); + + if (existing.length > 0) { + return NextResponse.json( + { error: `Subscription "${data.name}" already exists` }, + { status: 409 } + ); + } + + const [newSub] = await db + .insert(subscriptionTable) + .values(data) + .returning(); + + return NextResponse.json(newSub, { status: 201 }); + } catch (err) { + console.error("POST /admin/subscriptions error:", err); + + if (err instanceof z.ZodError) { + return NextResponse.json( + { error: "Validation failed", issues: err.errors }, + { status: 422 } + ); + } + + return NextResponse.json( + { error: "Failed to create subscription" }, + { status: 500 } + ); + } +}); From d281fbee0eed72f147ab72c2be494ffb97f14a4f Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Fri, 27 Jun 2025 16:46:12 +0530 Subject: [PATCH 18/24] added count of remaining and used count for the recordings --- app/src/app/api/admin/users/[id]/route.ts | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/app/api/admin/users/[id]/route.ts b/app/src/app/api/admin/users/[id]/route.ts index 613ed5f..edc6890 100644 --- a/app/src/app/api/admin/users/[id]/route.ts +++ b/app/src/app/api/admin/users/[id]/route.ts @@ -1,7 +1,7 @@ import { db } from "@/db"; -import { subscriptionTable, userTable } from "@/db/schema"; +import { subscriptionTable, transcriptions, userTable } from "@/db/schema"; import { withAdmin } from "@/lib/withAdmin"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { ROLES } from "@/constants/roles"; @@ -11,7 +11,9 @@ export const GET = withAdmin(async function ( { params }: { params: { id: string } } ) { const { id } = params; - const user = await db + + // Fetch user with subscription info + const [user] = await db .select({ id: userTable.id, username: userTable.username, @@ -30,16 +32,28 @@ export const GET = withAdmin(async function ( subscriptionTable, eq(userTable.subscriptionId, subscriptionTable.id) ) - .where(eq(userTable.id, id)) - .then((res) => res[0]); + .where(eq(userTable.id, id)); if (!user) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } - return NextResponse.json(user); -}); + // Count user’s personal (non-default) transcriptions + const [{ count: usedCount }] = await db + .select({ count: sql`COUNT(*)` }) + .from(transcriptions) + .where(eq(transcriptions.userID, id)); + + // Calculate remaining recordings + const limit = user.subscription?.recordingCount ?? 0; + const remaining = Math.max(limit - usedCount, 0); + return NextResponse.json({ + ...user, + recordingsUsed: usedCount, + recordingsRemaining: remaining, + }); +}); // PATCH /api/admin/users/:id export const PATCH = withAdmin(async function ( req: NextRequest, From 18b3819d353a5ff039880803326c050cafbc362b Mon Sep 17 00:00:00 2001 From: Pooja lande Date: Fri, 27 Jun 2025 18:39:41 +0530 Subject: [PATCH 19/24] Added edit functionality for user record --- app/src/app/admin/users/page.tsx | 93 +++++----- app/src/components/EditSubscription.tsx | 222 ++++++++++++++++++++++++ 2 files changed, 276 insertions(+), 39 deletions(-) create mode 100644 app/src/components/EditSubscription.tsx diff --git a/app/src/app/admin/users/page.tsx b/app/src/app/admin/users/page.tsx index 54f4527..f51f5a2 100644 --- a/app/src/app/admin/users/page.tsx +++ b/app/src/app/admin/users/page.tsx @@ -12,11 +12,14 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { Edit2, Trash2 } from "lucide-react"; +import { Modal } from "@/components/ui/modal"; +import EditSubscription from "@/components/EditSubscription"; export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); - + const [isModalOpen, setIsModalOpen] = useState(null); const fetchUsers = async () => { try { setLoading(true); @@ -39,47 +42,59 @@ export default function UsersPage() { }; useEffect(() => { + if (!isModalOpen) { + fetchUsers(); + } fetchUsers(); - }, []); + }, [isModalOpen]); return ( - - -

Users

- {loading ? ( -

Loading...

- ) : ( - - - - Username - Name - Role - Subscription - Actions - - - - {users.map((u) => ( - - {u.username} - {u.name} - {u.role} - {u.subscription.name} - - - + <> + + +

Users

+ {loading ? ( +

Loading...

+ ) : ( +
+ + + Username + Name + Role + Subscription + Actions - ))} - -
- )} -
-
+ + + {users.map((u) => ( + + {u.username} + {u.name} + {u.role} + {u.subscription.name} + +
setIsModalOpen(u.id)} className="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow-xl hover:bg-green-500 cursor-pointer border-green-800 hover:text-white"> + +
+
+ handleDelete(u.id)} size={20} /> +
+
+
+ ))} +
+ + )} + + + setIsModalOpen(null)} + title="Update Subscription" + > + + + ); } diff --git a/app/src/components/EditSubscription.tsx b/app/src/components/EditSubscription.tsx new file mode 100644 index 0000000..0ec40d9 --- /dev/null +++ b/app/src/components/EditSubscription.tsx @@ -0,0 +1,222 @@ +"use client"; +import { useEffect, useState } from "react"; +import { API } from "@/lib/axios"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { Check, Loader2, RefreshCcw } from "lucide-react"; + +const EditSubscription = ({ userId ,setIsModalOpen}: { userId: string | null, setIsModalOpen: any }) => { + const router = useRouter(); + const [profileData, setProfileData] = useState({ + username: "", + name: "", + contactNumber: "", + role: "", + subscription: { + id: "", + name: "", + recordingCount: 0, + fileSizeLimitMB: 0, + durationDays: 0, + }, + recordingsUsed: 0, + recordingsRemaining: 0, + }); +const [hasChanged,setHasChanged] = useState(false) + const [subscriptions, setSubscriptions] = useState< + { + id: string; + name: string; + recordingCount: number; + fileSizeLimitMB: number; + durationDays: number; + }[] + >([]); + + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(""); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + if (!userId) return; + + const [userRes, subsRes] = await Promise.all([ + API.get(`/admin/users/${userId}`), + API.get("/admin/subscriptions"), + ]); + + const user = userRes.data; + const subs = subsRes.data.subscriptions; + + setSubscriptions(subs); + + const selectedSub = subs.find( + (s: any) => s.name === user.subscription?.name + ); + + const subId = selectedSub?.id || ""; + + setProfileData({ + username: user.username || "", + name: user.name || "", + contactNumber: user.contactNumber || "", + role: user.role || "", + subscription: { + id: subId, + name: selectedSub?.name || "", + recordingCount: selectedSub?.recordingCount || 0, + fileSizeLimitMB: selectedSub?.fileSizeLimitMB || 0, + durationDays: selectedSub?.durationDays || 0, + }, + recordingsUsed: user.recordingsUsed || 0, + recordingsRemaining: user.recordingsRemaining || 0, + }); + + setSelectedSubscriptionId(subId); + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [userId]); + + const { + name, + username, + contactNumber, + role, + subscription, + recordingsUsed, + recordingsRemaining, + } = profileData; + + const handleSubscriptionChange = (subscriptionId: string) => { + const selected = subscriptions.find((s) => s.id === subscriptionId); + if (!selected) return; + + setSelectedSubscriptionId(subscriptionId); + + // setProfileData((prev) => ({ + // ...prev, + // subscription: { + // id: selected.id, + // name: selected.name, + // recordingCount: selected.recordingCount, + // fileSizeLimitMB: selected.fileSizeLimitMB, + // durationDays: selected.durationDays, + // }, + // })); + }; + + const handleUpdate = async () => { + try { + setUpdating(true); + await API.patch(`/admin/users/${userId}`, { + subscriptionId: selectedSubscriptionId, + }); +toast.success("Subscription updated successfully"); + router.push("/admin/users"); + setIsModalOpen(null); + // alert("Subscription updated successfully"); + } catch (err) { + console.error("Update failed:", err); + // alert("Failed to update subscription."); + } finally { + setUpdating(false); + } + }; +useEffect(()=>{ + setHasChanged(selectedSubscriptionId !== subscription.id); +},[selectedSubscriptionId]) + + return ( +
+ {/* Profile Initial */} +
+ {loading ? ( +
+ ) : ( + name?.charAt(0)?.toUpperCase() || "?" + )} +
+ + {/* Basic Info */} +
+

{loading ? "..." : name || "N/A"}

+

{username || "N/A"}

+

{contactNumber || "N/A"}

+

{role || "N/A"}

+
+ + {/* Separator */} +
+ + {/* Subscription Plan Selector */} +
+
+ Subscription Plan: + +
+ + + {hasChanged && ( + + )} +
+
+
+ + + {/* Plan Details */} + {!loading && ( +
+

+ Plan Details +

+
+
+ Limit + {subscription.recordingCount} +
+
+ Used Recordings + {recordingsUsed} +
+
+ Remaining Recordings + {recordingsRemaining} +
+
+
+ )} +
+ ); +}; + +export default EditSubscription; From 9209aed159cdec1adbb24c1d97fb9924c0a54135 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Tue, 1 Jul 2025 14:32:56 +0530 Subject: [PATCH 20/24] added changes in amdin route handler --- app/src/lib/withAdmin.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/lib/withAdmin.ts b/app/src/lib/withAdmin.ts index 905501a..4656912 100644 --- a/app/src/lib/withAdmin.ts +++ b/app/src/lib/withAdmin.ts @@ -1,17 +1,17 @@ -// lib/withAdmin.ts import { validateRequest } from "@/auth"; import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; -export function withAdmin Promise>( - handler: T -) { - return async (...args: Parameters): Promise => { +export function withAdmin< + T extends (req: NextRequest, ctx: { params: any }) => Promise +>(handler: T) { + return async (req: NextRequest, context: { params: any }) => { const { user } = await validateRequest(); if (!user || user.role !== "ADMIN") { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return handler(...args); + return handler(req, context); // âś… Pass context (params) to your handler }; } From 07cc157e6e7bac3fbe745236303c54a97339ceed Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Tue, 1 Jul 2025 14:34:47 +0530 Subject: [PATCH 21/24] added sample transcription get api --- .../api/admin/transcriptions/sample/route.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 app/src/app/api/admin/transcriptions/sample/route.ts diff --git a/app/src/app/api/admin/transcriptions/sample/route.ts b/app/src/app/api/admin/transcriptions/sample/route.ts new file mode 100644 index 0000000..c2c9cd3 --- /dev/null +++ b/app/src/app/api/admin/transcriptions/sample/route.ts @@ -0,0 +1,41 @@ +import { db } from "@/db"; +import { transcriptions } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq, sql } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = withAdmin(async function (req: NextRequest) { + const { searchParams } = new URL(req.url); + const page = parseInt(searchParams.get("page") || "1"); + const limit = parseInt(searchParams.get("limit") || "10"); + const offset = (page - 1) * limit; + + // Fetch sample transcriptions + const sampleTranscriptions = await db + .select({ + id: transcriptions.id, + documentName: transcriptions.documentName, + translation: transcriptions.translation, + summary: transcriptions.summary, + createdAt: transcriptions.createdAt, + audioDuration: transcriptions.audioDuration, + isDefault: transcriptions.isDefault, + }) + .from(transcriptions) + .where(eq(transcriptions.isDefault, true)) + .limit(limit) + .offset(offset); + + const [{ count }] = await db + .select({ count: sql`COUNT(*)` }) + .from(transcriptions) + .where(eq(transcriptions.isDefault, true)); + + return NextResponse.json({ + data: sampleTranscriptions, + total: count, + page, + limit, + totalPages: Math.ceil(count / limit), + }); +}); From cfe04988050f350920c1e2c1879479d531165413 Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Tue, 1 Jul 2025 14:37:14 +0530 Subject: [PATCH 22/24] added GET and DELETE by id routes for transcriptions --- .../api/admin/transcriptions/[id]/route.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 app/src/app/api/admin/transcriptions/[id]/route.ts diff --git a/app/src/app/api/admin/transcriptions/[id]/route.ts b/app/src/app/api/admin/transcriptions/[id]/route.ts new file mode 100644 index 0000000..3229c24 --- /dev/null +++ b/app/src/app/api/admin/transcriptions/[id]/route.ts @@ -0,0 +1,47 @@ +import { db } from "@/db"; +import { transcriptions } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = withAdmin(async function ( + _req: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + + const [transcription] = await db + .select() + .from(transcriptions) + .where(eq(transcriptions.id, id)); + + if (!transcription) { + return NextResponse.json( + { error: "Transcription not found" }, + { status: 404 } + ); + } + + return NextResponse.json(transcription); +}); + +export const DELETE = withAdmin(async function ( + _req: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + + const [deleted] = await db + .delete(transcriptions) + .where(eq(transcriptions.id, id)) + .returning(); + + if (!deleted) { + return NextResponse.json( + { error: "Transcription not found" }, + { status: 404 } + ); + } + + return NextResponse.json({ success: true, id }); +}); From f17e85869545426849e28b6ea1e89ed015e6bf9e Mon Sep 17 00:00:00 2001 From: ingale12345 Date: Tue, 1 Jul 2025 14:38:21 +0530 Subject: [PATCH 23/24] added get transcriptions by id route .. user specific transcriptions --- .../users/transcriptions/[userId]/route.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/src/app/api/admin/users/transcriptions/[userId]/route.ts diff --git a/app/src/app/api/admin/users/transcriptions/[userId]/route.ts b/app/src/app/api/admin/users/transcriptions/[userId]/route.ts new file mode 100644 index 0000000..5406459 --- /dev/null +++ b/app/src/app/api/admin/users/transcriptions/[userId]/route.ts @@ -0,0 +1,56 @@ +import { db } from "@/db"; +import { transcriptions, userTable } from "@/db/schema"; +import { withAdmin } from "@/lib/withAdmin"; +import { and, eq, sql } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = withAdmin(async function ( + req: NextRequest, + { params }: { params: { userId: string } } +) { + const { searchParams } = new URL(req.url); + const page = parseInt(searchParams.get("page") || "1"); + const limit = parseInt(searchParams.get("limit") || "10"); + const offset = (page - 1) * limit; + const userId = params.userId; + console.log("userId is ", userId); + // Validate user exists + const [user] = await db + .select({ id: userTable.id }) + .from(userTable) + .where(eq(userTable.id, userId)); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Fetch paginated transcriptions + const transcriptionsList = await db + .select({ + id: transcriptions.id, + documentName: transcriptions.documentName, + translation: transcriptions.translation, + summary: transcriptions.summary, + createdAt: transcriptions.createdAt, + audioDuration: transcriptions.audioDuration, + isDefault: transcriptions.isDefault, + }) + .from(transcriptions) + .where(eq(transcriptions.userID, userId)) + .limit(limit) + .offset(offset); + + // Count total records + const [{ count }] = await db + .select({ count: sql`COUNT(*)` }) + .from(transcriptions) + .where(eq(transcriptions.userID, userId)); + + return NextResponse.json({ + data: transcriptionsList, + total: count, + page, + limit, + totalPages: Math.ceil(count / limit), + }); +}); From d199bbc629650d48d418d46925a1157a7eab6a4b Mon Sep 17 00:00:00 2001 From: Pooja lande Date: Thu, 3 Jul 2025 12:10:40 +0530 Subject: [PATCH 24/24] api integration for user , subscription section and added modal for users and subscription --- .../admin}/components/EditSubscription.tsx | 106 ++++++++-------- .../app/admin/components/SubscriptionEdit.tsx | 98 ++++++++++++++ app/src/app/admin/recordings/[id]/page.tsx | 92 ++++++++++++++ app/src/app/admin/subscriptions/page.tsx | 120 +++++++++++------- .../users/[id]/recordings/RecordingsPage.tsx | 95 ++++++++++++++ .../app/admin/users/[id]/recordings/page.tsx | 11 ++ app/src/app/admin/users/page.tsx | 44 ++++--- .../app/api/admin/subscriptions/[id]/route.ts | 39 ++++-- 8 files changed, 472 insertions(+), 133 deletions(-) rename app/src/{ => app/admin}/components/EditSubscription.tsx (72%) create mode 100644 app/src/app/admin/components/SubscriptionEdit.tsx create mode 100644 app/src/app/admin/recordings/[id]/page.tsx create mode 100644 app/src/app/admin/users/[id]/recordings/RecordingsPage.tsx create mode 100644 app/src/app/admin/users/[id]/recordings/page.tsx diff --git a/app/src/components/EditSubscription.tsx b/app/src/app/admin/components/EditSubscription.tsx similarity index 72% rename from app/src/components/EditSubscription.tsx rename to app/src/app/admin/components/EditSubscription.tsx index 0ec40d9..d38620a 100644 --- a/app/src/components/EditSubscription.tsx +++ b/app/src/app/admin/components/EditSubscription.tsx @@ -3,9 +3,15 @@ import { useEffect, useState } from "react"; import { API } from "@/lib/axios"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; -import { Check, Loader2, RefreshCcw } from "lucide-react"; - -const EditSubscription = ({ userId ,setIsModalOpen}: { userId: string | null, setIsModalOpen: any }) => { +import { Check, Loader2 } from "lucide-react"; + +const EditSubscription = ({ + userId, + setIsModalOpen, +}: { + userId: string | null; + setIsModalOpen: any; +}) => { const router = useRouter(); const [profileData, setProfileData] = useState({ username: "", @@ -22,7 +28,7 @@ const EditSubscription = ({ userId ,setIsModalOpen}: { userId: string | null, se recordingsUsed: 0, recordingsRemaining: 0, }); -const [hasChanged,setHasChanged] = useState(false) + const [hasChanged, setHasChanged] = useState(false); const [subscriptions, setSubscriptions] = useState< { id: string; @@ -100,17 +106,6 @@ const [hasChanged,setHasChanged] = useState(false) if (!selected) return; setSelectedSubscriptionId(subscriptionId); - - // setProfileData((prev) => ({ - // ...prev, - // subscription: { - // id: selected.id, - // name: selected.name, - // recordingCount: selected.recordingCount, - // fileSizeLimitMB: selected.fileSizeLimitMB, - // durationDays: selected.durationDays, - // }, - // })); }; const handleUpdate = async () => { @@ -119,7 +114,7 @@ const [hasChanged,setHasChanged] = useState(false) await API.patch(`/admin/users/${userId}`, { subscriptionId: selectedSubscriptionId, }); -toast.success("Subscription updated successfully"); + toast.success("Subscription updated successfully"); router.push("/admin/users"); setIsModalOpen(null); // alert("Subscription updated successfully"); @@ -130,9 +125,9 @@ toast.success("Subscription updated successfully"); setUpdating(false); } }; -useEffect(()=>{ - setHasChanged(selectedSubscriptionId !== subscription.id); -},[selectedSubscriptionId]) + useEffect(() => { + setHasChanged(selectedSubscriptionId !== subscription.id); + }, [selectedSubscriptionId]); return (
@@ -147,7 +142,9 @@ useEffect(()=>{ {/* Basic Info */}
-

{loading ? "..." : name || "N/A"}

+

+ {loading ? "..." : name || "N/A"} +

{username || "N/A"}

{contactNumber || "N/A"}

{role || "N/A"}

@@ -158,40 +155,41 @@ useEffect(()=>{ {/* Subscription Plan Selector */}
-
- Subscription Plan: - -
- - - {hasChanged && ( - - )} -
-
-
- +
+ + Subscription Plan: + + +
+ + + {hasChanged && ( + + )} +
+
+
{/* Plan Details */} {!loading && ( diff --git a/app/src/app/admin/components/SubscriptionEdit.tsx b/app/src/app/admin/components/SubscriptionEdit.tsx new file mode 100644 index 0000000..5007a84 --- /dev/null +++ b/app/src/app/admin/components/SubscriptionEdit.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { SubscriptionData } from "../subscriptions/page"; +import { API } from "@/lib/axios"; +import { toast } from "sonner"; + +type Props = { + recording: SubscriptionData; + onClose: () => void; +}; + +const SubscriptionEdit = ({ recording, onClose }: Props) => { + const [editData, setEditData] = useState(recording); + const [editingField, setEditingField] = useState(null); + const [updating, setUpdating] = useState(false); + console.log("recording", recording); + useEffect(() => { + setEditData(recording); + }, [recording]); + + const handleChange = ( + key: keyof SubscriptionData, + value: string | number + ) => { + setEditData((prev) => ({ ...prev, [key]: value })); + }; + + const handleSave = async () => { + setUpdating(true); + try { + const { id, ...data } = editData; + const response = await API.patch( + `/admin/subscriptions/free?id=${id}`, + data + ); + + const result = response.data; + console.log("Updated subscription:", result); + // Close modal + } catch (err: any) { + console.error("Save error:", err); + toast.error(err?.response.data.error || "Failed to update subscription"); + } finally { + onClose(); + setUpdating(false); + } + }; + + const renderField = (label: string, key: keyof SubscriptionData) => ( +
+ +
+ + handleChange( + key, + key === "name" ? e.target.value : parseFloat(e.target.value) + ) + } + /> +
+
+ ); + + return ( +
+

Edit Subscription Details

+ {renderField("File Name", "name")} + {renderField("Recording Count", "recordingCount")} + {renderField("File Size Limit (MB)", "fileSizeLimitMB")} + {renderField("Duration (Days)", "durationDays")} + {renderField("Price", "price")} + +
+ + +
+
+ ); +}; + +export default SubscriptionEdit; diff --git a/app/src/app/admin/recordings/[id]/page.tsx b/app/src/app/admin/recordings/[id]/page.tsx new file mode 100644 index 0000000..e925876 --- /dev/null +++ b/app/src/app/admin/recordings/[id]/page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Trash2 } from "lucide-react"; +import { API } from "@/lib/axios"; + +interface PageProps { + params: { + id: string; + }; +} + +interface Recording { + id: string; + documentName: string; + audioDuration: number; + createdAt: string; + translation: string; + summary: string; + isDefault: boolean; +} + +export default function RecordingsPage({ params }: PageProps) { + const [recordings, setRecordings] = useState([]); + const searchParams = useSearchParams(); + const page = searchParams.get("page") || "1"; + const limit = searchParams.get("limit") || "20"; + + useEffect(() => { + const fetchRecordings = async () => { + try { + const response = await API.get( + `/admin/users/transcriptions/${params.id}?page=${page}&limit=${limit}` + ); + setRecordings(response.data.data); // data array from response + } catch (error) { + console.error("Failed to fetch recordings:", error); + } + }; + + fetchRecordings(); + }, [params.id, page, limit]); + + return ( + + +

All Recordings

+ + + + File Name + Duration (s) + Upload Date + Action + + + + {recordings.map((audioFile) => ( + + + {audioFile.documentName} + + + {audioFile.audioDuration} + + + {new Date(audioFile.createdAt).toLocaleDateString()} + + +
+ {}} size={20} /> +
+
+
+ ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/src/app/admin/subscriptions/page.tsx b/app/src/app/admin/subscriptions/page.tsx index 9a186a6..624949c 100644 --- a/app/src/app/admin/subscriptions/page.tsx +++ b/app/src/app/admin/subscriptions/page.tsx @@ -1,9 +1,7 @@ -// app/admin/subscriptions/page.tsx "use client"; import { useEffect, useState } from "react"; import { API } from "@/lib/axios"; -import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Table, @@ -13,10 +11,24 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { Edit2 } from "lucide-react"; +import { Modal } from "@/components/ui/modal"; +import SubscriptionEdit from "../components/SubscriptionEdit"; + +export type SubscriptionData = { + id:string, + name: string; + recordingCount: number; + price: number; + fileSizeLimitMB: number; + durationDays: number; +}; export default function SubscriptionsPage() { const [subs, setSubs] = useState([]); const [loading, setLoading] = useState(false); + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(null); const fetchSubscriptions = async () => { try { @@ -29,58 +41,68 @@ export default function SubscriptionsPage() { setLoading(false); } }; - - const handleDelete = async (id: string) => { - try { - // await API.delete(`/admin/subscriptions/${id}`); - setSubs((prev) => prev.filter((s) => s.id !== id)); - } catch (err) { - console.error("Delete failed:", err); - } - }; - +useEffect(() => { + if(selectedSubscriptionId) setIsModalOpen(subs.find((s: any) => s.id === selectedSubscriptionId)) +}, [selectedSubscriptionId]) useEffect(() => { fetchSubscriptions(); - }, []); + }, [isModalOpen]); return ( - - -

Subscriptions

- {loading ? ( -

Loading...

- ) : ( - - - - Name - Recording Count - File Size Limit (MB) - Duration (Days) - Actions - - - - {subs.map((s) => ( - - {s.name} - {s.recordingCount} - {s.fileSizeLimitMB} - {s.durationDays} - - - + <> + + +

Subscriptions

+ {loading ? ( +

Loading...

+ ) : ( +
+ + + Name + Recording Count + File Size Limit (MB) + Duration (Days) + Price + Actions - ))} - -
+ + + {subs.map((s) => ( + + {s.name} + {s.recordingCount} + {s.fileSizeLimitMB} + {s.durationDays} + {s.price} + +
setSelectedSubscriptionId(s.id)} + className="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow hover:bg-green-500 cursor-pointer border-green-800 hover:text-white" + > + +
+
+
+ ))} +
+ + )} +
+
+ + setIsModalOpen(null)} + title="Update Subscription" + > + {isModalOpen && ( + setIsModalOpen(null)} + /> )} - - + + ); } diff --git a/app/src/app/admin/users/[id]/recordings/RecordingsPage.tsx b/app/src/app/admin/users/[id]/recordings/RecordingsPage.tsx new file mode 100644 index 0000000..96fde19 --- /dev/null +++ b/app/src/app/admin/users/[id]/recordings/RecordingsPage.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Trash2 } from "lucide-react"; +import { API } from "@/lib/axios"; + +interface Recording { + id: string; + documentName: string; + audioDuration: number; + createdAt: string; + translation: string; + summary: string; + isDefault: boolean; +} + +interface Props { + userId: string; +} + +export default function RecordingsPage({ userId }: Props) { + const [recordings, setRecordings] = useState([]); + const searchParams = useSearchParams(); + const page = searchParams.get("page") || "1"; + const limit = searchParams.get("limit") || "20"; + + useEffect(() => { + const fetchRecordings = async () => { + try { + const response = await API.get( + `/admin/users/transcriptions/${userId}?page=${page}&limit=${limit}` + ); + setRecordings(response.data.data); + } catch (error) { + console.error("Failed to fetch recordings:", error); + } + }; + + fetchRecordings(); + }, [userId, page, limit]); + + return ( + + +

All Recordings

+ + + + File Name + Duration (s) + Upload Date + Action + + + + {recordings.map((audioFile) => ( + + + {audioFile.documentName} + + + {audioFile.audioDuration} + + + {new Date(audioFile.createdAt).toLocaleDateString()} + + +
{ + // Add delete logic here + }} + > + +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/app/src/app/admin/users/[id]/recordings/page.tsx b/app/src/app/admin/users/[id]/recordings/page.tsx new file mode 100644 index 0000000..71ee52c --- /dev/null +++ b/app/src/app/admin/users/[id]/recordings/page.tsx @@ -0,0 +1,11 @@ +import RecordingsPage from "./RecordingsPage"; + +interface PageProps { + params: { + id: string; + }; +} + +export default function RecordingsWrapper({ params }: PageProps) { + return ; +} diff --git a/app/src/app/admin/users/page.tsx b/app/src/app/admin/users/page.tsx index f51f5a2..1e7dc5f 100644 --- a/app/src/app/admin/users/page.tsx +++ b/app/src/app/admin/users/page.tsx @@ -1,8 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { API } from "@/lib/axios"; // Capitalized Axios instance -import { Button } from "@/components/ui/button"; +import { API } from "@/lib/axios"; import { Card, CardContent } from "@/components/ui/card"; import { Table, @@ -12,14 +11,17 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Edit2, Trash2 } from "lucide-react"; +import { Edit2 } from "lucide-react"; import { Modal } from "@/components/ui/modal"; -import EditSubscription from "@/components/EditSubscription"; +import { useRouter } from "next/navigation"; +import EditSubscription from "../components/EditSubscription"; export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(null); + const router = useRouter(); + const fetchUsers = async () => { try { setLoading(true); @@ -32,15 +34,6 @@ export default function UsersPage() { } }; - const handleDelete = async (id: string) => { - try { - // await API.delete(`/admin/users/${id}`); - setUsers((prev) => prev.filter((u) => u.id !== id)); - } catch (err) { - console.error("Delete failed:", err); - } - }; - useEffect(() => { if (!isModalOpen) { fetchUsers(); @@ -68,18 +61,26 @@ export default function UsersPage() { {users.map((u) => ( - + router.push(`/admin/users/${u.id}/recordings`)} + > {u.username} {u.name} {u.role} {u.subscription.name} -
setIsModalOpen(u.id)} className="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow-xl hover:bg-green-500 cursor-pointer border-green-800 hover:text-white"> - -
-
- handleDelete(u.id)} size={20} /> +
{ + e.stopPropagation(); + setIsModalOpen(u.id); + }} + className="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow-xl hover:bg-green-500 cursor-pointer border-green-800 hover:text-white" + > +
+ ))} @@ -93,7 +94,10 @@ export default function UsersPage() { onClose={() => setIsModalOpen(null)} title="Update Subscription" > - + ); diff --git a/app/src/app/api/admin/subscriptions/[id]/route.ts b/app/src/app/api/admin/subscriptions/[id]/route.ts index d44214a..ef7472a 100644 --- a/app/src/app/api/admin/subscriptions/[id]/route.ts +++ b/app/src/app/api/admin/subscriptions/[id]/route.ts @@ -42,18 +42,37 @@ export const PATCH = withAdmin(async function ( ); } - const [updated] = await db - .update(subscriptionTable) - .set(data) - .where(eq(subscriptionTable.id, id)) - .returning(); + try { + const [updated] = await db + .update(subscriptionTable) + .set(data) + .where(eq(subscriptionTable.id, id)) + .returning(); + + if (!updated) { + return NextResponse.json( + { error: "Subscription not found" }, + { status: 404 } + ); + } + + return NextResponse.json(updated); + } catch (error: any) { + // Handle unique constraint error (PostgreSQL code 23505) + if (error.code === "23505") { + if (error.constraint === "subscriptions_name_unique") { + return NextResponse.json( + { error: "Subscription name already exists" }, + { status: 409 } + ); + } + } - if (!updated) { + // Generic DB error fallback + console.error("DB Error:", error); return NextResponse.json( - { error: "Subscription not found" }, - { status: 404 } + { error: "An unexpected error occurred" }, + { status: 500 } ); } - - return NextResponse.json(updated); });