diff --git a/.env.example b/.env.example index 1b057c22..a727b71f 100644 --- a/.env.example +++ b/.env.example @@ -1,46 +1,46 @@ -NEXTAUTH_URL=http://localhost:3000 -NEXT_PUBLIC_APP_URL=http://localhost:3000 -AUTH_SECRET= # https://generate-secret.vercel.app/32 - -COMMUNITIES_CONFIG_URL='' - -# Brevo -BREVO_API_KEY= -BREVO_SENDER_EMAIL= -BREVO_SENDER_NAME= - -# Server Account -SERVER_PRIVATE_KEY= -SERVER_ACCOUNT_ADDRESS= - -# Supabase top level -SUPABASE_URL=https://... -SUPABASE_ANON_KEY=... -SUPABASE_SERVICE_ROLE_KEY=... -SUPABASE_DB_PASSWORD= - -# Supabase for chain 100 (Gnosis Chain) -SUPABASE_100_URL=https://... -SUPABASE_100_ANON_KEY=... -SUPABASE_100_SERVICE_ROLE_KEY=... - -# Supabase for chain 137 (Polygon) -SUPABASE_137_URL=https://... -SUPABASE_137_ANON_KEY=... -SUPABASE_137_SERVICE_ROLE_KEY=... - -# Supabase for chain 42220 (Celo) -SUPABASE_42220_URL=https://... -SUPABASE_42220_ANON_KEY=... -SUPABASE_42220_SERVICE_ROLE_KEY=... -SERVER_42220_ACCOUNT_ADDRESS= -SERVER_42220_WALLET_PRIVATE_KEY= - - -# Supabase for chain 8453 (Base) -SUPABASE_8453_URL=https://... -SUPABASE_8453_ANON_KEY=... -SUPABASE_8453_SERVICE_ROLE_KEY=... - -# ONRAMP +NEXTAUTH_URL=http://localhost:3000 +NEXT_PUBLIC_APP_URL=http://localhost:3000 +AUTH_SECRET= # https://generate-secret.vercel.app/32 + +COMMUNITIES_CONFIG_URL='' + +# Brevo +BREVO_API_KEY= +BREVO_SENDER_EMAIL= +BREVO_SENDER_NAME= + +# Server Account +SERVER_PRIVATE_KEY= +SERVER_ACCOUNT_ADDRESS= + +# Supabase top level +SUPABASE_URL=https://... +SUPABASE_ANON_KEY=... +SUPABASE_SERVICE_ROLE_KEY=... +SUPABASE_DB_PASSWORD= + +# Supabase for chain 100 (Gnosis Chain) +SUPABASE_100_URL=https://... +SUPABASE_100_ANON_KEY=... +SUPABASE_100_SERVICE_ROLE_KEY=... + +# Supabase for chain 137 (Polygon) +SUPABASE_137_URL=https://... +SUPABASE_137_ANON_KEY=... +SUPABASE_137_SERVICE_ROLE_KEY=... + +# Supabase for chain 42220 (Celo) +SUPABASE_42220_URL=https://... +SUPABASE_42220_ANON_KEY=... +SUPABASE_42220_SERVICE_ROLE_KEY=... +SERVER_42220_ACCOUNT_ADDRESS= +SERVER_42220_WALLET_PRIVATE_KEY= + + +# Supabase for chain 8453 (Base) +SUPABASE_8453_URL=https://... +SUPABASE_8453_ANON_KEY=... +SUPABASE_8453_SERVICE_ROLE_KEY=... + +# ONRAMP TRANSAK_API_KEY=... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 961cdbcb..f7091ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,37 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* -!.env.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +config.bat diff --git a/app/(home)/_table/communities-table.tsx b/app/(home)/_table/communities-table.tsx index 53707ce4..f0ead667 100644 --- a/app/(home)/_table/communities-table.tsx +++ b/app/(home)/_table/communities-table.tsx @@ -1,51 +1,51 @@ -import { columns } from './columns'; -import { DataTable } from '@/components/ui/data-table'; -import { Config } from '@citizenwallet/sdk'; -import { fetchCommunitiesForAdminAction } from '@/app/_actions/community-actions'; -import { Separator } from '@/components/ui/separator'; - -interface CommunitiesTableProps { - query: string; - page: number; -} - -export async function CommunitiesTable({ query }: CommunitiesTableProps) { - let communities: Config[] = []; - let total: number = 0; - - try { - const result = await fetchCommunitiesForAdminAction({ - query: query - }); - - communities = result.communities; - total = result.total; - } catch (error) { - console.error(error); - } - - return ( -
-
-
-

Communities

-

Browse communities

-
-
- -
-
- -
-
- - - -
-

- Total: {total} -

-
-
- ); -} +import { columns } from './columns'; +import { DataTable } from '@/components/ui/data-table'; +import { Config } from '@citizenwallet/sdk'; +import { fetchCommunitiesForAdminAction } from '@/app/_actions/community-actions'; +import { Separator } from '@/components/ui/separator'; + +interface CommunitiesTableProps { + query: string; + page: number; +} + +export async function CommunitiesTable({ query }: CommunitiesTableProps) { + let communities: Config[] = []; + let total: number = 0; + + try { + const result = await fetchCommunitiesForAdminAction({ + query: query + }); + + communities = result.communities; + total = result.total; + } catch (error) { + console.error(error); + } + + return ( +
+
+
+

Communities

+

Browse communities

+
+
+ +
+
+ +
+
+ + + +
+

+ Total: {total} +

+
+
+ ); +} diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index cbc9a974..df6aeafa 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -1,37 +1,37 @@ -import { Suspense } from 'react'; -import { skeletonColumns } from './_table/columns'; -import { DataTable } from '@/components/ui/data-table'; -import { CommunitiesTable } from './_table/communities-table'; -import { placeholderData } from './_table/columns'; - -export default async function Page(props: { - searchParams: Promise<{ query?: string; page?: string }>; -}) { - const { query: queryParam, page: pageParam } = await props.searchParams; - const query = queryParam || ''; - const page = pageParam || '1'; - - return ( - }> - - - ); -} - -function Fallback() { - return ( -
-
-
-

Communities

-
-
- -
-
- -
-
-
- ); -} +import { Suspense } from 'react'; +import { skeletonColumns } from './_table/columns'; +import { DataTable } from '@/components/ui/data-table'; +import { CommunitiesTable } from './_table/communities-table'; +import { placeholderData } from './_table/columns'; + +export default async function Page(props: { + searchParams: Promise<{ query?: string; page?: string }>; +}) { + const { query: queryParam, page: pageParam } = await props.searchParams; + const query = queryParam || ''; + const page = pageParam || '1'; + + return ( + }> + + + ); +} + +function Fallback() { + return ( +
+
+
+

Communities

+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/app/(home)/user.tsx b/app/(home)/user.tsx index e32e9a91..448387c1 100644 --- a/app/(home)/user.tsx +++ b/app/(home)/user.tsx @@ -1,65 +1,65 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; -import { signOutAction } from '@/app/_actions/user-actions'; -import { UserT } from '@/services/top-db/users'; -import { Avatar, AvatarImage, AvatarFallback } from '@radix-ui/react-avatar'; - -interface UserProps { - user: UserT | null; -} - -export default function User(props: UserProps) { - const { user } = props; - - const getInitials = (name?: string) => { - if (!name) return 'U'; - const nameParts = name.split(' '); - if (nameParts.length >= 2) { - return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase(); - } - return name.slice(0, 2).toUpperCase(); - }; - - return ( - - - - - - -
{ - await signOutAction(); - }} - > - -
-
-
-
- ); -} +'use client'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { signOutAction } from '@/app/_actions/user-actions'; +import { UserT } from '@/services/top-db/users'; +import { Avatar, AvatarImage, AvatarFallback } from '@radix-ui/react-avatar'; + +interface UserProps { + user: UserT | null; +} + +export default function User(props: UserProps) { + const { user } = props; + + const getInitials = (name?: string) => { + if (!name) return 'U'; + const nameParts = name.split(' '); + if (nameParts.length >= 2) { + return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); + }; + + return ( + + + + + + +
{ + await signOutAction(); + }} + > + +
+
+
+
+ ); +} diff --git a/app/[alias]/(dashboard)/_components/app-sidebar.tsx b/app/[alias]/(dashboard)/_components/app-sidebar.tsx index 597d6634..8f416e65 100644 --- a/app/[alias]/(dashboard)/_components/app-sidebar.tsx +++ b/app/[alias]/(dashboard)/_components/app-sidebar.tsx @@ -1,143 +1,154 @@ -'use client'; - -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarMenuButton, - SidebarRail -} from '@/components/ui/sidebar'; -import { UserT } from '@/services/top-db/users'; -import { Config } from '@citizenwallet/sdk'; -import { - ArrowLeft, - Hammer, - Home, - Landmark, - List, - LucideLineChart, - Shield, - Users, - Webhook, - Wrench -} from 'lucide-react'; -import Link from 'next/link'; -import type * as React from 'react'; -import { CommunitySwitcher } from './community-switcher'; -import { NavProjects } from './nav-projects'; -import { NavUser } from './nav-user'; - -interface AppSidebarProps extends React.ComponentProps { - communities: Config[]; - config: Config; - user: UserT | null; - hasAccess: boolean; -} - -export function AppSidebar({ - communities, - config, - user, - hasAccess, - ...props -}: AppSidebarProps) { - const data = { - user: { - name: user?.name ?? '', - email: user?.email ?? '', - avatar: user?.avatar ?? '' - }, - projects: [ - { - name: 'Overview', - url: `/${config?.community.alias}`, - icon: Home - }, - { - name: 'Members', - url: `/${config?.community.alias}/members`, - icon: Users - }, - { - name: 'Transfers', - url: `/${config?.community.alias}/transfers`, - icon: LucideLineChart - }, - { - name: 'Treasury', - url: `/${config?.community.alias}/treasury`, - icon: Landmark, - items: [ - { - name: 'History', - url: `/${config?.community.alias}/treasury`, - icon: List - }, - { - name: 'Minters', - url: `/${config?.community.alias}/roles`, - icon: Hammer - } - ] - }, - { - name: 'Admins', - url: `/${config?.community.alias}/admins`, - icon: Shield - }, - { - name: 'Developer', - url: `/${config?.community.alias}`, - icon: Wrench, - items: [ - { - name: 'Webhooks', - url: `/${config?.community.alias}/webhooks`, - icon: Webhook - } - ] - } - ] - }; - - return ( - - - - - - - project.name == 'Overview') - } - /> - - - - - - - ); -} - -function BackToAllCommunities() { - const handleClick = () => { - document.cookie = `lastViewedAlias=; path=/; max-age=0`; - }; - - return ( - - - - Home - - - ); -} +'use client'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenuButton, + SidebarRail +} from '@/components/ui/sidebar'; +import { UserT } from '@/services/top-db/users'; +import { Config } from '@citizenwallet/sdk'; +import { + ArrowLeft, + CreditCard, + Hammer, + Home, + Landmark, + List, + LucideLineChart, + Shield, + Users, + Webhook, + Wrench +} from 'lucide-react'; +import Link from 'next/link'; +import type * as React from 'react'; +import { CommunitySwitcher } from './community-switcher'; +import { NavProjects } from './nav-projects'; +import { NavUser } from './nav-user'; + +interface AppSidebarProps extends React.ComponentProps { + communities: Config[]; + config: Config; + user: UserT | null; + hasAccess: boolean; + isAdmin: boolean; +} + +export function AppSidebar({ + communities, + config, + user, + hasAccess, + isAdmin, + ...props +}: AppSidebarProps) { + + + const data = { + user: { + name: user?.name ?? '', + email: user?.email ?? '', + avatar: user?.avatar ?? '' + }, + projects: [ + { + name: 'Overview', + url: `/${config?.community.alias}`, + icon: Home + }, + { + name: 'Members', + url: `/${config?.community.alias}/members`, + icon: Users + }, + { + name: 'Transfers', + url: `/${config?.community.alias}/transfers`, + icon: LucideLineChart + }, + { + name: 'Treasury', + url: `/${config?.community.alias}/treasury`, + icon: Landmark, + items: [ + { + name: 'History', + url: `/${config?.community.alias}/treasury`, + icon: List + }, + { + name: 'Minters', + url: `/${config?.community.alias}/roles`, + icon: Hammer + } + ] + }, + { + name: 'Admins', + url: `/${config?.community.alias}/admins`, + icon: Shield + }, + { + name: 'Developer', + url: `/${config?.community.alias}`, + icon: Wrench, + items: [ + { + name: 'Webhooks', + url: `/${config?.community.alias}/webhooks`, + icon: Webhook + }, + ...(isAdmin ? [{ + name: 'paymaster', + url: `/${config?.community.alias}/paymaster`, + icon: CreditCard + }] : []) + ] + }, + + ] + }; + + return ( + + + + + + + project.name == 'Overview') + } + /> + + + + + + + ); +} + +function BackToAllCommunities() { + const handleClick = () => { + document.cookie = `lastViewedAlias=; path=/; max-age=0`; + }; + + return ( + + + + Home + + + ); +} diff --git a/app/[alias]/(dashboard)/_components/community-switcher.tsx b/app/[alias]/(dashboard)/_components/community-switcher.tsx index a8ec2f95..c542ec46 100644 --- a/app/[alias]/(dashboard)/_components/community-switcher.tsx +++ b/app/[alias]/(dashboard)/_components/community-switcher.tsx @@ -1,118 +1,118 @@ -'use client'; - -import * as React from 'react'; -import { ChevronsUpDown } from 'lucide-react'; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar -} from '@/components/ui/sidebar'; -import { Config, CommunityConfig, ConfigToken } from '@citizenwallet/sdk'; -import { CommunityLogo } from '@/components/icons'; -import { useRouter } from 'next/navigation'; - -export function CommunitySwitcher({ - communities, - selectedCommunity -}: { - communities: Config[]; - selectedCommunity?: Config; -}) { - const { isMobile } = useSidebar(); - const [activeCommunity, setActiveCommunity] = - React.useState(selectedCommunity); - const router = useRouter(); - - if (!activeCommunity) { - return null; - } - - const communityConfig = new CommunityConfig(activeCommunity); - const primaryToken: ConfigToken = communityConfig.primaryToken; - const logo: string = communityConfig.community.logo; - - return ( - - - - - -
- -
-
- - {activeCommunity.community.name} - - - {activeCommunity.community.alias} - -
- -
-
- - - Communities - - {communities.map((community) => { - const communityConfig = new CommunityConfig(community); - const alias = communityConfig.community.alias; - const primaryToken = communityConfig.primaryToken; - const logo = communityConfig.community.logo; - - const onSelectCommunity = () => { - document.cookie = `lastViewedAlias=${alias}; path=/; max-age=31536000`; - setActiveCommunity(community); - router.push(`/${alias}`); - }; - - return ( - -
- -
- {community.community.name} -
- ); - })} - {/* */} - {/* -
- -
-
Add team
-
*/} -
-
-
-
- ); -} +'use client'; + +import * as React from 'react'; +import { ChevronsUpDown } from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar +} from '@/components/ui/sidebar'; +import { Config, CommunityConfig, ConfigToken } from '@citizenwallet/sdk'; +import { CommunityLogo } from '@/components/icons'; +import { useRouter } from 'next/navigation'; + +export function CommunitySwitcher({ + communities, + selectedCommunity +}: { + communities: Config[]; + selectedCommunity?: Config; +}) { + const { isMobile } = useSidebar(); + const [activeCommunity, setActiveCommunity] = + React.useState(selectedCommunity); + const router = useRouter(); + + if (!activeCommunity) { + return null; + } + + const communityConfig = new CommunityConfig(activeCommunity); + const primaryToken: ConfigToken = communityConfig.primaryToken; + const logo: string = communityConfig.community.logo; + + return ( + + + + + +
+ +
+
+ + {activeCommunity.community.name} + + + {activeCommunity.community.alias} + +
+ +
+
+ + + Communities + + {communities.map((community) => { + const communityConfig = new CommunityConfig(community); + const alias = communityConfig.community.alias; + const primaryToken = communityConfig.primaryToken; + const logo = communityConfig.community.logo; + + const onSelectCommunity = () => { + document.cookie = `lastViewedAlias=${alias}; path=/; max-age=31536000`; + setActiveCommunity(community); + router.push(`/${alias}`); + }; + + return ( + +
+ +
+ {community.community.name} +
+ ); + })} + {/* */} + {/* +
+ +
+
Add team
+
*/} +
+
+
+
+ ); +} diff --git a/app/[alias]/(dashboard)/_components/nav-projects.tsx b/app/[alias]/(dashboard)/_components/nav-projects.tsx index 1a58161c..3805057d 100644 --- a/app/[alias]/(dashboard)/_components/nav-projects.tsx +++ b/app/[alias]/(dashboard)/_components/nav-projects.tsx @@ -1,91 +1,91 @@ -'use client'; - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - SidebarGroup, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem -} from '@/components/ui/sidebar'; -import { ChevronRight, type LucideIcon } from 'lucide-react'; -import Link from 'next/link'; - -export function NavProjects({ - projects -}: { - projects: { - name: string; - url: string; - icon: LucideIcon; - items?: { - name: string; - url: string; - icon: LucideIcon; - }[]; - }[]; -}) { - return ( - - - - {projects.map((item) => ( - - - {item.items && item.items.length > 0 ? ( - - - - - {item.icon && } - {item.name} - - - - - - {item.items?.map((subItem) => ( - - - - - {subItem.name} - - - - ))} - - - - - ) : ( - - - - - - {item.name} - - - - - )} - - - ))} - - - - - ); +'use client'; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem +} from '@/components/ui/sidebar'; +import { ChevronRight, type LucideIcon } from 'lucide-react'; +import Link from 'next/link'; + +export function NavProjects({ + projects +}: { + projects: { + name: string; + url: string; + icon: LucideIcon; + items?: { + name: string; + url: string; + icon: LucideIcon; + }[]; + }[]; +}) { + return ( + + + + {projects.map((item) => ( + + + {item.items && item.items.length > 0 ? ( + + + + + {item.icon && } + {item.name} + + + + + + {item.items?.map((subItem) => ( + + + + + {subItem.name} + + + + ))} + + + + + ) : ( + + + + + + {item.name} + + + + + )} + + + ))} + + + + + ); } \ No newline at end of file diff --git a/app/[alias]/(dashboard)/_components/nav-user.tsx b/app/[alias]/(dashboard)/_components/nav-user.tsx index 848b2759..abf28f13 100644 --- a/app/[alias]/(dashboard)/_components/nav-user.tsx +++ b/app/[alias]/(dashboard)/_components/nav-user.tsx @@ -1,160 +1,160 @@ -'use client'; - -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar -} from '@/components/ui/sidebar'; -import { StorageService } from '@/services/storage'; -import { CommunityConfig, Config, revokeSession } from '@citizenwallet/sdk'; -import { Wallet } from "ethers"; -import { ChevronsUpDown, LogOut } from 'lucide-react'; -import { useSession as useNextAuthSession } from "next-auth/react"; -import { useRouter } from 'next/navigation'; -import { toast } from 'sonner'; -import { useSession } from 'state/session/action'; - -export function NavUser({ - user, - config -}: { - user: { - name: string; - email: string; - avatar: string; - }; - config?: Config; -}) { - const { isMobile } = useSidebar(); - const { data: session, update } = useNextAuthSession(); - const router = useRouter(); - const sessionActions = useSession(config as Config); - - - const removeSession = async () => { - - try { - - if (!config) { - return; - } - - const privateKey = sessionActions[1].storage.getKey("session_private_key"); - const account = await sessionActions[1].getAccountAddress(); - const communityConfig = new CommunityConfig(config); - const storageService = new StorageService(config.community.alias); - - if (!privateKey || !account) { - return; - } - - const signer = new Wallet(privateKey); - - const tx = await revokeSession({ - community: communityConfig, - signer, - account, - }); - - - if (!tx) { - toast.error("Signout failed"); - return; - } - sessionActions[1].clear(); - storageService.deleteKey("session_private_key"); - storageService.deleteKey("session_source_type"); - storageService.deleteKey("session_source_value"); - storageService.deleteKey("session_hash"); - - const removeChainIds = config?.community.profile.chain_id; - const updateChainIds = session?.user.chainIds?.filter((chainId: number) => chainId !== removeChainIds); - - //remove chainId from session - await update({ - chainIds: updateChainIds - }); - - toast.success("Signout successful"); - - } catch (error) { - console.error(error); - toast.error("Signout failed"); - - } finally { - router.push("/"); - } - - - } - - return ( - - - - - - - - - {user.name.slice(0, 2)} - - -
- {user.name} - {user.email} -
- -
-
- - -
- - - - {user.name.slice(0, 2)} - - -
- {user.name} - {user.email} -
-
-
- - - - - - - -
-
-
-
- ); -} +'use client'; + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar +} from '@/components/ui/sidebar'; +import { StorageService } from '@/services/storage'; +import { CommunityConfig, Config, revokeSession } from '@citizenwallet/sdk'; +import { Wallet } from "ethers"; +import { ChevronsUpDown, LogOut } from 'lucide-react'; +import { useSession as useNextAuthSession } from "next-auth/react"; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { useSession } from 'state/session/action'; + +export function NavUser({ + user, + config +}: { + user: { + name: string; + email: string; + avatar: string; + }; + config?: Config; +}) { + const { isMobile } = useSidebar(); + const { data: session, update } = useNextAuthSession(); + const router = useRouter(); + const sessionActions = useSession(config as Config); + + + const removeSession = async () => { + + try { + + if (!config) { + return; + } + + const privateKey = sessionActions[1].storage.getKey("session_private_key"); + const account = await sessionActions[1].getAccountAddress(); + const communityConfig = new CommunityConfig(config); + const storageService = new StorageService(config.community.alias); + + if (!privateKey || !account) { + return; + } + + const signer = new Wallet(privateKey); + + const tx = await revokeSession({ + community: communityConfig, + signer, + account, + }); + + + if (!tx) { + toast.error("Signout failed"); + return; + } + sessionActions[1].clear(); + storageService.deleteKey("session_private_key"); + storageService.deleteKey("session_source_type"); + storageService.deleteKey("session_source_value"); + storageService.deleteKey("session_hash"); + + const removeChainIds = config?.community.profile.chain_id; + const updateChainIds = session?.user.chainIds?.filter((chainId: number) => chainId !== removeChainIds); + + //remove chainId from session + await update({ + chainIds: updateChainIds + }); + + toast.success("Signout successful"); + + } catch (error) { + console.error(error); + toast.error("Signout failed"); + + } finally { + router.push("/"); + } + + + } + + return ( + + + + + + + + + {user.name.slice(0, 2)} + + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + + {user.name.slice(0, 2)} + + +
+ {user.name} + {user.email} +
+
+
+ + + + + + + +
+
+
+
+ ); +} diff --git a/app/[alias]/(dashboard)/admins/_table/admins-client-table.tsx b/app/[alias]/(dashboard)/admins/_table/admins-client-table.tsx index 1cb03b69..5efae925 100644 --- a/app/[alias]/(dashboard)/admins/_table/admins-client-table.tsx +++ b/app/[alias]/(dashboard)/admins/_table/admins-client-table.tsx @@ -1,59 +1,59 @@ -'use client'; - -import { DataTable } from '@/components/ui/data-table'; -import { - UserT, - UserCommunityAccessT, - CommunityAccessRoleT -} from '@/services/top-db/users'; - -import { createColumns } from './columns'; -import { useOptimistic, useTransition } from 'react'; -import { removeUserFromCommunityAction } from '@/app/[alias]/(dashboard)/admins/action'; - -interface AdminsClientTableProps { - data: (UserCommunityAccessT & { user: UserT })[]; - communityRole?: CommunityAccessRoleT; - alias: string; -} - -export function AdminsClientTable({ - data, - alias, - communityRole -}: AdminsClientTableProps) { - const [isPending, startTransition] = useTransition(); - const [optimisticAdmins, addOptimisticRemoval] = useOptimistic( - data, - (state, adminIdToRemove: number) => - state.filter((admin) => admin.user_id !== adminIdToRemove) - ); - - const handleRemoveAdmin = async (args: { userId: number }) => { - const { userId } = args; - - startTransition(async () => { - // Optimistically remove the admin from the UI - addOptimisticRemoval(userId); - - try { - await removeUserFromCommunityAction({ - userIdToRemove: userId, - alias: alias - }); - } catch (error) { - // If the removal fails, the state will automatically revert - console.error('Failed to remove admin:', error); - } - }); - }; - - const columns = createColumns({ - communityRole: communityRole, - alias: alias, - onRemoveAdmin: handleRemoveAdmin, - isPending: isPending - }); - - return ; -} +'use client'; + +import { DataTable } from '@/components/ui/data-table'; +import { + UserT, + UserCommunityAccessT, + CommunityAccessRoleT +} from '@/services/top-db/users'; + +import { createColumns } from './columns'; +import { useOptimistic, useTransition } from 'react'; +import { removeUserFromCommunityAction } from '@/app/[alias]/(dashboard)/admins/action'; + +interface AdminsClientTableProps { + data: (UserCommunityAccessT & { user: UserT })[]; + communityRole?: CommunityAccessRoleT; + alias: string; +} + +export function AdminsClientTable({ + data, + alias, + communityRole +}: AdminsClientTableProps) { + const [isPending, startTransition] = useTransition(); + const [optimisticAdmins, addOptimisticRemoval] = useOptimistic( + data, + (state, adminIdToRemove: number) => + state.filter((admin) => admin.user_id !== adminIdToRemove) + ); + + const handleRemoveAdmin = async (args: { userId: number }) => { + const { userId } = args; + + startTransition(async () => { + // Optimistically remove the admin from the UI + addOptimisticRemoval(userId); + + try { + await removeUserFromCommunityAction({ + userIdToRemove: userId, + alias: alias + }); + } catch (error) { + // If the removal fails, the state will automatically revert + console.error('Failed to remove admin:', error); + } + }); + }; + + const columns = createColumns({ + communityRole: communityRole, + alias: alias, + onRemoveAdmin: handleRemoveAdmin, + isPending: isPending + }); + + return ; +} diff --git a/app/[alias]/(dashboard)/admins/_table/admins-table.tsx b/app/[alias]/(dashboard)/admins/_table/admins-table.tsx index b0557b4d..3eacb0b7 100644 --- a/app/[alias]/(dashboard)/admins/_table/admins-table.tsx +++ b/app/[alias]/(dashboard)/admins/_table/admins-table.tsx @@ -1,70 +1,70 @@ -import UrlPagination from '@/components/custom/pagination-via-url'; -import { getUsersOfCommunityAction } from '@/app/[alias]/(dashboard)/admins/action'; -import { AdminsClientTable } from './admins-client-table'; -import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; -import { fetchCommunityByAliasAction } from '@/app/_actions/community-actions'; -import { Separator } from '@/components/ui/separator'; -import AddAdmin from '@/app/[alias]/(dashboard)/admins/_components/add-admin'; - -interface AdminsTableProps { - alias: string; -} - -export default async function AdminsTable({ alias }: AdminsTableProps) { - const { community: config } = await fetchCommunityByAliasAction(alias); - - const [usersResult, roleResult] = await Promise.allSettled([ - getUsersOfCommunityAction({ - alias - }), - getAuthUserRoleInCommunityAction({ - alias - }) - ]); - - const data = usersResult.status === 'fulfilled' ? usersResult.value.data : []; - - const totalCount = - usersResult.status === 'fulfilled' ? usersResult.value.count : 0; - - const communityRole = - roleResult.status === 'fulfilled' ? roleResult.value : undefined; - - const totalPages = 1; - - return ( -
-
-
-

Admins

-

{config.community.name}

-
- - {communityRole === 'owner' && ( -
- -
- )} -
- -
-
- -
-
- - - -
-

- Total: {totalCount} -

- -
-
- ); -} +import UrlPagination from '@/components/custom/pagination-via-url'; +import { getUsersOfCommunityAction } from '@/app/[alias]/(dashboard)/admins/action'; +import { AdminsClientTable } from './admins-client-table'; +import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; +import { fetchCommunityByAliasAction } from '@/app/_actions/community-actions'; +import { Separator } from '@/components/ui/separator'; +import AddAdmin from '@/app/[alias]/(dashboard)/admins/_components/add-admin'; + +interface AdminsTableProps { + alias: string; +} + +export default async function AdminsTable({ alias }: AdminsTableProps) { + const { community: config } = await fetchCommunityByAliasAction(alias); + + const [usersResult, roleResult] = await Promise.allSettled([ + getUsersOfCommunityAction({ + alias + }), + getAuthUserRoleInCommunityAction({ + alias + }) + ]); + + const data = usersResult.status === 'fulfilled' ? usersResult.value.data : []; + + const totalCount = + usersResult.status === 'fulfilled' ? usersResult.value.count : 0; + + const communityRole = + roleResult.status === 'fulfilled' ? roleResult.value : undefined; + + const totalPages = 1; + + return ( +
+
+
+

Admins

+

{config.community.name}

+
+ + {communityRole === 'owner' && ( +
+ +
+ )} +
+ +
+
+ +
+
+ + + +
+

+ Total: {totalCount} +

+ +
+
+ ); +} diff --git a/app/[alias]/(dashboard)/admins/_table/columns.tsx b/app/[alias]/(dashboard)/admins/_table/columns.tsx index ce83e974..b0832278 100644 --- a/app/[alias]/(dashboard)/admins/_table/columns.tsx +++ b/app/[alias]/(dashboard)/admins/_table/columns.tsx @@ -1,209 +1,209 @@ -'use client'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; - -import { - UserT, - UserCommunityAccessT, - CommunityAccessRoleT -} from '@/services/top-db/users'; -import { ColumnDef } from '@tanstack/react-table'; -import { Skeleton } from '@/components/ui/skeleton'; -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { Trash2 } from 'lucide-react'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog'; -import { useState } from 'react'; -import { toast } from 'sonner'; - -interface CreateColumnsProps { - communityRole?: CommunityAccessRoleT; - alias: string; - onRemoveAdmin: (args: { userId: number }) => Promise; - isPending: boolean; -} - -export const createColumns = ( - props: CreateColumnsProps -): ColumnDef[] => { - const baseColumns: ColumnDef[] = [ - { - header: 'Member', - cell: ({ row }) => { - const { user } = row.original; - const { avatar, name, email } = user; - return ( -
- - - {name?.slice(0, 2) ?? ''} - -
- {name} - {email} -
-
- ); - } - }, - { - header: 'Role', - accessorKey: 'role', - cell: ({ row }) => { - const role = row.original.role; - return ( -
- - {role} - -
- ); - } - }, - { - header: 'Created at', - accessorKey: 'created_at', - cell: ({ row }) => { - const createdAt = new Date(row.original.created_at); - return ( -
- - {createdAt.toLocaleString()} - -
- ); - } - } - ]; - - const ownerColumns: ColumnDef[] = [ - { - id: 'remove', - cell: function RemoveCell({ row }) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const { - user_id, - user: { name } - } = row.original; - - const handleOpenDialog = () => { - setIsDialogOpen(true); - }; - - const handleCloseDialog = () => { - setIsDialogOpen(false); - }; - - const onRemoveAdmin = async () => { - try { - await props.onRemoveAdmin({ - userId: user_id - }); - handleCloseDialog(); - toast.success('Admin removed successfully'); - } catch (error) { - if (error instanceof Error) { - toast.error(error.message); - } else { - toast.error('Could not remove admin'); - } - } - }; - - return ( - <> - - - - - - Remove Admin - - Are you sure you want to remove{' '} - {name} as an admin? - - - - - - - - - - - - ); - } - } - ]; - - return props.communityRole === 'owner' - ? [...baseColumns, ...ownerColumns] - : baseColumns; -}; - -export const skeletonColumns: ColumnDef< - UserCommunityAccessT & { admin: UserT } ->[] = [ - { - header: 'Member', - cell: () => ( -
- -
- {/* name */} - {/* email */} -
-
- ) - }, - { - header: 'Created at', - cell: () => ( -
- -
- ) - } -]; - -export const placeholderData: (UserCommunityAccessT & { admin: UserT })[] = - Array(5); +'use client'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +import { + UserT, + UserCommunityAccessT, + CommunityAccessRoleT +} from '@/services/top-db/users'; +import { ColumnDef } from '@tanstack/react-table'; +import { Skeleton } from '@/components/ui/skeleton'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Trash2 } from 'lucide-react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface CreateColumnsProps { + communityRole?: CommunityAccessRoleT; + alias: string; + onRemoveAdmin: (args: { userId: number }) => Promise; + isPending: boolean; +} + +export const createColumns = ( + props: CreateColumnsProps +): ColumnDef[] => { + const baseColumns: ColumnDef[] = [ + { + header: 'Member', + cell: ({ row }) => { + const { user } = row.original; + const { avatar, name, email } = user; + return ( +
+ + + {name?.slice(0, 2) ?? ''} + +
+ {name} + {email} +
+
+ ); + } + }, + { + header: 'Role', + accessorKey: 'role', + cell: ({ row }) => { + const role = row.original.role; + return ( +
+ + {role} + +
+ ); + } + }, + { + header: 'Created at', + accessorKey: 'created_at', + cell: ({ row }) => { + const createdAt = new Date(row.original.created_at); + return ( +
+ + {createdAt.toLocaleString()} + +
+ ); + } + } + ]; + + const ownerColumns: ColumnDef[] = [ + { + id: 'remove', + cell: function RemoveCell({ row }) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const { + user_id, + user: { name } + } = row.original; + + const handleOpenDialog = () => { + setIsDialogOpen(true); + }; + + const handleCloseDialog = () => { + setIsDialogOpen(false); + }; + + const onRemoveAdmin = async () => { + try { + await props.onRemoveAdmin({ + userId: user_id + }); + handleCloseDialog(); + toast.success('Admin removed successfully'); + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } else { + toast.error('Could not remove admin'); + } + } + }; + + return ( + <> + + + + + + Remove Admin + + Are you sure you want to remove{' '} + {name} as an admin? + + + + + + + + + + + + ); + } + } + ]; + + return props.communityRole === 'owner' + ? [...baseColumns, ...ownerColumns] + : baseColumns; +}; + +export const skeletonColumns: ColumnDef< + UserCommunityAccessT & { admin: UserT } +>[] = [ + { + header: 'Member', + cell: () => ( +
+ +
+ {/* name */} + {/* email */} +
+
+ ) + }, + { + header: 'Created at', + cell: () => ( +
+ +
+ ) + } +]; + +export const placeholderData: (UserCommunityAccessT & { admin: UserT })[] = + Array(5); diff --git a/app/[alias]/(dashboard)/admins/add/actions.ts b/app/[alias]/(dashboard)/admins/add/actions.ts index 6abeb546..9fa54630 100644 --- a/app/[alias]/(dashboard)/admins/add/actions.ts +++ b/app/[alias]/(dashboard)/admins/add/actions.ts @@ -1,123 +1,123 @@ -'use server'; - -import { getServiceRoleClient as getTopDbClient } from '@/services/top-db'; -import { z } from 'zod'; -import { inviteAdminFormSchema } from './form-schema'; - -import { addUserToApp, addUserToCommunity } from '@/services/top-db/users'; -import { saveOTP } from '@/services/top-db/otp'; - -import { sendCommunityInvitationEmail } from '@/services/brevo'; -import { generateOTP } from '@/lib/utils'; -import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; -import { Config } from '@citizenwallet/sdk'; -import { revalidatePath } from 'next/cache'; - -export async function submitAdminInvitation(args: { - formData: z.infer; - chainId: number; -}) { - const { formData, chainId } = args; - - const result = inviteAdminFormSchema.safeParse(formData); - - if (!result.success) { - console.error(result.error); - throw new Error('Invalid form data'); - } - - const userRole = await getAuthUserRoleInCommunityAction({ - alias: formData.alias - }); - - if (userRole !== 'owner') { - throw new Error('You are not authorized to add admins to this community'); - } - - const { email, name, avatar, alias, role } = result.data; - - const topDbClient = getTopDbClient(); - - const { data: user, error: addUserToAppError } = await addUserToApp({ - client: topDbClient, - data: { - email, - name, - avatar: avatar ?? null - } - }); - - if (addUserToAppError) { - console.error(addUserToAppError); - throw new Error('Failed to add user to app'); - } - - const { error: addUserToCommunityError } = await addUserToCommunity({ - client: topDbClient, - data: { - user_id: user.id, - alias, - role, - chain_id: chainId - } - }); - - if (addUserToCommunityError) { - console.error(addUserToCommunityError); - throw new Error('Failed to add user to community'); - } - - revalidatePath(`/${alias}/admins`); -} - -export async function sendAdminSignInInvitationAction(args: { - email: string; - config: Config; -}) { - const { email, config } = args; - - const { alias: communityAlias, name: communityName } = config.community; - - const userRole = await getAuthUserRoleInCommunityAction({ - alias: communityAlias - }); - - if (userRole !== 'owner') { - throw new Error('You are not authorized to add admins to this community'); - } - - const topDbClient = getTopDbClient(); - const otp = generateOTP(); - - // brevo - try { - sendCommunityInvitationEmail({ - email, - otp, - communityAlias, - communityName - }); - } catch (error) { - console.error(error); - // Preserve the original error message - if (error instanceof Error) { - throw error; - } - throw new Error('Failed to send invitation email'); - } - - // db - const { error: saveOTPError } = await saveOTP({ - client: topDbClient, - data: { - source: email, - code: otp, - source_type: 'email' - } - }); - - if (saveOTPError) { - console.error(saveOTPError); - throw new Error('Failed to save OTP'); - } -} +'use server'; + +import { getServiceRoleClient as getTopDbClient } from '@/services/top-db'; +import { z } from 'zod'; +import { inviteAdminFormSchema } from './form-schema'; + +import { addUserToApp, addUserToCommunity } from '@/services/top-db/users'; +import { saveOTP } from '@/services/top-db/otp'; + +import { sendCommunityInvitationEmail } from '@/services/brevo'; +import { generateOTP } from '@/lib/utils'; +import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; +import { Config } from '@citizenwallet/sdk'; +import { revalidatePath } from 'next/cache'; + +export async function submitAdminInvitation(args: { + formData: z.infer; + chainId: number; +}) { + const { formData, chainId } = args; + + const result = inviteAdminFormSchema.safeParse(formData); + + if (!result.success) { + console.error(result.error); + throw new Error('Invalid form data'); + } + + const userRole = await getAuthUserRoleInCommunityAction({ + alias: formData.alias + }); + + if (userRole !== 'owner') { + throw new Error('You are not authorized to add admins to this community'); + } + + const { email, name, avatar, alias, role } = result.data; + + const topDbClient = getTopDbClient(); + + const { data: user, error: addUserToAppError } = await addUserToApp({ + client: topDbClient, + data: { + email, + name, + avatar: avatar ?? null + } + }); + + if (addUserToAppError) { + console.error(addUserToAppError); + throw new Error('Failed to add user to app'); + } + + const { error: addUserToCommunityError } = await addUserToCommunity({ + client: topDbClient, + data: { + user_id: user.id, + alias, + role, + chain_id: chainId + } + }); + + if (addUserToCommunityError) { + console.error(addUserToCommunityError); + throw new Error('Failed to add user to community'); + } + + revalidatePath(`/${alias}/admins`); +} + +export async function sendAdminSignInInvitationAction(args: { + email: string; + config: Config; +}) { + const { email, config } = args; + + const { alias: communityAlias, name: communityName } = config.community; + + const userRole = await getAuthUserRoleInCommunityAction({ + alias: communityAlias + }); + + if (userRole !== 'owner') { + throw new Error('You are not authorized to add admins to this community'); + } + + const topDbClient = getTopDbClient(); + const otp = generateOTP(); + + // brevo + try { + sendCommunityInvitationEmail({ + email, + otp, + communityAlias, + communityName + }); + } catch (error) { + console.error(error); + // Preserve the original error message + if (error instanceof Error) { + throw error; + } + throw new Error('Failed to send invitation email'); + } + + // db + const { error: saveOTPError } = await saveOTP({ + client: topDbClient, + data: { + source: email, + code: otp, + source_type: 'email' + } + }); + + if (saveOTPError) { + console.error(saveOTPError); + throw new Error('Failed to save OTP'); + } +} diff --git a/app/[alias]/(dashboard)/admins/add/page.tsx b/app/[alias]/(dashboard)/admins/add/page.tsx index 219072b1..e0b15af0 100644 --- a/app/[alias]/(dashboard)/admins/add/page.tsx +++ b/app/[alias]/(dashboard)/admins/add/page.tsx @@ -1,40 +1,40 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from '@/components/ui/card'; -import InviteAdminForm from './form'; -import { fetchCommunityByAliasAction } from '@/app/_actions/community-actions'; -import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; -import { redirect } from 'next/navigation'; - -export default async function Page(props: { - params: Promise<{ alias: string }>; -}) { - const { alias } = await props.params; - const { community: config } = await fetchCommunityByAliasAction(alias); - - const authRole = await getAuthUserRoleInCommunityAction({ - alias - }); - - if (authRole !== 'owner') { - redirect(`/${alias}/admins`); - } - - return ( -
- - - Invite admin - Invite an admin to your community - - - - - -
- ); -} +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from '@/components/ui/card'; +import InviteAdminForm from './form'; +import { fetchCommunityByAliasAction } from '@/app/_actions/community-actions'; +import { getAuthUserRoleInCommunityAction } from '@/app/_actions/user-actions'; +import { redirect } from 'next/navigation'; + +export default async function Page(props: { + params: Promise<{ alias: string }>; +}) { + const { alias } = await props.params; + const { community: config } = await fetchCommunityByAliasAction(alias); + + const authRole = await getAuthUserRoleInCommunityAction({ + alias + }); + + if (authRole !== 'owner') { + redirect(`/${alias}/admins`); + } + + return ( +
+ + + Invite admin + Invite an admin to your community + + + + + +
+ ); +} diff --git a/app/[alias]/(dashboard)/layout.tsx b/app/[alias]/(dashboard)/layout.tsx index 892577cd..4025521e 100644 --- a/app/[alias]/(dashboard)/layout.tsx +++ b/app/[alias]/(dashboard)/layout.tsx @@ -1,71 +1,87 @@ -import { - SidebarInset, - SidebarProvider, - SidebarTrigger -} from '@/components/ui/sidebar'; -import { AppSidebar } from './_components/app-sidebar'; -import { fetchCommunitiesAction } from '@/app/_actions/community-actions'; -import { redirect } from 'next/navigation'; -import { getAuthUserAction } from '@/app/_actions/user-actions'; -import { getCommunity } from '@/services/cw'; - -export default async function DashboardLayout({ - children, - params -}: { - children: React.ReactNode; - params: Promise<{ alias: string }>; -}) { - const { alias } = await params; - let hasAccess = false; - - - const { community } = await getCommunity(alias); - const community_chain_id = community.community.primary_token.chain_id; - - const response = await getAuthUserAction({ chain_id: community_chain_id }); - if (!response || !response.data) { - redirect(`/${alias}/login`); - } - const { data: user } = response; - - - const { role } = user; - const accessList = user?.users_community_access.map((access) => access) ?? []; - - if (role === 'user' && accessList.length < 1) { - redirect('/'); - } - - - if (role === 'user' && accessList.length > 0) { - const chain_id = accessList[0].chain_id; - - if (chain_id == community_chain_id && accessList[0].alias == alias) { - hasAccess = true; - } - } - - if (role === 'admin') { - hasAccess = true; - } - - - const { communities } = await fetchCommunitiesAction({ alias }); - - return ( - - - -
-
- -
-
-
- {children} -
-
-
- ); -} +import { + SidebarInset, + SidebarProvider, + SidebarTrigger +} from '@/components/ui/sidebar'; +import { AppSidebar } from './_components/app-sidebar'; +import { fetchCommunitiesAction } from '@/app/_actions/community-actions'; +import { redirect } from 'next/navigation'; +import { getAuthUserAction } from '@/app/_actions/user-actions'; +import { getCommunity } from '@/services/cw'; + +export default async function DashboardLayout({ + children, + params +}: { + children: React.ReactNode; + params: Promise<{ alias: string }>; +}) { + const { alias } = await params; + let hasAccess = false; + let isAdmin = false; + + + const { community } = await getCommunity(alias); + const community_chain_id = community.community.primary_token.chain_id; + + const response = await getAuthUserAction({ chain_id: community_chain_id }); + if (!response || !response.data) { + redirect(`/${alias}/login`); + } + const { data: user } = response; + + + const { role } = user; + const accessList = user?.users_community_access.map((access) => access) ?? []; + + if (role === 'user' && accessList.length < 1) { + redirect('/'); + } + + + if (role === 'user' && accessList.length > 0) { + const chain_id = accessList[0].chain_id; + + if (chain_id == community_chain_id && accessList[0].alias == alias) { + hasAccess = true; + } + } + + if (role === 'user' && accessList.length > 0) { + accessList.forEach(access => { + if (access.alias === alias && access.role === 'owner') { + isAdmin = true; + } + }); + } + + if (role === 'admin') { + hasAccess = true; + isAdmin = true; + } + + + const { communities } = await fetchCommunitiesAction({ alias }); + + return ( + + + +
+
+ +
+
+
+ {children} +
+
+
+ ); +} diff --git a/app/[alias]/(dashboard)/members/[account]/action.ts b/app/[alias]/(dashboard)/members/[account]/action.ts index a3829cb5..88e7e0f0 100644 --- a/app/[alias]/(dashboard)/members/[account]/action.ts +++ b/app/[alias]/(dashboard)/members/[account]/action.ts @@ -1,156 +1,156 @@ -'use server'; - -import { - getAuthUserRoleInAppAction, - getAuthUserRoleInCommunityAction -} from '@/app/_actions/user-actions'; -import { pinFileToIPFS, pinJSONToIPFS, unpin } from '@/services/pinata/pinata'; -import { - BundlerService, - CommunityConfig, - Config, - waitForTxSuccess -} from '@citizenwallet/sdk'; -import { Wallet } from 'ethers'; -import { getServiceRoleClient } from '@/services/chain-db'; -import { - MemberT, - removeMember, - updateMember -} from '@/services/chain-db/members'; -import { revalidatePath } from 'next/cache'; - -export type Profile = Pick< - MemberT, - | 'account' - | 'description' - | 'image' - | 'image_medium' - | 'image_small' - | 'name' - | 'username' ->; - -function convertIpfsUrl(ipfsUrl: string) { - if (ipfsUrl.startsWith('ipfs://')) { - return ipfsUrl.replace( - 'ipfs://', - 'https://ipfs.internal.citizenwallet.xyz/' - ); - } - return ipfsUrl; -} - -export async function updateProfileImageAction(file: File, alias: string) { - const roleInApp = await getAuthUserRoleInAppAction(); - const roleResult = await getAuthUserRoleInCommunityAction({ alias }); - - if (roleInApp != 'admin' && roleResult != 'owner') { - throw new Error('You are not authorized to update profile image'); - } - - const result = await pinFileToIPFS(file); - return result; -} - -export async function updateProfileAction( - profile: Profile, - alias: string, - config: Config -) { - const roleInApp = await getAuthUserRoleInAppAction(); - const roleResult = await getAuthUserRoleInCommunityAction({ alias }); - - if (roleInApp != 'admin' && roleResult != 'owner') { - throw new Error('You are not authorized to update profile image'); - } - - const result = await pinJSONToIPFS(profile); - const profileCid = result.IpfsHash; - - const community = new CommunityConfig(config); - const bundler = new BundlerService(community); - - const signer = new Wallet(process.env.SERVER_PRIVATE_KEY as string); - const signerAccountAddress = process.env.SERVER_ACCOUNT_ADDRESS as string; - - const account = profile.account; - const username = profile.username; - - const txHash = await bundler.setProfile( - signer, - signerAccountAddress, - account, - username, - profileCid - ); - const isSuccess = await waitForTxSuccess(community, txHash); - - if (isSuccess) { - const supabase = getServiceRoleClient(config.community.profile.chain_id); - const profileContract = config.community.profile.address; - - //convert ipfs url to https url - profile.image = convertIpfsUrl(profile.image); - profile.image_medium = convertIpfsUrl(profile.image_medium); - profile.image_small = convertIpfsUrl(profile.image_small); - - await updateMember({ - client: supabase, - account, - profileContract, - profile - }); - - revalidatePath(`/${alias}/members`, 'page'); - } -} - -export async function deleteProfileAction( - imageCid: string, - alias: string, - config: Config, - account: string -) { - const roleInApp = await getAuthUserRoleInAppAction(); - const roleResult = await getAuthUserRoleInCommunityAction({ alias }); - - if (roleInApp != 'admin' && roleResult != 'owner') { - throw new Error('You are not authorized to update profile image'); - } - - try { - //unpin profile image - const cid = imageCid.split('/').pop(); - await unpin(cid as string); - - const community = new CommunityConfig(config); - const bundler = new BundlerService(community); - - const signer = new Wallet(process.env.SERVER_PRIVATE_KEY as string); - const signerAccountAddress = process.env.SERVER_ACCOUNT_ADDRESS as string; - - const txHash = await bundler.burnProfile( - signer, - signerAccountAddress, - account - ); - - const isSuccess = await waitForTxSuccess(community, txHash); - - if (isSuccess) { - const supabase = getServiceRoleClient(config.community.profile.chain_id); - const profileContract = config.community.profile.address; - - await removeMember({ - client: supabase, - account, - profileContract - }); - - revalidatePath(`/${alias}/members`, 'page'); - } - } catch (error) { - console.error(error); - } -} +'use server'; + +import { + getAuthUserRoleInAppAction, + getAuthUserRoleInCommunityAction +} from '@/app/_actions/user-actions'; +import { pinFileToIPFS, pinJSONToIPFS, unpin } from '@/services/pinata/pinata'; +import { + BundlerService, + CommunityConfig, + Config, + waitForTxSuccess +} from '@citizenwallet/sdk'; +import { Wallet } from 'ethers'; +import { getServiceRoleClient } from '@/services/chain-db'; +import { + MemberT, + removeMember, + updateMember +} from '@/services/chain-db/members'; +import { revalidatePath } from 'next/cache'; + +export type Profile = Pick< + MemberT, + | 'account' + | 'description' + | 'image' + | 'image_medium' + | 'image_small' + | 'name' + | 'username' +>; + +function convertIpfsUrl(ipfsUrl: string) { + if (ipfsUrl.startsWith('ipfs://')) { + return ipfsUrl.replace( + 'ipfs://', + 'https://ipfs.internal.citizenwallet.xyz/' + ); + } + return ipfsUrl; +} + +export async function updateProfileImageAction(file: File, alias: string) { + const roleInApp = await getAuthUserRoleInAppAction(); + const roleResult = await getAuthUserRoleInCommunityAction({ alias }); + + if (roleInApp != 'admin' && roleResult != 'owner') { + throw new Error('You are not authorized to update profile image'); + } + + const result = await pinFileToIPFS(file); + return result; +} + +export async function updateProfileAction( + profile: Profile, + alias: string, + config: Config +) { + const roleInApp = await getAuthUserRoleInAppAction(); + const roleResult = await getAuthUserRoleInCommunityAction({ alias }); + + if (roleInApp != 'admin' && roleResult != 'owner') { + throw new Error('You are not authorized to update profile image'); + } + + const result = await pinJSONToIPFS(profile); + const profileCid = result.IpfsHash; + + const community = new CommunityConfig(config); + const bundler = new BundlerService(community); + + const signer = new Wallet(process.env.SERVER_PRIVATE_KEY as string); + const signerAccountAddress = process.env.SERVER_ACCOUNT_ADDRESS as string; + + const account = profile.account; + const username = profile.username; + + const txHash = await bundler.setProfile( + signer, + signerAccountAddress, + account, + username, + profileCid + ); + const isSuccess = await waitForTxSuccess(community, txHash); + + if (isSuccess) { + const supabase = getServiceRoleClient(config.community.profile.chain_id); + const profileContract = config.community.profile.address; + + //convert ipfs url to https url + profile.image = convertIpfsUrl(profile.image); + profile.image_medium = convertIpfsUrl(profile.image_medium); + profile.image_small = convertIpfsUrl(profile.image_small); + + await updateMember({ + client: supabase, + account, + profileContract, + profile + }); + + revalidatePath(`/${alias}/members`, 'page'); + } +} + +export async function deleteProfileAction( + imageCid: string, + alias: string, + config: Config, + account: string +) { + const roleInApp = await getAuthUserRoleInAppAction(); + const roleResult = await getAuthUserRoleInCommunityAction({ alias }); + + if (roleInApp != 'admin' && roleResult != 'owner') { + throw new Error('You are not authorized to update profile image'); + } + + try { + //unpin profile image + const cid = imageCid.split('/').pop(); + await unpin(cid as string); + + const community = new CommunityConfig(config); + const bundler = new BundlerService(community); + + const signer = new Wallet(process.env.SERVER_PRIVATE_KEY as string); + const signerAccountAddress = process.env.SERVER_ACCOUNT_ADDRESS as string; + + const txHash = await bundler.burnProfile( + signer, + signerAccountAddress, + account + ); + + const isSuccess = await waitForTxSuccess(community, txHash); + + if (isSuccess) { + const supabase = getServiceRoleClient(config.community.profile.chain_id); + const profileContract = config.community.profile.address; + + await removeMember({ + client: supabase, + account, + profileContract + }); + + revalidatePath(`/${alias}/members`, 'page'); + } + } catch (error) { + console.error(error); + } +} diff --git a/app/[alias]/(dashboard)/members/[account]/add/page.tsx b/app/[alias]/(dashboard)/members/[account]/add/page.tsx index 5da17789..4595c631 100644 --- a/app/[alias]/(dashboard)/members/[account]/add/page.tsx +++ b/app/[alias]/(dashboard)/members/[account]/add/page.tsx @@ -1,51 +1,51 @@ -import { Skeleton } from '@/components/ui/skeleton'; -import { getCommunity } from '@/services/cw'; -import { Config } from '@citizenwallet/sdk'; -import { Suspense } from 'react'; -import Profile from './profile'; - -interface PageProps { - params: Promise<{ - account: string; - alias: string; - }>; -} - -export default async function page(props: PageProps) { - const { account, alias } = await props.params; - const { community: config } = await getCommunity(alias); - - return ( -
-
-
-

Member Profile

-

- {config.community.name} -

-
-
- -
-
- } - > - - -
-
-
- ); -} - -async function AsyncPage({ - config, - account -}: { - config: Config; - account: string; -}) { - return ; -} +import { Skeleton } from '@/components/ui/skeleton'; +import { getCommunity } from '@/services/cw'; +import { Config } from '@citizenwallet/sdk'; +import { Suspense } from 'react'; +import Profile from './profile'; + +interface PageProps { + params: Promise<{ + account: string; + alias: string; + }>; +} + +export default async function page(props: PageProps) { + const { account, alias } = await props.params; + const { community: config } = await getCommunity(alias); + + return ( +
+
+
+

Member Profile

+

+ {config.community.name} +

+
+
+ +
+
+ } + > + + +
+
+
+ ); +} + +async function AsyncPage({ + config, + account +}: { + config: Config; + account: string; +}) { + return ; +} diff --git a/app/[alias]/(dashboard)/members/[account]/add/profile.tsx b/app/[alias]/(dashboard)/members/[account]/add/profile.tsx index 92c9aefb..3754d048 100644 --- a/app/[alias]/(dashboard)/members/[account]/add/profile.tsx +++ b/app/[alias]/(dashboard)/members/[account]/add/profile.tsx @@ -1,264 +1,264 @@ -'use client'; - -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardFooter } from '@/components/ui/card'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { - CommunityConfig, - Config, - checkUsernameAvailability -} from '@citizenwallet/sdk'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Upload, User } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { useDebounce } from 'use-debounce'; -import * as z from 'zod'; -import type { Profile } from '../action'; -import { updateProfileAction, updateProfileImageAction } from '../action'; - -const profileFormSchema = z.object({ - username: z.string().min(1, 'Username is required'), - name: z.string().optional(), - description: z.string().optional() -}); - -export default function Profile({ - config, - account -}: { - config: Config; - account: string; -}) { - const community = useMemo(() => new CommunityConfig(config), [config]); - const router = useRouter(); - - const [imageFile, setImageFile] = useState(null); - const [avatarUrl, setAvatarUrl] = useState(''); - const [isAvailable, setIsAvailable] = useState(true); - const [usernameEdit, setUsernameEdit] = useState(false); - const fileInputRef = useRef(null); - const [isLoading, setIsLoading] = useState(false); - - const form = useForm>({ - resolver: zodResolver(profileFormSchema), - defaultValues: { - username: '', - name: '', - description: '' - } - }); - - const username = form.watch('username'); - const [debouncedUsername] = useDebounce(username, 1000); - - useEffect(() => { - if (debouncedUsername && usernameEdit) { - const checkUsername = async () => { - try { - const isAvailable = await checkUsernameAvailability( - community, - debouncedUsername - ); - if (!isAvailable) { - toast.error('Username is already taken'); - setIsAvailable(false); - } else { - setIsAvailable(true); - } - } catch (error) { - console.error('Error checking username availability:', error); - } - }; - - checkUsername(); - } - }, [debouncedUsername, usernameEdit, community]); - - const handleImageUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - setImageFile(file); - const imageUrl = URL.createObjectURL(file); - setAvatarUrl(imageUrl); - } - }; - - const triggerFileInput = () => { - fileInputRef.current?.click(); - }; - - const onSubmit = async (values: z.infer) => { - try { - setIsLoading(true); - - if (!values.username || !values.name) { - toast.error('Please enter a username and name'); - setIsLoading(false); - return; - } - - if (!isAvailable) { - toast.error('Username is already taken, you cannot save it'); - setIsLoading(false); - return; - } - - // Default image - let cid = 'QmZjzYmcbxj6Yr9EBmuMu3knYd25oYvnTu92yLWhiajvMr'; - - if (imageFile) { - const response = await updateProfileImageAction( - imageFile, - community.community.alias - ); - cid = response.IpfsHash; - } - - const profile: Profile = { - account, - description: values.description || '', - image: `ipfs://${cid}`, - image_medium: `ipfs://${cid}`, - image_small: `ipfs://${cid}`, - name: values.name || '', - username: values.username - }; - - await updateProfileAction(profile, config.community.alias, config); - toast.success('Profile updated successfully'); - router.push(`/${config.community.alias}/members`); - } catch (error) { - console.error('Error adding member:', error); - toast.error('Error updating profile'); - } finally { - setIsLoading(false); - } - }; - - return ( - - -
- -
- - - - - - - - -
- -
-
- ( - - Username - - { - setUsernameEdit(true); - field.onChange(e); - }} - className={`bg-white ${isAvailable ? '' : 'border-red-500'}`} - /> - - - - )} - /> - ( - - Name - - - - - - )} - /> -
- ( - - Description - -