diff --git a/database.types.ts b/database.types.ts deleted file mode 100644 index 8d39734..0000000 Binary files a/database.types.ts and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 92a1894..21c688a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", "@supabase/ssr": "^0.5.2", @@ -1852,6 +1854,270 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", @@ -1981,6 +2247,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", diff --git a/package.json b/package.json index 75bb1d9..3e5e58c 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,50 @@ { - "name": "sipher", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@radix-ui/react-accordion": "^1.2.1", - "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-avatar": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-scroll-area": "^1.2.1", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-toast": "^1.2.2", - "@radix-ui/react-tooltip": "^1.1.4", - "@supabase/ssr": "^0.5.2", - "@supabase/supabase-js": "^2.47.3", - "argon2": "^0.41.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "framer-motion": "^11.13.5", - "lucide-react": "^0.468.0", - "next": "15.0.4", - "next-themes": "^0.4.4", - "random-words": "^2.0.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "9.16.0", - "eslint-config-next": "15.1.0", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } + "name": "sipher", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.2", + "@radix-ui/react-tooltip": "^1.1.4", + "@supabase/ssr": "^0.5.2", + "@supabase/supabase-js": "^2.47.3", + "argon2": "^0.41.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^11.13.5", + "lucide-react": "^0.468.0", + "next": "15.0.4", + "next-themes": "^0.4.4", + "random-words": "^2.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "9.16.0", + "eslint-config-next": "15.1.0", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } } diff --git a/src/app/[id]/page.tsx b/src/app/[id]/page.tsx new file mode 100644 index 0000000..9e95abf --- /dev/null +++ b/src/app/[id]/page.tsx @@ -0,0 +1,396 @@ +"use client" +import {useEffect, useState} from 'react'; +import {AnimatePresence, motion} from 'framer-motion'; +import {useTheme} from 'next-themes'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {ScrollArea} from '@/components/ui/scroll-area'; +import {Avatar, AvatarFallback} from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip'; +import { + Archive, + Ban, + Clock, + Download, + Info, + Key, + KeyRound, + MoreVertical, + Send, + ShieldCheck, + UserCheck, + UserX +} from 'lucide-react'; +import {usePathname} from "next/navigation"; +import {useUser} from "@/contexts/user"; +import {useToast} from "@/hooks/use-toast"; +import {useSharedState} from "@/hooks/shared-states"; +import {createBrowserClient} from '@/lib/supabase/browser' +import {CryptoManager} from "@/lib/crypto/keys"; +import {REALTIME_SUBSCRIBE_STATES} from "@supabase/realtime-js"; + +export default function ChatPage() { + const {theme} = useTheme(); + const {toast} = useToast(); + const supabase = createBrowserClient(); + + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showKeyDialog, setShowKeyDialog] = useState(false); + const [showUserDialog, setShowUserDialog] = useState(false); + const [isEncrypted, setIsEncrypted] = useState(true); + + const [realtimeSubscribed, setRealtimeSubscribed] = useState() + + const [isLoaded, setIsLoaded] = useState(false); + + const [user, setUser] = useState(null); + const pathName = usePathname(); + const threadId = pathName.replace("/", ""); + + const { + user: currentUser, + getUser + } = useUser() + + const {threads} = useSharedState(); + + useEffect(() => { + const channel = supabase + .channel(`messages:${threadId}`) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'messages', + }, + async (payload) => { + if (payload.eventType === "INSERT") { + const messageData = payload.new as SiPher.RealtimeMessageData; + const isSender = messageData.sender_uuid === currentUser.uuid; + + const decryptedMsg = await CryptoManager.decryptMessage(messageData.sender_content) + console.log(`Hello there`) + setMessages((prevState) => { + return [ + ...prevState, + { + id: messageData.id, + content: decryptedMsg, + sender_uuid: messageData.sender_uuid, + created_at: messageData.created_at, + isSender + } + ] + }) + } + } + ) + .subscribe((status) => { + setRealtimeSubscribed(status) + console.log('Realtime subscription status:', status) + }) + + return () => { + supabase.removeChannel(channel) + } + }, [threadId]) + + useEffect(() => { + const getUserDataAndChat = async () => { + const {thread: getThread} = await (await fetch(`/api/user/get/thread?threadId=${threadId}`)).json() as { + thread: SiPher.Thread + }; + + const otherUser = getThread.participant_suuids.filter((ids) => ids !== currentUser.suuid); + const user = await getUser(`Being called from chat page (${threadId}`, otherUser[0], "suuid", true) + + if (!(user.user[0].suuid && user.user[0].username)) { + toast({ + title: "Error", + description: "Could not verify the existence of this user", + variant: "destructive", + duration: 5000 + }); + } + + setUser(user.user[0]) + + const decryptedMsg = await CryptoManager.decryptThreadMessages(getThread["messages"], currentUser.uuid) + setMessages(decryptedMsg) + } + + if (threads.length > 0) { + setIsLoaded(true) + getUserDataAndChat() + } + + return () => { + setUser(null) + setMessages([]) + setIsLoaded(false) + } + }, [setUser, setMessages, setIsLoaded, threads]) + + if (!isLoaded || !user || realtimeSubscribed !== "SUBSCRIBED") { + return ( + <> + a + + ) + } + + // Mock functions - replace with actual implementations + const checkUserValidity = async () => { + // Implementation for checking user validity + setShowUserDialog(true); + }; + + const checkCurrentKey = async () => { + // Implementation for checking current key + setShowKeyDialog(true); + }; + + const deleteUser = async () => { + // Implementation for deleting user + setShowDeleteDialog(true); + }; + + const sendMessage = async (content: string) => { + if (!content.trim()) return; + setInputMessage(''); + + await CryptoManager.prepareAndSendMessage( + content, + currentUser.public_key, + user.public_key, + threadId + ) + + }; + + return ( +
+ {/* Chat Header */} +
+
+ + + { + user.username.charAt(0).toLocaleUpperCase() + } + + +
+

+ { + user.username.charAt(0).toLocaleUpperCase() + user.username.slice(1) + } +

+
+
+ +
+ + + + + + + {isEncrypted ? 'Encrypted Chat' : 'Encryption Issue'} + + + + + + + + + + Chat Options + + + + + Check User + + + + + Check Current Key + + + + + + + Message History + + + + + Archive Chat + + + + + Export Chat + + + + + + + Delete User + + + +
+
+ + {/* Chat Messages */} + +
+ + {messages.map((message) => ( + +
+

{message.content}

+
+ + {new Date(message.created_at).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })} + +
+
+
+ ))} +
+
+
+ + {/* Input Area */} +
+
+ setInputMessage(e.target.value)} + placeholder="Type a message..." + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(inputMessage); + } + }} + /> + +
+
+ + {/* Dialogs */} + + + + Delete User + + Are you sure you want to delete this user? This will remove them from your contacts + and delete all messages. This action cannot be undone. + + + + Cancel + Delete + + + + + + + + Encryption Status + +
+ + Local private key is valid and active +
+
+ + Remote public key is verified +
+
+ + End-to-end encryption is active +
+
+
+ + Close + +
+
+ + + + + User Verification + +
+ + User is verified and active +
+
+ + Last active: 2 minutes ago +
+
+ + Secure connection established +
+
+
+ + Close + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/api/auth/get_user/route.ts b/src/app/api/auth/get_user/route.ts index 7633791..e4e5d7a 100644 --- a/src/app/api/auth/get_user/route.ts +++ b/src/app/api/auth/get_user/route.ts @@ -4,17 +4,32 @@ import getUserByUUID from "@/lib/api/helpers/getUserByUUID"; // Helper function to get user data by UUID - export async function GET(request: Request) { try { const supabase = await createClient(); const {searchParams} = new URL(request.url); const uuid = searchParams.get('uuid'); + const suuid = searchParams.get('suuid'); + const getDetails = searchParams.get("detailed") if (uuid) { // Get specific user by UUID const userData = await getUserByUUID(supabase, uuid); return NextResponse.json({user: userData}); + } else if (suuid) { + const {data, error} = await supabase.rpc('search_users', { + search_term: suuid + }); + + if (error) { + return NextResponse.json({error: error}, {status: 500}); + } + + if (getDetails) { + return NextResponse.json({user: data}) + } + + return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200}); } else { // Get current authenticated user const {data: {user}, error: authError} = await supabase.auth.getUser(); @@ -28,6 +43,13 @@ export async function GET(request: Request) { return NextResponse.json({user: userData}); } } catch (error) { + if (typeof error === "object") { + return NextResponse.json( + {error: `Failed to fetch user: ${JSON.stringify(error)}`}, + {status: 500} + ); + } + return NextResponse.json( {error: `Failed to fetch user: ${error}`}, {status: 500} diff --git a/src/app/api/user/create/thread/route.ts b/src/app/api/user/create/thread/route.ts index 14f5acd..af5d214 100644 --- a/src/app/api/user/create/thread/route.ts +++ b/src/app/api/user/create/thread/route.ts @@ -1,7 +1,6 @@ import {NextResponse} from "next/server"; import {createClient} from "@/lib/supabase/server"; import getUserByUUID from "@/lib/api/helpers/getUserByUUID"; -import updateUserRequests from "@/lib/api/helpers/updateUserRequests"; export async function POST(req: Request) { const {participant} = await req.json(); @@ -28,7 +27,7 @@ export async function POST(req: Request) { /** First we need to check if the requested participant is in the user's request array */ const dbUser = await getUserByUUID(supabase, user.id) - + if (!dbUser) { return NextResponse.json( {error: "User not found"}, @@ -49,7 +48,7 @@ export async function POST(req: Request) { const {error} = await supabase.rpc('create_private_thread', { participant_suuid: participant }); - + if (error) { return NextResponse.json({error}, {status: 500}); } diff --git a/src/app/api/user/get/thread/route.ts b/src/app/api/user/get/thread/route.ts new file mode 100644 index 0000000..4f7daa0 --- /dev/null +++ b/src/app/api/user/get/thread/route.ts @@ -0,0 +1,53 @@ +import {createClient} from "@/lib/supabase/server"; +import {NextResponse} from "next/server"; + +export async function GET(request: Request) { + try { + const {searchParams} = new URL(request.url); + const threadId = searchParams.get('threadId'); + + if (!threadId) { + return NextResponse.json({ + error: "No thread id provided" + }, {status: 400}) + } + + const supabase = await createClient(); + + const {data: {user}, error: userError} = await supabase.auth.getUser() + + if (userError) { + NextResponse.json( + {error: userError}, + {status: userError?.status} + ) + } else if (!user) { + NextResponse.json( + {error: "User not found"}, + {status: 401} + ) + } + + const {data, error} = await supabase.rpc( + "get_thread", + { + thread_uuid: threadId, + user_id: user!.id + } + ) + + if (error) { + return NextResponse.json({error}, {status: 400}) + } + + return NextResponse.json({thread: data[0]}, {status: 200}); + + } catch (e: any) { + console.log(e) + if (typeof e === "object") { + return NextResponse.json({error: JSON.stringify(e)}, {status: 500}) + } + + return NextResponse.json({error: e}, {status: 500}) + } +} \ No newline at end of file diff --git a/src/app/api/user/search/user/route.ts b/src/app/api/user/search/user/route.ts index 4c9e1b6..23f5e7e 100644 --- a/src/app/api/user/search/user/route.ts +++ b/src/app/api/user/search/user/route.ts @@ -6,7 +6,7 @@ export async function GET(request: Request) { const supabase = await createClient(); const {searchParams} = new URL(request.url); const uuid = searchParams.get('uuid'); - console.log('Searching for UUID:', uuid); + const getDetails = searchParams.get("detailed") if (!uuid) { return NextResponse.json({error: "Missing UUID from request"}, {status: 400}) @@ -36,6 +36,10 @@ export async function GET(request: Request) { return NextResponse.json({error: error}, {status: 500}); } + if (getDetails) { + return NextResponse.json({user: data}) + } + return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200}); } catch (error) { diff --git a/src/app/api/user/send/message/route.ts b/src/app/api/user/send/message/route.ts index e69de29..2d16b2e 100644 --- a/src/app/api/user/send/message/route.ts +++ b/src/app/api/user/send/message/route.ts @@ -0,0 +1,31 @@ +import {createClient} from "@/lib/supabase/server"; +import {NextResponse} from "next/server"; + +export async function POST(request: Request) { + try { + const {threadId, senderContent, recipientContent} = await request.json(); + const supabase = await createClient(); + + const {data, error} = await supabase.rpc('send_message', { + thread_uuid: threadId, + sender_content: senderContent, + recipient_content: recipientContent + }); + + if (error) throw error; + + return NextResponse.json({messageId: data}); + } catch (error: any) { + if (typeof error === "object") { + return NextResponse.json( + {error}, + {status: 500} + ); + } + + return NextResponse.json( + {error: 'Failed to send message', details: error.message}, + {status: 500} + ); + } +} \ No newline at end of file diff --git a/src/app/api/user/send/request/route.ts b/src/app/api/user/send/request/route.ts index 525997e..f291698 100644 --- a/src/app/api/user/send/request/route.ts +++ b/src/app/api/user/send/request/route.ts @@ -1,6 +1,5 @@ import {createClient} from "@/lib/supabase/server"; import {NextResponse} from "next/server"; -import {SupabaseClient} from "@supabase/supabase-js"; import getUserByUUID from "@/lib/api/helpers/getUserByUUID"; import updateUserRequests from "@/lib/api/helpers/updateUserRequests"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 82072f7..fbeac88 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,7 +9,6 @@ import {SharedStateProvider} from "@/hooks/shared-states"; import ThemeProvider from "@/components/ui/theme-provider"; import {headers} from "next/headers"; import {Toaster} from "@/components/ui/toaster"; -import {RealtimeRequests} from "@/components/main/realtime/request"; const publicSans = Public_Sans({ subsets: ['latin'], diff --git a/src/app/page.tsx b/src/app/page.tsx index 2457b1a..54f9e6e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -26,6 +26,7 @@ export default function SiPher() { /** CryptoManager Alert */ const [privateKeyPresent, setPrivateKeyPresent] = useState(true); + const [backupPanel, setBackupPanel] = useState(false); /** Consent Form states */ const [showConsentForm, setShowConsentForm] = useState(false); @@ -164,8 +165,8 @@ export default function SiPher() { Private Key Missing - This app could not retrieve your private key, which means it's either lost, never stored or corrupted. Want to try again or insert it from a backup? - You can also regenerate it if you do not have it backed up, but this would mean that you'll loose access to all old messages. + This app could not retrieve your private key, which means it's either lost, never stored or corrupted. Want to try again or insert it from a backup? + You can also regenerate it if you do not have it backed up, but this would mean that you'll loose access to all old messages. diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..9379e13 --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,287 @@ +"use client" +import {motion} from "framer-motion"; +import {useTheme} from "next-themes"; +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; +import {Button} from "@/components/ui/button"; +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; +import {Input} from "@/components/ui/input"; +import {Label} from "@/components/ui/label"; +import {Switch} from "@/components/ui/switch"; +import {Separator} from "@/components/ui/separator"; +import {useUser} from "@/contexts/user"; +import {useState} from "react"; +import {AlertTriangle, Copy, Download, Eye, EyeOff, Key, Lock, Save, User} from "lucide-react"; +import {CryptoManager} from "@/lib/crypto/keys"; +import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"; + +export default function SettingsPage() { + const {theme, setTheme} = useTheme(); + const {user} = useUser(); + const [loading, setLoading] = useState(false); + const [privateKeyVisible, setPrivateKeyVisible] = useState(false); + const [privateKeyData, setPrivateKeyData] = useState<{ text: string; file: File } | null>(null); + const [backupError, setBackupError] = useState(""); + + const containerVariants = { + hidden: {opacity: 0, y: 20}, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + staggerChildren: 0.1 + } + } + }; + + const itemVariants = { + hidden: {opacity: 0, y: 20}, + visible: {opacity: 1, y: 0} + }; + + return ( + +
+
+

Settings

+

+ Manage your account settings and preferences +

+
+
+ + + + + + Profile + + + + Privacy + + + + + + + + Profile Information + + Update your profile information and settings + + + +
+ + +
+
+ +
+ + +
+
+
+
+
+ + + + + Privacy Settings + + Manage your privacy and security preferences + + + +
+
+ +

+ End-to-end encryption is always enabled +

+
+ +
+ +
+
+
+ +

+ View and download your private key for backup +

+
+
+ + +
+
+ + {backupError && ( + + + Error + {backupError} + + )} + + {privateKeyData && ( + + +
+ Private Key +
+ + +
+
+
+ +
+
+                            {privateKeyData.text}
+                          
+
+
+
+ )} +
+ +
+
+ +

+ Receive message requests from other users +

+
+ +
+
+
+ + + + Private Key Management + + Your private key is stored securely in your browser. + Make sure to back it up to avoid losing access to your messages. + + +
+
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/main/realtime/threads.tsx b/src/components/main/realtime/threads.tsx index 8665dcf..5834b5f 100644 --- a/src/components/main/realtime/threads.tsx +++ b/src/components/main/realtime/threads.tsx @@ -1,15 +1,15 @@ // hooks/useRealtime.ts -import {useEffect} from 'react' +import {Dispatch, SetStateAction, useEffect} from 'react' import {createBrowserClient} from '@/lib/supabase/browser' import {useUser} from '@/contexts/user' import {useToast} from '@/hooks/use-toast' interface UseRealtimeProps { - setThreads: React.Dispatch>; - threads: SiPher.Messages[] + setThreads: Dispatch>; + threads: SiPher.Thread[] } -export function useRealtime({setThreads}: UseRealtimeProps) { +export function useRealtime({setThreads, threads}: UseRealtimeProps) { const supabase = createBrowserClient(); const {user, updateUser} = useUser(); const {toast} = useToast(); @@ -38,6 +38,7 @@ export function useRealtime({setThreads}: UseRealtimeProps) { table: 'users', filter: `uuid=eq.${user.uuid}`, }, async (payload) => { + console.log(payload) if (payload.eventType === "UPDATE") { // This will also handle updates for the threads, but only for the user that accepted the request. // Why? Because the function that creates the thread will also update the current user request field and remove @@ -48,6 +49,13 @@ export function useRealtime({setThreads}: UseRealtimeProps) { requests: payload.new.requests }) } + } else if (payload.eventType === "DELETE") { + console.log(`Payload from delete: \n${payload}`) + updateUser({ + ...user, + //@ts-expect-error + requests: payload.new + }) } }).subscribe() @@ -56,11 +64,18 @@ export function useRealtime({setThreads}: UseRealtimeProps) { .on("postgres_changes", { event: "*", schema: 'public', - // Using on this one because it's easier table: "thread_participants", - filter: `user_uuid=${user.uuid}`, + filter: `user_uuid=eq.${user.uuid}` }, async (payload) => { - console.log(payload) + if (payload.new !== payload.old) { + await fetchAndUpdateThreads(); + } }).subscribe() + + return () => { + threadUpdate.unsubscribe() + userUpdate.unsubscribe() + } + }, [user?.uuid]); } \ No newline at end of file diff --git a/src/components/main/sidebar/rightsidebar.tsx b/src/components/main/sidebar/rightsidebar.tsx index dee01d3..e4da858 100644 --- a/src/components/main/sidebar/rightsidebar.tsx +++ b/src/components/main/sidebar/rightsidebar.tsx @@ -10,6 +10,8 @@ import {GearIcon} from "@radix-ui/react-icons"; import Link from "next/link"; import {useRealtime} from "@/components/main/realtime/threads"; import {useUser} from "@/contexts/user"; +import {usePathname} from "next/navigation"; +import {useSharedState} from "@/hooks/shared-states"; interface RightSidebarContentProps { isDarkMode: boolean; @@ -20,30 +22,22 @@ export default function RightSidebarContent( isDarkMode, }: RightSidebarContentProps) { - const [selectedThreads, setSelectedThreads] = useState(""); - const [threadMenu, setThreadMenu] = useState([]); - const [pendingRequest, setPendingRequest] = useState(0); - - - const [threads, setThreads] = useState([]); - useRealtime( - {setThreads} - ); const [copied, setCopied] = useState(false); + const {threads, setThreads} = useSharedState(); + useRealtime({setThreads, threads}); + const {user} = useUser(); const {username, suuid, requests = []} = user; + const pathname = usePathname(); - - // No need for separate requests state since it's in user object const pendingRequests = requests?.length ?? 0; - // Move fetch to separate function const fetchThreads = useCallback(async () => { try { const req = await fetch("/api/user/get/threads") if (req.ok) { - const {threads} = await req.json() as { threads: SiPher.Messages[] | [] } + const {threads} = await req.json() as { threads: SiPher.Thread[] | [] } setThreads(threads) } else { setThreads([]) @@ -65,7 +59,6 @@ export default function RightSidebarContent( body: JSON.stringify({participant: request}), }); if (response.ok) { - // Optionally refresh threads after successful creation fetchThreads(); } } catch (error) { @@ -73,7 +66,6 @@ export default function RightSidebarContent( } } - return ( <>
@@ -149,17 +141,19 @@ export default function RightSidebarContent( {threads && threads.length > 0 ? ( threads.map((thread, index) => { + // Gets the user's username instead of the SUUID to use as a recognizable user. const otherUser = thread.participants.filter((user) => user !== username)[0]; - console.log(thread) return (
  • -
    +
    +
  • ) @@ -175,7 +169,7 @@ export default function RightSidebarContent(