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
diff --git a/app/src/app/admin/components/EditSubscription.tsx b/app/src/app/admin/components/EditSubscription.tsx
new file mode 100644
index 0000000..d38620a
--- /dev/null
+++ b/app/src/app/admin/components/EditSubscription.tsx
@@ -0,0 +1,220 @@
+"use client";
+import { useEffect, useState } from "react";
+import { API } from "@/lib/axios";
+import { toast } from "sonner";
+import { useRouter } from "next/navigation";
+import { Check, Loader2 } 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);
+ };
+
+ 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;
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/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/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
new file mode 100644
index 0000000..624949c
--- /dev/null
+++ b/app/src/app/admin/subscriptions/page.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { API } from "@/lib/axios";
+import { Card, CardContent } from "@/components/ui/card";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ 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 {
+ setLoading(true);
+ const { data } = await API.get("/admin/subscriptions");
+ setSubs(data.subscriptions);
+ } catch (err) {
+ console.error("Failed to fetch subscriptions:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+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)
+ 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
new file mode 100644
index 0000000..1e7dc5f
--- /dev/null
+++ b/app/src/app/admin/users/page.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { API } from "@/lib/axios";
+import { Card, CardContent } from "@/components/ui/card";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Edit2 } from "lucide-react";
+import { Modal } from "@/components/ui/modal";
+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);
+ 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);
+ }
+ };
+
+ useEffect(() => {
+ if (!isModalOpen) {
+ fetchUsers();
+ }
+ fetchUsers();
+ }, [isModalOpen]);
+
+ return (
+ <>
+
+
+ Users
+ {loading ? (
+ Loading...
+ ) : (
+
+
+
+ Username
+ Name
+ Role
+ Subscription
+ Actions
+
+
+
+ {users.map((u) => (
+ router.push(`/admin/users/${u.id}/recordings`)}
+ >
+ {u.username}
+ {u.name}
+ {u.role}
+ {u.subscription.name}
+
+ {
+ 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"
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ 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
new file mode 100644
index 0000000..ef7472a
--- /dev/null
+++ b/app/src/app/api/admin/subscriptions/[id]/route.ts
@@ -0,0 +1,78 @@
+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 }
+ );
+ }
+
+ 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 }
+ );
+ }
+ }
+
+ // Generic DB error fallback
+ console.error("DB Error:", error);
+ return NextResponse.json(
+ { error: "An unexpected error occurred" },
+ { status: 500 }
+ );
+ }
+});
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..2148028
--- /dev/null
+++ b/app/src/app/api/admin/subscriptions/route.ts
@@ -0,0 +1,66 @@
+// src/app/api/admin/subscriptions/route.ts
+
+import { db } from "@/db";
+import { subscriptionTable } from "@/db/schema";
+import { withAdmin } from "@/lib/withAdmin";
+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 }
+ );
+ }
+});
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 });
+});
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),
+ });
+});
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..edc6890
--- /dev/null
+++ b/app/src/app/api/admin/users/[id]/route.ts
@@ -0,0 +1,81 @@
+import { db } from "@/db";
+import { subscriptionTable, transcriptions, userTable } from "@/db/schema";
+import { withAdmin } from "@/lib/withAdmin";
+import { eq, sql } 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;
+
+ // Fetch user with subscription info
+ 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));
+
+ if (!user) {
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
+ }
+
+ // 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,
+ { 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);
+});
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..e5cfbf9
--- /dev/null
+++ b/app/src/app/api/admin/users/route.ts
@@ -0,0 +1,58 @@
+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,
+ subscription: {
+ name: subscriptionTable.name,
+ recordingCount: subscriptionTable.recordingCount,
+ fileSizeLimitMB: subscriptionTable.fileSizeLimitMB,
+ durationDays: subscriptionTable.durationDays,
+ },
+ })
+ .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/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),
+ });
+});
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 });
+});
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..d26c164
--- /dev/null
+++ b/app/src/app/api/admin/users/update-subscription/route.ts
@@ -0,0 +1,28 @@
+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 });
+ }
+
+ const [updatedUser] = await db
+ .update(userTable)
+ .set({ subscriptionId })
+ .where(eq(userTable.id, userId))
+ .returning();
+
+ if (!updatedUser) {
+ return NextResponse.json(
+ { error: "User not found or not updated" },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({ user: updatedUser });
+});
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 });
+ }
}
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;
}
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];
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(),
});
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
},
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",
+ },
+});
diff --git a/app/src/lib/withAdmin.ts b/app/src/lib/withAdmin.ts
new file mode 100644
index 0000000..4656912
--- /dev/null
+++ b/app/src/lib/withAdmin.ts
@@ -0,0 +1,17 @@
+import { validateRequest } from "@/auth";
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+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(req, context); // âś… Pass context (params) to your handler
+ };
+}