From 2afc18ee99ce8f7b425214e8d7ee9cfbd33d2bb8 Mon Sep 17 00:00:00 2001 From: Nixyan Date: Wed, 3 Dec 2025 09:41:21 -0300 Subject: [PATCH] Restarted the project. Old one is at the branch @v0. --- .eslintrc.json | 3 - .gitignore | 1 + .idea/.gitignore | 8 - .idea/discord.xml | 14 - .idea/material_theme_project_new.xml | 13 - .idea/modules.xml | 8 - .idea/sipher.iml | 12 - .idea/vcs.xml | 6 - .vscode/settings.json | 18 + .vscode/tailwind.json | 105 + README.md | 249 - bun.lock | 670 ++ components.json | 7 +- convex/_generated/api.d.ts | 1013 +++ convex/_generated/api.js | 23 + convex/_generated/dataModel.d.ts | 58 + convex/_generated/server.d.ts | 143 + convex/_generated/server.js | 93 + convex/auth.config.ts | 8 + convex/auth.ts | 60 + convex/betterAuth/_generated/api.ts | 52 + convex/betterAuth/_generated/component.ts | 992 +++ convex/betterAuth/_generated/dataModel.ts | 60 + convex/betterAuth/_generated/server.ts | 161 + convex/betterAuth/adapter.ts | 13 + convex/betterAuth/auth.ts | 5 + convex/betterAuth/convex.config.ts | 5 + convex/betterAuth/schema.ts | 73 + convex/convex.config.ts | 7 + convex/http.ts | 8 + next.config.ts | 1 + package-lock.json | 7310 ----------------- package.json | 86 +- postcss.config.mjs | 3 +- public/assets/logo/logo-dark.svg | 11 + public/assets/logo/logo-white.svg | 38 + public/logos/logo-light.png | Bin 43282 -> 0 bytes public/logos/logo.png | Bin 43429 -> 0 bytes public/logos/united-chat.png | Bin 6188 -> 0 bytes src/app/[id]/page.tsx | 411 - src/app/[id]/skeleton.tsx | 50 - src/app/about/page.tsx | 234 - src/app/api/auth/[...all]/route.ts | 3 + src/app/api/auth/get_user/route.ts | 58 - src/app/api/auth/login/route.ts | 49 - src/app/api/auth/register/route.ts | 61 - src/app/api/user/create/thread/route.ts | 57 - src/app/api/user/get/thread/route.ts | 53 - src/app/api/user/get/threads/route.ts | 37 - src/app/api/user/search/user/route.ts | 48 - src/app/api/user/send/message/route.ts | 31 - src/app/api/user/send/request/route.ts | 49 - src/app/api/user/send/update/key/route.ts | 23 - src/app/auth/components/sign-in-form.tsx | 87 + src/app/auth/components/sign-up-form.tsx | 234 + src/app/auth/login/login.ts | 36 - src/app/auth/login/page.tsx | 225 - src/app/auth/login/register.ts | 46 - src/app/auth/page.tsx | 158 + src/app/globals.css | 259 +- src/app/layout.tsx | 114 +- src/app/page.tsx | 328 +- src/app/settings/page.tsx | 287 - src/components/home/index.tsx | 72 + src/components/main/realtime/index.tsx | 77 - src/components/main/sidebar/mobile.tsx | 59 - src/components/main/sidebar/rightsidebar.tsx | 196 - src/components/main/sidebar/sidebar.tsx | 92 - src/components/mode-toggle.tsx | 20 + src/components/socket-test.tsx | 93 + src/components/theme-provider.tsx | 12 + src/components/ui/accordion.tsx | 57 - src/components/ui/alert-dialog.tsx | 141 - src/components/ui/alert.tsx | 59 - src/components/ui/avatar.tsx | 50 - src/components/ui/button-group.tsx | 83 + src/components/ui/button.tsx | 70 +- src/components/ui/calendar.tsx | 216 + src/components/ui/captcha.tsx | 24 + src/components/ui/card.tsx | 150 +- src/components/ui/checkbox.tsx | 32 + src/components/ui/command.tsx | 184 + src/components/ui/context-menu.tsx | 252 + src/components/ui/dialog.tsx | 143 + src/components/ui/dropdown-menu.tsx | 201 - src/components/ui/empty.tsx | 104 + src/components/ui/field.tsx | 248 + src/components/ui/input.tsx | 35 +- src/components/ui/item.tsx | 193 + src/components/ui/kbd.tsx | 28 + src/components/ui/label.tsx | 36 +- src/components/ui/logo-icon.tsx | 17 + src/components/ui/menubar.tsx | 276 + src/components/ui/popover.tsx | 48 + src/components/ui/progress.tsx | 31 + src/components/ui/scroll-area.tsx | 90 +- src/components/ui/separator.tsx | 45 +- src/components/ui/sheet.tsx | 139 + src/components/ui/sidebar.tsx | 726 ++ src/components/ui/skeleton.tsx | 22 +- src/components/ui/sonner.tsx | 40 + src/components/ui/spinner.tsx | 16 + src/components/ui/switch.tsx | 29 - src/components/ui/tabs.tsx | 55 - src/components/ui/theme-provider.tsx | 19 - src/components/ui/toast.tsx | 129 - src/components/ui/toaster.tsx | 28 - src/components/ui/tooltip.tsx | 73 +- src/contexts/user.tsx | 104 - src/hooks/shared-states.tsx | 80 - src/hooks/use-mobile.ts | 19 + src/hooks/use-toast.ts | 191 - src/lib/api/helpers/getUserByUUID.ts | 12 - src/lib/api/helpers/updateUserRequests.ts | 23 - src/lib/auth/client.ts | 10 + src/lib/auth/index.ts | 42 - src/lib/crypto/helpers/updateKey.ts | 27 - src/lib/crypto/keys.ts | 406 - src/lib/db/index.ts | 0 src/lib/providers/Convex.tsx | 16 + src/lib/supabase/browser.tsx | 10 - src/lib/supabase/server.tsx | 39 - src/lib/utils.ts | 6 +- src/middleware.ts | 68 - src/server.ts | 20 + src/types/globals.d.ts | 104 + src/types/user.d.ts | 47 - supabase/main.py | 116 - .../sql_snippets/Add public key to users.sql | 1 - ...time and Replica Identity for Messages.sql | 1 - ...et Replica Identity for Messages Table.sql | 1 - .../Create Private Thread Function.sql | 1 - .../Create Private Thread Function_1.sql | 1 - ...plica Identity for Thread Participants.sql | 1 - ... Full Replica Identity for Users Table.sql | 1 - .../Enable Replication for Messages Table.sql | 1 - supabase/sql_snippets/Get Thread Details.sql | 1 - supabase/sql_snippets/Get User Threads.sql | 1 - ...w Level Security Policies for Messages.sql | 1 - ...el Security Policies for Messaging App.sql | 1 - .../sql_snippets/Send Message Function.sql | 1 - .../Update User Requests Function.sql | 1 - ...User Access Policy for Search Function.sql | 1 - .../User Management Functions.sql | 1 - .../sql_snippets/User Management Table.sql | 1 - .../sql_snippets/User Registration Policy.sql | 1 - .../User Requests and Messages Management.sql | 1 - .../sql_snippets/User and Message Indexes.sql | 1 - supabase/sql_snippets/Users Table.sql | 1 - tailwind.config.ts | 84 - tsconfig.json | 70 +- 151 files changed, 7922 insertions(+), 12578 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .idea/.gitignore delete mode 100644 .idea/discord.xml delete mode 100644 .idea/material_theme_project_new.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/sipher.iml delete mode 100644 .idea/vcs.xml create mode 100644 .vscode/settings.json create mode 100644 .vscode/tailwind.json delete mode 100644 README.md create mode 100644 bun.lock create mode 100644 convex/_generated/api.d.ts create mode 100644 convex/_generated/api.js create mode 100644 convex/_generated/dataModel.d.ts create mode 100644 convex/_generated/server.d.ts create mode 100644 convex/_generated/server.js create mode 100644 convex/auth.config.ts create mode 100644 convex/auth.ts create mode 100644 convex/betterAuth/_generated/api.ts create mode 100644 convex/betterAuth/_generated/component.ts create mode 100644 convex/betterAuth/_generated/dataModel.ts create mode 100644 convex/betterAuth/_generated/server.ts create mode 100644 convex/betterAuth/adapter.ts create mode 100644 convex/betterAuth/auth.ts create mode 100644 convex/betterAuth/convex.config.ts create mode 100644 convex/betterAuth/schema.ts create mode 100644 convex/convex.config.ts create mode 100644 convex/http.ts delete mode 100644 package-lock.json create mode 100644 public/assets/logo/logo-dark.svg create mode 100644 public/assets/logo/logo-white.svg delete mode 100644 public/logos/logo-light.png delete mode 100644 public/logos/logo.png delete mode 100644 public/logos/united-chat.png delete mode 100644 src/app/[id]/page.tsx delete mode 100644 src/app/[id]/skeleton.tsx delete mode 100644 src/app/about/page.tsx create mode 100644 src/app/api/auth/[...all]/route.ts delete mode 100644 src/app/api/auth/get_user/route.ts delete mode 100644 src/app/api/auth/login/route.ts delete mode 100644 src/app/api/auth/register/route.ts delete mode 100644 src/app/api/user/create/thread/route.ts delete mode 100644 src/app/api/user/get/thread/route.ts delete mode 100644 src/app/api/user/get/threads/route.ts delete mode 100644 src/app/api/user/search/user/route.ts delete mode 100644 src/app/api/user/send/message/route.ts delete mode 100644 src/app/api/user/send/request/route.ts delete mode 100644 src/app/api/user/send/update/key/route.ts create mode 100644 src/app/auth/components/sign-in-form.tsx create mode 100644 src/app/auth/components/sign-up-form.tsx delete mode 100644 src/app/auth/login/login.ts delete mode 100644 src/app/auth/login/page.tsx delete mode 100644 src/app/auth/login/register.ts create mode 100644 src/app/auth/page.tsx delete mode 100644 src/app/settings/page.tsx create mode 100644 src/components/home/index.tsx delete mode 100644 src/components/main/realtime/index.tsx delete mode 100644 src/components/main/sidebar/mobile.tsx delete mode 100644 src/components/main/sidebar/rightsidebar.tsx delete mode 100644 src/components/main/sidebar/sidebar.tsx create mode 100644 src/components/mode-toggle.tsx create mode 100644 src/components/socket-test.tsx create mode 100644 src/components/theme-provider.tsx delete mode 100644 src/components/ui/accordion.tsx delete mode 100644 src/components/ui/alert-dialog.tsx delete mode 100644 src/components/ui/alert.tsx delete mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/button-group.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/captcha.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/context-menu.tsx create mode 100644 src/components/ui/dialog.tsx delete mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/empty.tsx create mode 100644 src/components/ui/field.tsx create mode 100644 src/components/ui/item.tsx create mode 100644 src/components/ui/kbd.tsx create mode 100644 src/components/ui/logo-icon.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/spinner.tsx delete mode 100644 src/components/ui/switch.tsx delete mode 100644 src/components/ui/tabs.tsx delete mode 100644 src/components/ui/theme-provider.tsx delete mode 100644 src/components/ui/toast.tsx delete mode 100644 src/components/ui/toaster.tsx delete mode 100644 src/contexts/user.tsx delete mode 100644 src/hooks/shared-states.tsx create mode 100644 src/hooks/use-mobile.ts delete mode 100644 src/hooks/use-toast.ts delete mode 100644 src/lib/api/helpers/getUserByUUID.ts delete mode 100644 src/lib/api/helpers/updateUserRequests.ts create mode 100644 src/lib/auth/client.ts delete mode 100644 src/lib/auth/index.ts delete mode 100644 src/lib/crypto/helpers/updateKey.ts delete mode 100644 src/lib/crypto/keys.ts create mode 100644 src/lib/db/index.ts create mode 100644 src/lib/providers/Convex.tsx delete mode 100644 src/lib/supabase/browser.tsx delete mode 100644 src/lib/supabase/server.tsx delete mode 100644 src/middleware.ts create mode 100644 src/server.ts create mode 100644 src/types/globals.d.ts delete mode 100644 src/types/user.d.ts delete mode 100644 supabase/main.py delete mode 100644 supabase/sql_snippets/Add public key to users.sql delete mode 100644 supabase/sql_snippets/Check Realtime and Replica Identity for Messages.sql delete mode 100644 supabase/sql_snippets/Check and Set Replica Identity for Messages Table.sql delete mode 100644 supabase/sql_snippets/Create Private Thread Function.sql delete mode 100644 supabase/sql_snippets/Create Private Thread Function_1.sql delete mode 100644 supabase/sql_snippets/Enable Full Replica Identity for Thread Participants.sql delete mode 100644 supabase/sql_snippets/Enable Full Replica Identity for Users Table.sql delete mode 100644 supabase/sql_snippets/Enable Replication for Messages Table.sql delete mode 100644 supabase/sql_snippets/Get Thread Details.sql delete mode 100644 supabase/sql_snippets/Get User Threads.sql delete mode 100644 supabase/sql_snippets/Row Level Security Policies for Messages.sql delete mode 100644 supabase/sql_snippets/Row Level Security Policies for Messaging App.sql delete mode 100644 supabase/sql_snippets/Send Message Function.sql delete mode 100644 supabase/sql_snippets/Update User Requests Function.sql delete mode 100644 supabase/sql_snippets/User Access Policy for Search Function.sql delete mode 100644 supabase/sql_snippets/User Management Functions.sql delete mode 100644 supabase/sql_snippets/User Management Table.sql delete mode 100644 supabase/sql_snippets/User Registration Policy.sql delete mode 100644 supabase/sql_snippets/User Requests and Messages Management.sql delete mode 100644 supabase/sql_snippets/User and Message Indexes.sql delete mode 100644 supabase/sql_snippets/Users Table.sql delete mode 100644 tailwind.config.ts diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 15b1ed9..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next" -} diff --git a/.gitignore b/.gitignore index d32cc78..5ef6a52 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* # env files (can opt-in for committing if needed) .env* diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 912db82..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml deleted file mode 100644 index 9c331f5..0000000 --- a/.idea/material_theme_project_new.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6d06177..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/sipher.iml b/.idea/sipher.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/sipher.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f66bd35 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "css.customData": [ + ".vscode/tailwind.json" + ], + "editor.tabSize": 2, + "editor.wordWrap": "wordWrapColumn", + "editor.insertSpaces": false, + "editor.wordWrapColumn": 120, + "editor.detectIndentation": false, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.formatOnType": true, + "typescript.preferences.quoteStyle": "double", + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } +} \ No newline at end of file diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json new file mode 100644 index 0000000..8f1241e --- /dev/null +++ b/.vscode/tailwind.json @@ -0,0 +1,105 @@ +{ + "version": 4.0, + "atDirectives": [ + { + "name": "@import", + "description": "Use the `@import` directive to inline import CSS files, including Tailwind itself.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#import-directive" + } + ] + }, + { + "name": "@theme", + "description": "Use the `@theme` directive to define your project's custom design tokens, like fonts, colors, and breakpoints.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#theme-directive" + } + ] + }, + { + "name": "@source", + "description": "Use the `@source` directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#source-directive" + } + ] + }, + { + "name": "@utility", + "description": "Use the `@utility` directive to add custom utilities to your project that work with variants like `hover`, `focus` and `lg`.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#utility-directive" + } + ] + }, + { + "name": "@variant", + "description": "Use the `@variant` directive to apply a Tailwind variant to styles in your CSS. If you need to apply multiple variants at the same time, use nesting.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#variant-directive" + } + ] + }, + { + "name": "@custom-variant", + "description": "Use the `@custom-variant` directive to add a custom variant in your project. This lets you write utilities like `pointer-coarse:size-48` and `theme-midnight:bg-slate-900`.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#custom-variant-directive" + } + ] + }, + { + "name": "@apply", + "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you need to write custom CSS (like to override the styles in a third-party library) but still want to work with your design tokens and use the same syntax you’re used to using in your HTML.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#apply-directive" + } + ] + }, + { + "name": "@reference", + "description": "If you want to use `@apply` or `@variant` in the ` + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logos/logo-light.png b/public/logos/logo-light.png deleted file mode 100644 index cb478f19080f33aa03c37235ad31a5260ad89772..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43282 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 diff --git a/public/logos/logo.png b/public/logos/logo.png deleted file mode 100644 index f05e14c6ee9ebbdb9a2559af9d4a33f0b9063838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43429 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

ycY;zWUV{m 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 Q!T+)_vAX=`;_V0j1;kMJiU0rr diff --git a/public/logos/united-chat.png b/public/logos/united-chat.png deleted file mode 100644 index fb550dcfdd8bfb20944dfa71982dd560efe85bb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6188 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 diff --git a/src/app/[id]/page.tsx b/src/app/[id]/page.tsx deleted file mode 100644 index 6cd8647..0000000 --- a/src/app/[id]/page.tsx +++ /dev/null @@ -1,411 +0,0 @@ -"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"; -import ChatSkeleton from "@/app/[id]/skeleton"; - -export default function ChatPage() { - 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(REALTIME_SUBSCRIBE_STATES.CLOSED); - - 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") { - try { - const messageData = payload.new as SiPher.RealtimeMessageData; - const isSender = messageData.sender_uuid === currentUser.uuid; - - const decryptedMsg = await CryptoManager.decryptMessage( - // I forgot to add this, without this, it's pretty much unusable. - isSender ? messageData.sender_content : messageData.recipient_content - ) - - setMessages((prevState) => { - return [ - ...prevState, - { - id: messageData.id, - content: decryptedMsg, - sender_uuid: messageData.sender_uuid, - created_at: messageData.created_at, - isSender - } - ] - }) - } catch (e: any) { - console.error(`Something went wrong on the message update: ${e}`) - } - } - } - ) - .subscribe((status) => { - setRealtimeSubscribed(status) - console.info(`Subscription for thread ${threadId} has the status "${status}"`) - console.info("If closed, something bad might be happening at the backend.") - }) - - return () => { - supabase.removeChannel(channel) - } - }, [threadId, currentUser.uuid, supabase]) - - 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) - } - }, [threadId, currentUser.uuid, supabase]) // Never trusting the lint again - - useEffect(() => { - if (!realtimeSubscribed) return; - - const timeoutId = setTimeout(() => { - if (realtimeSubscribed === 'TIMED_OUT' || realtimeSubscribed === 'CLOSED') { - toast({ - title: "Connection Issue", - description: "You might need to restart your browser due to connection issues.", - variant: "destructive", - duration: 10000, - }); - } - }, 10000); - - return () => clearTimeout(timeoutId); - }, [realtimeSubscribed, toast]); - - if (!isLoaded || !user || realtimeSubscribed !== "SUBSCRIBED") { - return ; - } - - 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 ( -

-
-
- - - { - 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 - - - -
-
- - -
- - {messages.map((message) => ( - -
-

{message.content}

-
- - {new Date(message.created_at).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - })} - -
-
-
- ))} -
-
-
- -
-
- setInputMessage(e.target.value)} - placeholder="Type a message..." - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendMessage(inputMessage); - } - }} - /> - -
-
- - - - - 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/[id]/skeleton.tsx b/src/app/[id]/skeleton.tsx deleted file mode 100644 index ff18e85..0000000 --- a/src/app/[id]/skeleton.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import {Skeleton} from "@/components/ui/skeleton"; -import {ScrollArea} from "@/components/ui/scroll-area"; - -export default function ChatSkeleton() { - return ( -
- {/* Header Skeleton */} -
-
- - -
-
- - -
-
- - {/* Messages Skeleton */} - -
- {/* Left message */} -
- -
- {/* Right message */} -
- -
- {/* Left message */} -
- -
- {/* Right message */} -
- -
-
-
- - {/* Input Area Skeleton */} -
-
- - -
-
-
- ); -} \ No newline at end of file diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx deleted file mode 100644 index 015ee0d..0000000 --- a/src/app/about/page.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client" -import {motion} from "framer-motion"; -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; -import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"; -import {Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion"; -import {Separator} from "@/components/ui/separator"; -import {AlertTriangle, KeyRound, Lock, MessageSquare, Shield, UserCheck,} from "lucide-react"; - -export default function AboutPage() { - const containerVariants = { - hidden: {opacity: 0}, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1 - } - } - }; - - const itemVariants = { - hidden: {opacity: 0, y: 20}, - visible: {opacity: 1, y: 0} - }; - - return ( - - -

About SiPher

-

- Where privacy meets simplicity in secure communication -

-
- - - - - - - Important Notice - - SiPher is a CS50X final project and is not intended for production use. - While we implement strong encryption, please do not use it for sensitive communications. - - - - - - - - How SiPher Works - - Understanding the security behind your messages - - - -
-
- -
-

Key Generation

-

- Each user has a unique public-private key pair generated in their browser. Lost it and didn't - make a - backup? Welp, skill issue I guess. -

-
-
- -
- -
-

End-to-End Encryption

-

- Messages are encrypted before leaving your device -

-
-
- -
- -
-

Zero (And A Half) Trust

-

- Server never sees your decrypted messages. But we do store their encrypted version though lmao. -

-
-
- -
- -
-

User Privacy

-

- Users are identified by unique IDs, not personal information. No e-mail, no nothing, only your ID - (and probably IP due to Supabase logging it) -

-
-
-
-
-
-
- - - - - Technical Details - - The technology powering SiPher's "security" - - - -
-

Encryption

-
    -
  • RSA-OAEP for key exchange
  • -
  • AES-GCM for message encryption
  • -
  • PBKDF2 for key derivation
  • -
  • SHA-256 for message integrity
  • -
-
- -
-

Implementation

-
    -
  • Web Crypto API for cryptographic operations
  • -
  • Next.js for the application framework
  • -
  • Supabase for real-time messaging
  • -
  • TailwindCSS and ShadcnUI for the interface (I suck at design)
  • -
-
-
-
-
- - - - - Frequently Asked Questions - - - - - How secure are my messages? - - Messages are encrypted using industry-standard algorithms and never stored in plaintext. - However, as this is an educational project, I recommend not using it for sensitive communications. - If you do and I get a notice, I will give out the data I have on you. I don't care. - - - - - What happens if I lose my private key? - - If you lose your private key, you won't be able to decrypt previous messages. - You can generate a new key pair, but you'll need to start fresh conversations, previous messages - from - other conversations will be lost forever. - Always backup your private key in the settings. - - - - - Can I recover deleted messages? - - You can't even delete chats, imagine messages lmao. - - - - - How do I verify a user's identity? - - Each user has a unique SUUID (Short UUID) that can be shared and verified. - You can verify a user's identity by comparing their SUUID in a secure channel. - - - - - Is SiPher open source? - - Not yet. As this is a CS50X final project, the code will be made available - for educational purposes in the future. - - - - - Will you continue this project after submitting it? - - Probably. It's quite fun dealing with encryption. - - - - - - - - - - - Message Flow - - How your message travels from you to the other user - - - -
-
- -
-
-
-
- -
-
-

- Messages are encrypted on your device before being sent through our servers, - ensuring end-to-end encryption for all communications. -

- - - - - -

Built with đź’– as a CS50X final project

-
- - ); -} \ No newline at end of file diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..6cfe400 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,3 @@ +import { nextJsHandler } from "@convex-dev/better-auth/nextjs"; + +export const { GET, POST } = nextJsHandler(); \ 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 deleted file mode 100644 index e4e5d7a..0000000 --- a/src/app/api/auth/get_user/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; -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(); - if (authError) throw authError; - - if (!user) { - return NextResponse.json({user: null}, {status: 401}); - } - - const userData = await getUserByUUID(supabase, user.id); - 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} - ); - } -} \ No newline at end of file diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts deleted file mode 100644 index d7fa4d9..0000000 --- a/src/app/api/auth/login/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -// app/api/auth/login/route.ts -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; - -export async function POST(request: Request) { - try { - const {username, password} = await request.json() - const supabase = await createClient() - - const domain = process.env.DOMAIN; - - if (!domain) { - return NextResponse.json({ - error: "Server is misconfigured, please check env variables and try again." - }, - { - status: 500 - }) - } - - // Mocks the email with the domain we configured on the local env - const email = `${username.toLowerCase()}@${domain}` - - // Sends the request through supabase - const {data: {user}, error: authError} = await supabase.auth.signInWithPassword({ - email: email, - password: password, - }) - - if (authError) throw authError - - // Fetch our custom user data - const {data: userData, error: userError} = await supabase - .from('users') - .select('*, public_key') - .eq('uuid', user?.id) - .single() - - if (userError) throw userError - - // Returns simple data - return NextResponse.json({user: userData}) - } catch (error) { - return NextResponse.json( - {error: `Login failed: ${error}`}, - {status: 401} - ) - } -} \ No newline at end of file diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts deleted file mode 100644 index 37af265..0000000 --- a/src/app/api/auth/register/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {NextResponse} from 'next/server' -import {createClient} from "@/lib/supabase/server"; - -export async function POST(request: Request) { - const {username, password, public_key} = await request.json() - const supabase = await createClient() - - try { - const domain = process.env.DOMAIN; - - if (!domain) { - return NextResponse.json({ - error: "Server is misconfigured, please check env variables and try again." - }, - { - status: 500 - }) - } else if (!username || !password || !public_key) { - return NextResponse.json({ - error: "Missing params" - }, {status: 400}) - } - - // First create the auth user - const {data: {user}, error: authError} = await supabase.auth.signUp({ - email: `${username}@${domain}`, // Using username as email - password: password, - }) - - if (authError) throw authError - if (!user) throw new Error('No user returned from sign up') - - // Then create our custom user record - const {error: insertError} = await supabase - .from('users') - .insert({ - uuid: user.id, - username: username, - public_key - }) - - if (insertError) { - // Rollback auth user if custom user creation fails - await supabase.auth.admin.deleteUser(user.id) - throw insertError - } - - return NextResponse.json({success: true}) - } catch (error) { - if (typeof error === "object") { - return NextResponse.json( - {error: JSON.stringify(error)}, - {status: 400} - ) - } - return NextResponse.json( - {error: `Registration failed: ${error}`}, - {status: 400} - ) - } -} \ No newline at end of file diff --git a/src/app/api/user/create/thread/route.ts b/src/app/api/user/create/thread/route.ts deleted file mode 100644 index af5d214..0000000 --- a/src/app/api/user/create/thread/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {NextResponse} from "next/server"; -import {createClient} from "@/lib/supabase/server"; -import getUserByUUID from "@/lib/api/helpers/getUserByUUID"; - -export async function POST(req: Request) { - const {participant} = await req.json(); - - if (!participant) { - return NextResponse.json({error: 'Participant not found'}, {status: 400}); - } - - const supabase = await createClient() - - const {data: {user}, error: userError} = await supabase.auth.getUser() - console.log("From user: ", user?.id) - if (userError) { - return NextResponse.json( - {error: userError}, - {status: userError?.status} - ) - } else if (!user) { - return NextResponse.json( - {error: "User not found"}, - {status: 401} - ) - } - - /** 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"}, - {status: 401} - ) - } - - const requests = dbUser.requests as string[] - - if (!requests.includes(participant)) { - return NextResponse.json({error: "Requested user not in requests array."}, {status: 400}) - } else if (participant === dbUser.suuid) { - return NextResponse.json({error: "Cannot add self to a new thread"}, {status: 400}) - } - - /** Then we can create the thread */ - - const {error} = await supabase.rpc('create_private_thread', { - participant_suuid: participant - }); - - if (error) { - return NextResponse.json({error}, {status: 500}); - } - - return NextResponse.json({success: true}, {status: 200}); -} \ No newline at end of file diff --git a/src/app/api/user/get/thread/route.ts b/src/app/api/user/get/thread/route.ts deleted file mode 100644 index 4f7daa0..0000000 --- a/src/app/api/user/get/thread/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/get/threads/route.ts b/src/app/api/user/get/threads/route.ts deleted file mode 100644 index 7d14dd9..0000000 --- a/src/app/api/user/get/threads/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; - -export async function GET() { - try { - 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_user_threads", - { - user_id: user!.id - } - ) - - if (error) { - return NextResponse.json({error}, {status: 400}) - } - - return NextResponse.json({threads: data}, {status: 200}); - - } catch (e) { - } -} \ 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 deleted file mode 100644 index 23f5e7e..0000000 --- a/src/app/api/user/search/user/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; - -export async function GET(request: Request) { - try { - const supabase = await createClient(); - const {searchParams} = new URL(request.url); - const uuid = searchParams.get('uuid'); - const getDetails = searchParams.get("detailed") - - if (!uuid) { - return NextResponse.json({error: "Missing UUID from request"}, {status: 400}) - } else if (uuid.length > 10) { - return NextResponse.json({error: "UUID is not valid."}, {status: 400}); - } - - const {data: {user}, error: userError} = await supabase.auth.getUser() - - if (userError) { - return NextResponse.json( - {error: userError}, - {status: userError?.status} - ) - } else if (!user) { - return NextResponse.json( - {error: "User not found"}, - {status: 401} - ) - } - - const {data, error} = await supabase.rpc('search_users', { - search_term: uuid - }); - - 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}); - - } catch (error) { - return NextResponse.json({error: error}, {status: 500}); - } -} \ No newline at end of file diff --git a/src/app/api/user/send/message/route.ts b/src/app/api/user/send/message/route.ts deleted file mode 100644 index 2d16b2e..0000000 --- a/src/app/api/user/send/message/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index f291698..0000000 --- a/src/app/api/user/send/request/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; -import getUserByUUID from "@/lib/api/helpers/getUserByUUID"; -import updateUserRequests from "@/lib/api/helpers/updateUserRequests"; - -export async function POST(request: Request) { - try { - const supabase = await createClient(); - - const {searchTerm} = await request.json(); - - if (!searchTerm) { - return NextResponse.json( - {error: "Missing required fields"}, - {status: 400} - ); - } - - const {data: {user}, error: authError} = await supabase.auth.getUser(); - if (authError) throw authError; - - if (!user) { - return NextResponse.json({user: null}, {status: 401}); - } - - const getUser = await getUserByUUID(supabase, user.id) - const userSuuid = getUser.suuid; - - if (userSuuid === searchTerm) { - return NextResponse.json({success: false, hint: "Cannot send request to self"}, {status: 409}); - } - - const result = await updateUserRequests(searchTerm, userSuuid, supabase); - - if (!result.success) { - return NextResponse.json( - {error: result.error}, - {status: 500} - ); - } - - return NextResponse.json({success: true}); - } catch (err) { - return NextResponse.json( - {error: `Failed to update requests: ${err}`}, - {status: 500} - ); - } -} \ No newline at end of file diff --git a/src/app/api/user/send/update/key/route.ts b/src/app/api/user/send/update/key/route.ts deleted file mode 100644 index f59e220..0000000 --- a/src/app/api/user/send/update/key/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {createClient} from "@/lib/supabase/server"; -import {NextResponse} from "next/server"; - -export async function POST(request: Request) { - try { - const {publicKey} = await request.json(); - const supabase = await createClient(); - - const {error} = await supabase - .from('users') - .update({public_key: publicKey}) - .eq('uuid', (await supabase.auth.getUser()).data.user?.id); - - if (error) throw error; - - return NextResponse.json({success: true}); - } catch (error) { - return NextResponse.json( - {error: 'Failed to update public key'}, - {status: 500} - ); - } -} \ No newline at end of file diff --git a/src/app/auth/components/sign-in-form.tsx b/src/app/auth/components/sign-in-form.tsx new file mode 100644 index 0000000..4815651 --- /dev/null +++ b/src/app/auth/components/sign-in-form.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { authClient } from "@/lib/auth/client"; +import { ErrorContext } from "better-auth/react"; +import { Loader2 } from "lucide-react"; +import { redirect } from "next/navigation"; +import { useState } from "react"; +import { toast } from "sonner"; + +export function SignInForm( + { captchaToken }: { captchaToken: string | null } +) { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); + await authClient.signIn.username( + { + username, + password, + fetchOptions: { + headers: { + "x-captcha-response": captchaToken ?? "", + }, + }, + }, + { + onRequest: () => { + setLoading(true); + }, + onSuccess: () => { + setLoading(false); + toast.success("Signed in successfully"); + redirect("/"); + }, + onError: (ctx: ErrorContext) => { + setLoading(false); + toast.error(ctx.error.message); + }, + + } + ); + }; + + return ( +
+
+ + setUsername(e.target.value)} + className="bg-background/50 focus:bg-background transition-colors" + /> +
+
+ + setPassword(e.target.value)} + className="bg-background/50 focus:bg-background transition-colors" + /> +
+ +
+ ); +} + diff --git a/src/app/auth/components/sign-up-form.tsx b/src/app/auth/components/sign-up-form.tsx new file mode 100644 index 0000000..6dd22c0 --- /dev/null +++ b/src/app/auth/components/sign-up-form.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { authClient } from "@/lib/auth/client"; +import { ErrorContext } from "better-auth/react"; +import { Check, Eye, EyeOff, Loader2, RefreshCw, X } from "lucide-react"; +import { redirect } from "next/navigation"; +import { useState } from "react"; +import { toast } from "sonner"; + +export function SignUpForm( + { captchaToken }: { captchaToken: string | null } +) { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isUsernameAvailable, setIsUsernameAvailable] = useState(null); + const [loading, setLoading] = useState(false); + const [isValidatingUsername, setIsValidatingUsername] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + if (password.length > 30) { + toast.error("Password must be less than 30 characters"); + return; + } + + await authClient.signUp.email( + { + email: `${username}.user@sipher.space`, + name: username, + username, + password, + fetchOptions: { + headers: { + "x-captcha-response": captchaToken ?? "", + }, + }, + }, + { + onRequest: () => { + setLoading(true); + }, + onSuccess: async () => { + setLoading(false); + toast.success("Account created successfully, logging in..."); + await authClient.signIn.username( + { + username, + password, + fetchOptions: { + headers: { + "x-captcha-response": captchaToken ?? "", + }, + }, + }, + { + onSuccess: () => { + toast.success("Logged in successfully"); + redirect("/"); + }, + onError: (ctx: ErrorContext) => { + toast.error(ctx.error.message); + }, + } + ); + }, + onError: (ctx: ErrorContext) => { + setLoading(false); + toast.error(ctx.error.message); + }, + + } + ); + }; + + const generatePassword = () => { + const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+"; + let newPassword = ""; + for (let i = 0; i < 16; i++) { + newPassword += chars.charAt(Math.floor(Math.random() * chars.length)); + } + setPassword(newPassword); + setConfirmPassword(newPassword); + navigator.clipboard.writeText(newPassword); + toast.success("Password generated and copied to clipboard"); + }; + + return ( +
+
+ +
+ { + const val = e.target.value; + setUsername(val); + if (val) { + setIsValidatingUsername(true); + // @ts-ignore + const isValid = await authClient.isUsernameAvailable({ username: val }); + setIsUsernameAvailable(!!isValid); + setIsValidatingUsername(false); + } else { + setIsUsernameAvailable(null); + } + }} + className={`bg-background/50 focus:bg-background transition-colors pr-10 ${isUsernameAvailable === false ? "border-red-500 focus-visible:ring-red-500" : + isUsernameAvailable === true ? "border-green-500 focus-visible:ring-green-500" : "" + }`} + /> +
+ {isValidatingUsername ? ( + + ) : isUsernameAvailable === true ? ( + + ) : isUsernameAvailable === false ? ( + + ) : null} +
+
+ {isUsernameAvailable === false && ( +

Username is already taken

+ )} +
+
+ +
+ setPassword(e.target.value)} + className={`bg-background/50 focus:bg-background transition-colors pr-24 ${password.length >= 8 && password.length <= 30 + ? "border-green-500 focus-visible:ring-green-500" + : password.length > 30 + ? "border-red-500 focus-visible:ring-red-500" + : "" + }`} + /> +
+ {password.length > 30 ? ( + + ) : password.length >= 8 && ( + + )} + + +
+
+ {password.length > 30 && ( +

Password must be less than 30 characters

+ )} +
+
+ +
+ setConfirmPassword(e.target.value)} + className={`bg-background/50 focus:bg-background transition-colors pr-10 ${confirmPassword && password === confirmPassword && password.length <= 30 + ? "border-green-500 focus-visible:ring-green-500" + : (confirmPassword && password !== confirmPassword) || (confirmPassword && password.length > 30) + ? "border-red-500 focus-visible:ring-red-500" + : "" + }`} + /> +
+ {confirmPassword && password === confirmPassword && password.length <= 30 ? ( + + ) : confirmPassword && (password !== confirmPassword || password.length > 30) ? ( + + ) : null} +
+
+ {confirmPassword && password !== confirmPassword && ( +

Passwords do not match

+ )} +
+ +
+ ); +} diff --git a/src/app/auth/login/login.ts b/src/app/auth/login/login.ts deleted file mode 100644 index c59245b..0000000 --- a/src/app/auth/login/login.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * - * @param username - The unique username of that user. This will be checked for collision. - * @param password - The plain-text password of the user. Supabase will try to match it. - * @constructor - */ -export default async function Login(username: string, password: string) { - try { - let response = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({username, password}), - }); - - // Simple error handling. - // Since we mock an email on the main app to bypass Supabase's authentication method, we can just return whatever the API returns. - // This also means this might be insecure, but oh well. Don't lose your password, I guess? - let resData = await response.json(); - - if (!response.ok) { - return ({ - code: resData.code, - message: resData.message - }); - } - - return ({ - code: 200, - message: resData.data - }); - } catch (e) { - return {code: 500, message: "An unknown error occurred"}; - } -} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx deleted file mode 100644 index c5bcf9a..0000000 --- a/src/app/auth/login/page.tsx +++ /dev/null @@ -1,225 +0,0 @@ -"use client" - -import React, {useCallback, useEffect, useState} from 'react' -import Image from 'next/image' -import {motion} from 'framer-motion' -import {Button} from "@/components/ui/button" -import {Input} from "@/components/ui/input" -import {Label} from "@/components/ui/label" -import {Card, CardContent} from "@/components/ui/card" -import {EyeIcon, EyeOffIcon} from 'lucide-react' -import {useToast} from "@/hooks/use-toast" -import {ToastActionElement} from "@/components/ui/toast"; -import {useUser} from "@/contexts/user"; -import {useRouter} from "next/navigation"; -import {useTheme} from "next-themes"; -import Register from "@/app/auth/login/register"; -import Login from "@/app/auth/login/login"; - -export default function AuthPage() { - const {checkAuth} = useUser(); - const {theme, systemTheme} = useTheme() - const {toast} = useToast(); - const [mounted, setMounted] = useState(false); - const [isLogin, setIsLogin] = useState(true); - const [showPassword, setShowPassword] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const router = useRouter(); - - const check = useCallback(async () => { - const isAuthenticated = await checkAuth("Called on Login page"); - if (isAuthenticated) { - router.replace('/'); - } else { - setMounted(true); - } - }, [checkAuth, router, setMounted]) - - useEffect(() => { - check().then(() => { - console.log("Login page check finished") - }) - }, [check]); - - if (!mounted) { - return
- -
; - } - - - const getTheme = () => { - if (theme === "system") { - switch (systemTheme) { - case "dark": - return "dark" - default: - return "light" - } - } - - return theme === "dark" ? "dark" : "light" - } - - const logoSrc = getTheme() === 'dark' ? '/logos/logo-light.png' : '/logos/logo.png'; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const username = (document.getElementById('username') as HTMLInputElement).value; - const password = (document.getElementById('password') as HTMLInputElement).value; - - let response: { - code: number; - message: string; - action?: ToastActionElement | undefined; - } - - if (!isLogin) { - response = await Register(username, password); - } else { - response = await Login(username, password); - } - - if (response.code !== 200) { - const msg = response.message - - try { - const parsed = JSON.parse(msg); - let desc = parsed.name; - - switch (desc) { - case "AuthWeakPasswordError": { - desc = "Password too weak, please try again."; - break; - } - default: { - desc = "An unknown error occurred"; - } - } - - toast({ - title: "Error", - description: desc, - variant: "destructive", - duration: 5000 - }); - } catch (e) { - // If msg isn't valid JSON, show the raw message - toast({ - title: "Error", - description: msg, - variant: "destructive", - duration: 5000 - }); - } - } else { - toast({ - title: "Success", - description: response.message, - variant: "default", - duration: 5000, // Increased duration for better visibility - }); - window.location.href = "/"; - } - - setTimeout(() => { - setIsSubmitting(false); - }, 2000) - }; - - return ( -
- - -
-
- SiPher -

- Silent Whisper -

-

- Trust the shadows. Whisper safely. -

-
-
- -

- {isLogin ? "Sign In" : "Sign Up"} -

-
-
- - -
-
- -
- - -
-
- -
-
- -
-
-
-
-
-
-
- ) -} \ No newline at end of file diff --git a/src/app/auth/login/register.ts b/src/app/auth/login/register.ts deleted file mode 100644 index e9aa465..0000000 --- a/src/app/auth/login/register.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {CryptoManager} from "@/lib/crypto/keys"; - -/** - * - * @param username - The unique username of that user. This will be checked for collision. - * @param password - The plain-text password of the user. Will be encrypted later by Supabase - * @constructor - */ -export default async function Register(username: string, password: string) { - try { - const keyPair = await CryptoManager.generateUserKeys(); - await CryptoManager.storePrivateKey(keyPair.privateKey); - - // Export public key for server - const exportedPublic = await crypto.subtle.exportKey('jwk', keyPair.publicKey); - - // Sends the request to the API - let res = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({username, password, public_key: exportedPublic}), // Stringifies the JSON - }); - - // Default error handler, if not OK just return whatever the API returned - if (!res.ok) { - let data = await res.json(); - return { - code: res.status, - message: data.error - } - } - - // User was created, now it just needs to login on the service. - return { - code: 200, - message: "User created successfully, go ahead and login." - } - } catch (e: any) { - return { - code: 500, - message: e.error - } - } -} diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx new file mode 100644 index 0000000..c45220a --- /dev/null +++ b/src/app/auth/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { ModeToggle } from "@/components/mode-toggle"; +import { Button } from "@/components/ui/button"; +import Captcha, { CaptchaRef } from "@/components/ui/captcha"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; +import { authClient } from "@/lib/auth/client"; +import { AnimatePresence, motion } from "framer-motion"; +import { RefreshCw } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { useRef, useState } from "react"; +import { toast } from "sonner"; +import { SignInForm } from "./components/sign-in-form"; +import { SignUpForm } from "./components/sign-up-form"; + +export default function AuthPage() { + const { data, error, isPending } = authClient.useSession(); + const [captchaToken, setCaptchaToken] = useState(null); + const [method, setMethod] = useState<"signIn" | "signUp">("signIn"); + const captchaRef = useRef(null); + + if (isPending) { + return ( +
+ +
+ ); + } + + if (error && error.status !== 404) { + console.error("[AuthPage] > Error:", error); + toast.error(error.message); + } else if (data) { + console.log(`[AuthPage] > User ${data.user.username} logged in, redirecting to home...`); + redirect("/"); + } + + const toggleMethod = () => { + setMethod(method === "signIn" ? "signUp" : "signIn"); + }; + + return ( +
+ {/* Animated Background Blobs */} + + + + + +
+ +
+ + + + + {method === "signIn" ? "Welcome Back" : "Create Account"} + + + {method === "signIn" + ? "Enter your credentials to access your account" + : "Enter your details to get started with us"} + + + + + + {method === "signIn" ? : } + + +
+ {method === "signIn" ? "Don't have an account? " : "Already have an account? "} + +
+ {/* Turnstile */} +
+ + {/* Reload the captcha */} + +
+ +
+

+ built with{" "} + + better-auth + +

+
+
+
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index b1b7658..b23277f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,61 +1,202 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1.0000 0 0); + --foreground: oklch(0.2686 0 0); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.2686 0 0); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.2686 0 0); + --primary: oklch(0.7686 0.1647 70.0804); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.9670 0.0029 264.5419); + --secondary-foreground: oklch(0.4461 0.0263 256.8018); + --muted: oklch(0.9846 0.0017 247.8389); + --muted-foreground: oklch(0.5510 0.0234 264.3637); + --accent: oklch(0.9869 0.0214 95.2774); + --accent-foreground: oklch(0.4732 0.1247 46.2007); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9276 0.0058 264.5313); + --input: oklch(0.9276 0.0058 264.5313); + --ring: oklch(0.7686 0.1647 70.0804); + --chart-1: oklch(0.7686 0.1647 70.0804); + --chart-2: oklch(0.6658 0.1574 58.3183); + --chart-3: oklch(0.5553 0.1455 48.9975); + --chart-4: oklch(0.4732 0.1247 46.2007); + --chart-5: oklch(0.4137 0.1054 45.9038); + --sidebar: oklch(0.9846 0.0017 247.8389); + --sidebar-foreground: oklch(0.2686 0 0); + --sidebar-primary: oklch(0.7686 0.1647 70.0804); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9869 0.0214 95.2774); + --sidebar-accent-foreground: oklch(0.4732 0.1247 46.2007); + --sidebar-border: oklch(0.9276 0.0058 264.5313); + --sidebar-ring: oklch(0.7686 0.1647 70.0804); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-x: 1px; + --shadow-y: 2px; + --shadow-blur: 8px; + --shadow-spread: -1px; + --shadow-opacity: 0.1; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); + --tracking-normal: 0em; + --spacing: 0.25rem; + --shadow-offset-x: 0px; + --shadow-offset-y: 4px; + --letter-spacing: 0em; +} + +.dark { + --background: oklch(0.2046 0 0); + --foreground: oklch(0.9219 0 0); + --card: oklch(0.2686 0 0); + --card-foreground: oklch(0.9219 0 0); + --popover: oklch(0.2686 0 0); + --popover-foreground: oklch(0.9219 0 0); + --primary: oklch(0.7686 0.1647 70.0804); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.2686 0 0); + --secondary-foreground: oklch(0.9219 0 0); + --muted: oklch(0.2393 0 0); + --muted-foreground: oklch(0.7155 0 0); + --accent: oklch(0.4732 0.1247 46.2007); + --accent-foreground: oklch(0.9243 0.1151 95.7459); + --destructive: oklch(0.6368 0.2078 25.3313); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.3715 0 0); + --input: oklch(0.3715 0 0); + --ring: oklch(0.7686 0.1647 70.0804); + --chart-1: oklch(0.8369 0.1644 84.4286); + --chart-2: oklch(0.6658 0.1574 58.3183); + --chart-3: oklch(0.4732 0.1247 46.2007); + --chart-4: oklch(0.5553 0.1455 48.9975); + --chart-5: oklch(0.4732 0.1247 46.2007); + --sidebar: oklch(0.1684 0 0); + --sidebar-foreground: oklch(0.9219 0 0); + --sidebar-primary: oklch(0.7686 0.1647 70.0804); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.4732 0.1247 46.2007); + --sidebar-accent-foreground: oklch(0.9243 0.1151 95.7459); + --sidebar-border: oklch(0.3715 0 0); + --sidebar-ring: oklch(0.7686 0.1647 70.0804); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-x: 1px; + --shadow-y: 2px; + --shadow-blur: 8px; + --shadow-spread: -1px; + --shadow-opacity: 0.1; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10); + --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); + --shadow-offset-x: 0px; + --shadow-offset-y: 4px; + --letter-spacing: 0em; + --spacing: 0.25rem; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: Inter, sans-serif; + --font-mono: JetBrains Mono, monospace; + --font-serif: Source Serif 4, serif; + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); + + --tracking-tighter: calc(var(--tracking-normal) - 0.05em); + --tracking-tight: calc(var(--tracking-normal) - 0.025em); + --tracking-normal: var(--tracking-normal); + --tracking-wide: calc(var(--tracking-normal) + 0.025em); + --tracking-wider: calc(var(--tracking-normal) + 0.05em); + --tracking-widest: calc(var(--tracking-normal) + 0.1em); + --radius: 0.375rem; + --spacing: var(--spacing); + --letter-spacing: var(--letter-spacing); + --shadow-offset-y: var(--shadow-offset-y); + --shadow-offset-x: var(--shadow-offset-x); + --shadow-spread: var(--shadow-spread); + --shadow-blur: var(--shadow-blur); + --shadow-opacity: var(--shadow-opacity); + --color-shadow-color: var(--shadow-color); +} + +body { + letter-spacing: var(--tracking-normal); +} @layer base { - :root { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 20.5 90.2% 48.2%; - --radius: 0.75rem; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } - - .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 20.5 90.2% 48.2%; - --radius: 0.75rem; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + letter-spacing: var(--tracking-normal); + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ad055f3..43552a3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,73 +1,57 @@ -// app/layout.tsx -import type {Metadata} from "next"; +import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; +import { ConvexClientProvider } from "@/lib/providers/Convex"; +import type { Metadata } from "next"; import "./globals.css"; -import {Public_Sans} from 'next/font/google'; -import {UserProvider} from "@/contexts/user"; -import Sidebar from "@/components/main/sidebar/sidebar"; -import {getAuthenticatedUser} from "@/lib/auth"; -import {SharedStateProvider} from "@/hooks/shared-states"; -import ThemeProvider from "@/components/ui/theme-provider"; -import {headers} from "next/headers"; -import {Toaster} from "@/components/ui/toaster"; - -const publicSans = Public_Sans({ - subsets: ['latin'], - display: 'swap', - variable: '--font-public-sans' -}); export const metadata: Metadata = { - title: "SiPher - Where Shadows Live", - description: "Secrecy? Not here, absolutely.", - icons: [{rel: "icon", url: "/logos/logo.png"}], + title: "SiPher - Don't trust us. We don't trust you.", + description: "SiPher is a platform made for communication. Secure? Maybe. Reliable? I don't think so. We don't trust you. We don't trust us. We don't trust anyone.", + icons: { + icon: [ + { + url: "/assets/logo/logo-white.svg", + href: "/assets/logo/logo-white.svg", + media: "(prefers-color-scheme: dark)", + type: "image/svg+xml", + sizes: "32x32", + rel: "icon" + }, + { + url: "/assets/logo/logo-dark.svg", + href: "/assets/logo/logo-dark.svg", + media: "(prefers-color-scheme: light)", + type: "image/svg+xml", + sizes: "32x32", + rel: "icon" + } + ] + } }; -export default async function RootLayout( - { - children, - }: { - children: React.ReactNode & { props?: { childProp?: { segment?: string } } }; - }) { - const initialUser = await getAuthenticatedUser(); - const isAuthPage = (await headers()).get("x-current-pathname")?.includes("auth"); - - // Auth layout - if (isAuthPage) { - return ( - - - - - {children} - - - - - - ); - } - - // Main layout +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( - - - - - -
-
-
- - {children} - -
-
-
-
-
- -
- + + + + + {children} + + + + ); -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7834722..f00042e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,318 +1,28 @@ "use client" -import {useTheme} from "next-themes"; -import Image from "next/image"; -import {Feather, Search} from "lucide-react"; -import {useEffect, useState} from "react"; -import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion"; -import {Separator} from "@/components/ui/separator"; -import Link from "next/link"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger -} from "@/components/ui/alert-dialog"; -import {CryptoManager} from "@/lib/crypto/keys"; -import UpdateKey from "@/lib/crypto/helpers/updateKey"; +import AppSidebar from "@/components/home"; +import { Spinner } from "@/components/ui/spinner"; +import { authClient } from "@/lib/auth/client"; +import { redirect } from "next/navigation"; -export default function SiPher() { - const {theme, systemTheme} = useTheme(); - const [mounted, setMounted] = useState(false); - - /** CryptoManager Alert */ - const [privateKeyPresent, setPrivateKeyPresent] = useState(true); - const [backupPanel, setBackupPanel] = useState(false); // I still need to do this, but... ugh. - - /** Consent Form states */ - const [showConsentForm, setShowConsentForm] = useState(false); - const [formError, setFormError] = useState(""); - - /** Input states */ - const [inputDisabled, setInputDisabled] = useState(false); - const [inputValue, setInputValue] = useState(""); - - /** Search expandability state */ - const [isSearchExpanded, setIsSearchExpanded] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - CryptoManager.getPrivateKey().then((res) => { - if (!res) { - console.log(res) - setPrivateKeyPresent(false); - } - }) - }, []) - - /** - * @param search_term Either the SUUID or username (If not indexable, will return false.) - */ - const fetchUser = async (search_term: string) => { - // Search term cannot be empty - if (search_term.length <= 0) { - return false; - } - - // Sends the requisition to the API by using native fetch. - const req = await fetch(`/api/user/search/user?uuid=${search_term}`); - - // Checks if the response is ok (200) or not, if not, returns false. - if (!req.ok) { - return false - } - - const user = await req.json() as { exists: boolean }; - // If the user does not exist, just return it - if (!user.exists) return user.exists; - - setShowConsentForm(true); // Shows the confirmation to ask the other user to consent to the communication; - setInputDisabled(true); // Makes the input disabled until either the user cancels the consent form or accepts it; - return user.exists; // If everything went right and the user was found, return true +export default function Home() { + const { data, error, isPending, } = authClient.useSession(); + + if (isPending) { + return
+ +
} - const sendRequest = async (user: string) => { - if (user.length <= 0) { - return false; - } - - const req = await fetch(`/api/user/send/request`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - searchTerm: user, // SUUID or username - }) - }); - - if (!req.ok) { - const res = await req.json(); - setFormError(res.hint); - return false; - } - - const {sent} = await req.json() as { sent: boolean }; - // If the user does not exist, just return it - if (!sent) return sent; - - return sent; + + if (error || !data) { + return redirect(`/auth${error ? `?error=${error.cause}` : ""}`); } - - const getTheme = () => { - if (!mounted) return "light"; - if (theme === "system") { - return systemTheme === "dark" ? "dark" : "light"; - } - return theme === "dark" ? "dark" : "light"; - }; - - const currentTheme = getTheme(); - - const MainPageAlerts = () => { - return ( - <> - { - if (!open) setFormError(""); - }}> - - - - Consent Form - - { - formError ? ( - {formError} - ) : null - } - - Are you sure you want to contact {inputValue}? - - - By continuing, {inputValue} will receive a notification to accept - it. If accepted, that user will appear on your sidebar, if rejected, you will never know about it. - - - - - { - setShowConsentForm(false); - setInputDisabled(false); - }} - >Cancel - { - sendRequest(inputValue); - setInputDisabled(false); - setShowConsentForm(false); - }} - >Continue - - - - - - - - - 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. - - - - { - setShowConsentForm(false); - setInputDisabled(false); - }} - >Cancel - { - sendRequest(inputValue).then((result) => { - if (!result) setFormError("Could not send notification for whatever reason. Sorry."); - }); - setInputDisabled(false); - }} - >Try Again - { - UpdateKey().then((result) => { - if (result.status !== 200) { - return; - } - setPrivateKeyPresent(true) - }) - }} - >Regenerate - - - - - ) - } - + return ( <> - - -
-
-
-
- SiPher -
- -
-

- Where shadows dance and secrets nest, Silent Whisper serves as the dark sanctuary for those - who value discretion above all. Born from ancient corvid traditions, this messenger’s haven ensures - your - whispers remain unheard by all but their intended recipients. -

- -

- Like the sacred ravens of old, your messages fly through the darkness, their contents sealed by shadows - and - protected by forgotten wards. Each member of our dark fellowship is known only by their chosen name, their - true identity shrouded in mystery. -

-
- -
-
- - - setInputValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - fetchUser(inputValue).then((res) => { - console.log(res); - }) - } - }} - /> -
- - -
- - -
-

- F.A.Q -

- - - How does this works? - - - Please, click here - - - - - Why does this exists? - - I made this as a CS50X final project, hence why it is not intended for real usage. (Do not use it in a - situation where you need real privacy.) - - - - Is this open-source? - - No, not yet (As of 11/12/2024) - - - -
+ +
-
+ - ); + ) } \ No newline at end of file diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx deleted file mode 100644 index 9379e13..0000000 --- a/src/app/settings/page.tsx +++ /dev/null @@ -1,287 +0,0 @@ -"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/home/index.tsx b/src/components/home/index.tsx new file mode 100644 index 0000000..e7ce899 --- /dev/null +++ b/src/components/home/index.tsx @@ -0,0 +1,72 @@ +import LogoIcon from "@/components/ui/logo-icon"; +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuItem, + SidebarProvider, + SidebarTrigger +} from "@/components/ui/sidebar"; +import { Button } from "../ui/button"; +import { Separator } from "../ui/separator"; + +const SidebarItems = [ + { + id: "home", + // The icon of the home item is the same as the logo + icon: + } +] + +/** + * The main component for the homepage. This component is used to wrap all the components of any page. + * It also is the controller for everything on the app, including going to other pages, showing conversations and other. + * @param children - The children to be rendered in the sidebar inset + */ +export default function AppSidebar({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {SidebarItems.map((item) => ( + + + + ))} + + + +
+
+
+ +
+

Your Header Title

+
{/* Spacer for centering on mobile */} +
+ +
+ {children} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/main/realtime/index.tsx b/src/components/main/realtime/index.tsx deleted file mode 100644 index ae11924..0000000 --- a/src/components/main/realtime/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// hooks/useRealtime.ts -import {Dispatch, SetStateAction, useCallback, useEffect} from 'react' -import {createBrowserClient} from '@/lib/supabase/browser' -import {useUser} from '@/contexts/user' - -interface UseRealtimeProps { - setThreads: Dispatch>; -} - -export function useRealtime({setThreads}: UseRealtimeProps) { - const supabase = createBrowserClient(); - const {user, updateUser} = useUser(); - - const fetchAndUpdateThreads = useCallback(async () => { - try { - const response = await fetch("/api/user/get/threads"); - if (response.ok) { - const {threads} = await response.json(); - setThreads(threads); - } - } catch (error) { - console.error('Error fetching threads:', error); - } - }, [setThreads]) - - useEffect(() => { - if (!user) return; - - const userUpdate = supabase - .channel("request updates") - .on("postgres_changes", { - event: "*", - schema: 'public', - 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 - // the corresponding request. - if (payload.new.requests !== payload.old.requests) { - updateUser({ - ...user, - 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() - - const threadUpdate = supabase - .channel("thread updates") - .on("postgres_changes", { - event: "*", - schema: 'public', - table: "thread_participants", - filter: `user_uuid=eq.${user.uuid}` - }, async (payload) => { - if (payload.new !== payload.old) { - await fetchAndUpdateThreads(); - } - }).subscribe() - - return () => { - threadUpdate.unsubscribe() - userUpdate.unsubscribe() - } - - }, [user?.uuid, fetchAndUpdateThreads, supabase, updateUser, user]); -} \ No newline at end of file diff --git a/src/components/main/sidebar/mobile.tsx b/src/components/main/sidebar/mobile.tsx deleted file mode 100644 index e69ccf7..0000000 --- a/src/components/main/sidebar/mobile.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client" -import React from 'react' -import {Button} from "@/components/ui/button" -import {HamburgerMenuIcon} from "@radix-ui/react-icons" -import {useTheme} from "next-themes" -import Image from "next/image" -import {useUIState} from "@/hooks/shared-states" -import Link from "next/link"; - -const MobileHeader: React.FC = () => { - const {setIsDrawerOpen} = useUIState() - const {theme, systemTheme} = useTheme() - - const getTheme = () => { - if (theme === "system") { - switch (systemTheme) { - case "dark": - return "dark" - default: - return "light" - } - } - return theme === "dark" ? "dark" : "light" - } - - const logoSrc = getTheme() === 'dark' ? '/logos/logo-light.png' : '/logos/logo.png' - - return ( -
-
- - -
- - Logo - -
- - {/* Empty div to maintain center alignment */} -
-
-
- ) -} - -export default MobileHeader \ No newline at end of file diff --git a/src/components/main/sidebar/rightsidebar.tsx b/src/components/main/sidebar/rightsidebar.tsx deleted file mode 100644 index 75a3851..0000000 --- a/src/components/main/sidebar/rightsidebar.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, {useCallback, useEffect, useState} from "react"; -import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; -import {Avatar, AvatarFallback} from "@/components/ui/avatar"; -import {Separator} from "@/components/ui/separator"; -import {ScrollArea} from "@/components/ui/scroll-area"; -import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"; -import {Check, LogOut, Mail, MailPlus, X} from "lucide-react"; -import {Button} from "@/components/ui/button"; -import {GearIcon} from "@radix-ui/react-icons"; -import Link from "next/link"; -import {useRealtime} from "@/components/main/realtime"; -import {useUser} from "@/contexts/user"; -import {usePathname} from "next/navigation"; -import {useSharedState} from "@/hooks/shared-states"; - -interface RightSidebarContentProps { - isDarkMode: boolean; -} - -export default function RightSidebarContent( - { - isDarkMode, - }: RightSidebarContentProps) { - - const [copied, setCopied] = useState(false); - - const {threads, setThreads} = useSharedState(); - useRealtime({setThreads}); - - const {user} = useUser(); - const {username, suuid, requests = []} = user; - const pathname = usePathname(); - - const pendingRequests = requests?.length ?? 0; - - const fetchThreads = useCallback(async () => { - try { - const req = await fetch("/api/user/get/threads") - if (req.ok) { - const {threads} = await req.json() as { threads: SiPher.Thread[] | [] } - setThreads(threads) - } else { - setThreads([]) - } - } catch (error) { - console.log(error); - setThreads([]) - } - }, [setThreads]); - - useEffect(() => { - fetchThreads(); - }, [fetchThreads]); - - const handleAccept = async (request: string) => { - try { - const response = await fetch("/api/user/create/thread", { - method: "POST", - body: JSON.stringify({participant: request}), - }); - if (response.ok) { - fetchThreads(); - } - } catch (error) { - console.error('Error accepting request:', error); - } - } - - return ( - <> -
- - - - - Copied SUUID to clipboard! - - - -
{ - setCopied(true) - navigator.clipboard.writeText(suuid) - }} - className={`flex items-center p-3 m-2 ${isDarkMode ? "hover:bg-accent/90" : "hover:bg-secondary/20"} rounded-full transition-colors duration-200 cursor-pointer select-none`}> - - {username.charAt(0)} - -
-

{username}

-

${suuid}

-
-
- - - - -
- - - -
-
- - ) -} \ No newline at end of file diff --git a/src/components/main/sidebar/sidebar.tsx b/src/components/main/sidebar/sidebar.tsx deleted file mode 100644 index 2843719..0000000 --- a/src/components/main/sidebar/sidebar.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client" -import React from "react" -import Link from "next/link" -import {AnimatePresence, motion} from "framer-motion" -import {X} from "lucide-react" -import {Button} from "@/components/ui/button" -import Image from "next/image"; -import MobileHeader from "@/components/main/sidebar/mobile"; -import {useRefs, useUIState} from "@/hooks/shared-states"; -import {useTheme} from "next-themes"; -import RightSidebarContent from "@/components/main/sidebar/rightsidebar"; - -type SidebarProps = { - children?: React.ReactNode -} - -function Sidebar( - { - children - }: SidebarProps -) { - const {theme, systemTheme} = useTheme(); - - const {isDrawerOpen, setIsDrawerOpen} = useUIState(); - const {drawerRef} = useRefs(); - - const isDarkMode = theme === "system" - ? systemTheme === "dark" - : theme === "dark" - - return ( - <> - - - - {isDrawerOpen && ( - -
- - -
-
- )} -
-
{ - children ?? null - } -
- - ) -} - -export default Sidebar \ No newline at end of file diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx new file mode 100644 index 0000000..a4776c9 --- /dev/null +++ b/src/components/mode-toggle.tsx @@ -0,0 +1,20 @@ +"use client" + +import * as React from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" + +export function ModeToggle() { + const { theme, setTheme } = useTheme() + + return ( + + ) +} + diff --git a/src/components/socket-test.tsx b/src/components/socket-test.tsx new file mode 100644 index 0000000..b2c20ec --- /dev/null +++ b/src/components/socket-test.tsx @@ -0,0 +1,93 @@ +"use client" + +import { useEffect, useState } from "react" +import { io, Socket } from "socket.io-client" +import { Button } from "./ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" +import { Input } from "./ui/input" + +export default function SocketTest() { + const [socket, setSocket] = useState(null) + const [isConnected, setIsConnected] = useState(false) + const [messages, setMessages] = useState([]) + const [inputMessage, setInputMessage] = useState("") + + useEffect(() => { + // Initialize Socket.IO client + const socketInstance = io() + + socketInstance.on("connect", () => { + console.log("Connected to Socket.IO:", socketInstance.id) + setIsConnected(true) + setMessages(prev => [...prev, `✅ Connected: ${socketInstance.id}`]) + }) + + socketInstance.on("disconnect", (reason) => { + console.log("Disconnected:", reason) + setIsConnected(false) + setMessages(prev => [...prev, `❌ Disconnected: ${reason}`]) + }) + + socketInstance.on("message", (data) => { + console.log("Message received:", data) + setMessages(prev => [...prev, `📩 Received: ${data}`]) + }) + + setSocket(socketInstance) + + return () => { + socketInstance.disconnect() + } + }, []) + + const sendMessage = () => { + if (socket && inputMessage.trim()) { + socket.emit("message", inputMessage) + setMessages(prev => [...prev, `📤 Sent: ${inputMessage}`]) + setInputMessage("") + } + } + + return ( + + + Socket.IO Test Client + + Status: {isConnected ? ( + 🟢 Connected + ) : ( + 🔴 Disconnected + )} + + + +
+ setInputMessage(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + disabled={!isConnected} + /> + +
+ +
+
+ {messages.length === 0 ? ( +

No messages yet...

+ ) : ( + messages.map((msg, idx) => ( +

{msg}

+ )) + )} +
+
+
+
+ ) +} + diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..3f9d035 --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,12 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" +import * as React from "react" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} + diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx deleted file mode 100644 index b432017..0000000 --- a/src/components/ui/accordion.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client" - -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import {ChevronDown} from "lucide-react" - -import {cn} from "@/lib/utils" - -const Accordion = AccordionPrimitive.Root - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, children, ...props}, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, children, ...props}, ref) => ( - -
{children}
-
-)) -AccordionContent.displayName = AccordionPrimitive.Content.displayName - -export {Accordion, AccordionItem, AccordionTrigger, AccordionContent} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 461c340..0000000 --- a/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import {cn} from "@/lib/utils" -import {buttonVariants} from "@/components/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - - - - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props - }: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props - }: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx deleted file mode 100644 index c382e3a..0000000 --- a/src/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import {cva, type VariantProps} from "class-variance-authority" - -import {cn} from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({className, variant, ...props}, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({className, ...props}, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({className, ...props}, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export {Alert, AlertTitle, AlertDescription} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx deleted file mode 100644 index 027d499..0000000 --- a/src/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import {cn} from "@/lib/utils" - -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName - -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName - -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName - -export {Avatar, AvatarImage, AvatarFallback} diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx new file mode 100644 index 0000000..8600af0 --- /dev/null +++ b/src/components/ui/button-group.tsx @@ -0,0 +1,83 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + } +) + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot : "div" + + return ( + + ) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e69ddcc..6810932 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,30 +1,33 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" import * as React from "react" -import {Slot} from "@radix-ui/react-slot" -import {cva, type VariantProps} from "class-variance-authority" -import {cn} from "@/lib/utils" +import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + "icon-xl": "size-12", }, }, defaultVariants: { @@ -34,24 +37,25 @@ const buttonVariants = cva( } ) -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) } -const Button = React.forwardRef( - ({className, variant, size, asChild = false, ...props}, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export {Button, buttonVariants} +export { Button, buttonVariants } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..6f304b5 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,216 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( +