fix: auth rebuild

This commit is contained in:
nftchance 2023-01-28 00:59:25 -06:00
parent 9064711b93
commit 082be01ef9
34 changed files with 408 additions and 268 deletions

View File

@ -26,7 +26,17 @@ class SerializerRepresentationMixin:
class ConnectedMixin:
async def connect(self, *args, **kwargs):
await super().connect(*args, **kwargs)
if self.scope['user'].is_anonymous:
await self.send_json({
'action': 'disconnected',
'message': 'You must be logged in to connect.'
})
await self.close()
return
print('connected to ', self.scope['user'])
await self.send_json({
'action': 'connected',
})

View File

@ -3,7 +3,6 @@ from rest_framework.permissions import (
IsAuthenticated
)
def generator(existing_permissions=None):
permission_classes = [IsAuthenticated]
@ -32,6 +31,8 @@ class CanManageOrganization(permissions.BasePermission):
class CanManageBadge(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print('checking if can manage badge')
organization = obj.organization
return (
organization.owner == request.user

View File

@ -2,15 +2,20 @@ from djangochannelsrestframework import permissions
from djangochannelsrestframework.observer import model_observer
from api.mixins import ManagedModelMixin
from api.permissions import CanManageBadge
from .models import Badge
from .serializers import BadgeSerializer
class WrappedCanManageBadge(permissions.WrappedDRFPermission):
def has_object_permission(self, request, view, obj):
return CanManageBadge().has_object_permission(request, view, obj)
class BadgeConsumer(ManagedModelMixin):
queryset = Badge.objects.all()
serializer_class = BadgeSerializer
permissions = (permissions.IsAuthenticated,)
permissions = (permissions.IsAuthenticated, CanManageBadge)
@model_observer(Badge)
async def model_change(self, message, observer=None, **kwargs):

View File

@ -1,15 +1,38 @@
from djangochannelsrestframework import permissions
from djangochannelsrestframework.observer import model_observer
from django.db.models import Q
from api.mixins import ManagedModelMixin
from api.permissions import CanManageOrganization
from .models import Organization
from .serializers import OrganizationSerializer
class WrappedCanManageOrganization(permissions.WrappedDRFPermission):
def has_permission(self, request, view):
print('has_permission')
return CanManageOrganization().has_permission(request, view)
def has_object_permission(self, request, view, obj):
print('in here')
return CanManageOrganization().has_object_permission(request, view, obj)
class OrganizationConsumer(ManagedModelMixin):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permissions = (permissions.IsAuthenticated,)
permissions = (permissions.IsAuthenticated)
# TODO: Make it to where staff users can see all organizations.
def get_queryset(self, **kwargs):
query = (
Q(owner=self.scope['user']) |
Q(delegates=self.scope['user'])
)
return self.queryset.filter(query).distinct()
@model_observer(Organization)
async def model_change(self, message, observer=None, **kwargs):

View File

@ -2,6 +2,8 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { library } from '@fortawesome/fontawesome-svg-core'
import { fal } from '@fortawesome/pro-light-svg-icons'
import { ErrorContextProvider, OrgContextProvider, UserContextProvider } from "@contexts"
import { SEO, WalletWrapper } from "@components"
import { Dashboard, Landing } from "@pages"
@ -23,7 +25,13 @@ function App() {
<Route exact path="/" element={<Landing />} />
<Route exact path="/dashboard/*" element={
<WalletWrapper>
<Dashboard />
<ErrorContextProvider>
<OrgContextProvider>
<UserContextProvider>
<Dashboard />
</UserContextProvider>
</OrgContextProvider>
</ErrorContextProvider>
</WalletWrapper>
} />
</Routes>

View File

@ -1,14 +1,13 @@
import { useEffect, useContext, useState, useCallback } from "react";
import { useLocation } from 'react-router-dom';
import { useNetwork, useSwitchNetwork } from "wagmi";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useAccount, useNetwork, useSwitchNetwork } from "wagmi";
import { OrgContext, UserContext } from "@contexts";
import { useENSProfile } from "@hooks";
import { ActionButton, LogoutButton, OrgView, ProfileView } from "@components"
import { ActionButton, ConnectButton, LogoutButton, OrgView, ProfileView } from "@components"
import { sliceAddress } from "@utils";
@ -19,14 +18,14 @@ import "@style/Bar/ActionBar.css";
const PRIMARY_PRODUCTION_CHAIN = process.env.REACT_APP_PRODUCTION_CHAIN;
const ActionBar = ({ address, collapsed, setCollapsed }) => {
const ActionBar = ({ collapsed, setCollapsed }) => {
const { chain } = useNetwork();
const { chains, switchNetwork } = useSwitchNetwork();
const { openConnectModal } = useConnectModal();
const { ensAvatar, ensName, isFetched: ensFetched } = useENSProfile(address);
const { authenticatedAddress, isAuthenticated, tryAuthentication } = useContext(UserContext);
const { ensAvatar, ensName, isFetched: ensFetched } = useENSProfile(authenticatedAddress);
const { isAuthenticated, tryAuthentication } = useContext(UserContext);
const { orgData } = useContext(OrgContext);
const { pathname: path } = useLocation();
@ -52,40 +51,20 @@ const ActionBar = ({ address, collapsed, setCollapsed }) => {
onSwitchNetworkRequest();
}, [chain, isWrongNetwork, onSwitchNetworkRequest]);
// Opens the connect modal on landing if connection has not already been
// established in prior session.
useEffect(() => {
if (openConnectModal && !address)
openConnectModal()
}, [openConnectModal, address])
return (
<div className="action_bar">
<div className="action_bar__view">
{!address
? <button onClick={openConnectModal}>
Connect Wallet
</button>
: isWrongNetwork ?
<button onClick={onSwitchNetworkRequest}>
{`Switch to ${PRIMARY_PRODUCTION_CHAIN}`}
</button>
: !isAuthenticated || !ensFetched ?
<button onClick={tryAuthentication}>
Sign In
</button>
: !orgId || !orgData?.name ? <ProfileView
ensAvatar={ensAvatar}
ensName={ensName}
address={sliceAddress(address)}
/>
: <OrgView
orgData={orgData}
ipfs={IPFS_GATEWAY_URL}
sliceAddress={sliceAddress}
/>
}
{!isAuthenticated ?
<ConnectButton /> :
!orgId || !orgData?.name ? <ProfileView
ensAvatar={ensAvatar}
ensName={ensName}
address={sliceAddress(authenticatedAddress)}
/> : <OrgView
orgData={orgData}
ipfs={IPFS_GATEWAY_URL}
sliceAddress={sliceAddress}
/>}
</div>
<div className="action_bar__actions">
@ -104,7 +83,7 @@ const ActionBar = ({ address, collapsed, setCollapsed }) => {
/>
{address && <LogoutButton />}
{isAuthenticated && authenticatedAddress && <LogoutButton />}
</div>
</div >
)

View File

@ -0,0 +1,31 @@
import { useAccount, useNetwork, useSwitchNetwork } from "wagmi"
import { useConnectModal } from "@rainbow-me/rainbowkit"
import { sliceAddress } from "@utils"
const PRIMARY_PRODUCTION_CHAIN = process.env.REACT_APP_PRODUCTION_CHAIN
const ConnectButton = () => {
const { chain } = useNetwork()
const { chains, switchNetwork } = useSwitchNetwork()
const { address, isConnected, isIdle, isLoading } = useAccount()
const { openConnectModal } = useConnectModal()
const isWrongNetwork = chain?.name !== PRIMARY_PRODUCTION_CHAIN
if (!isConnected) return <button onClick={openConnectModal}>
{isIdle || isLoading ? "Loading..." : "Connect Wallet"}
</button>
if (isWrongNetwork) return <button
disabled={!switchNetwork}
onClick={switchNetwork?.bind(null, chains.find(c => c.name === PRIMARY_PRODUCTION_CHAIN)?.id)}
>Switch to {PRIMARY_PRODUCTION_CHAIN}</button>
return <button onClick={() => { }}>Sign In</button>
}
export { ConnectButton }

View File

@ -10,9 +10,10 @@ const LogoutButton = () => {
const navigate = useNavigate();
const onDisconnect = () => {
disconnect();
document.cookie = 'csrftoken=; Path=/; Expires=Sat, 01 Jan 2000 00:00:001 GMT;';
navigate("/");
document.cookie = 'authenticatedAddress=; Path=/; Expires=Sat, 01 Jan 2000 00:00:001 GMT;';
disconnect({ onSuccess: () => { navigate("/dashboard/") } });
}
return (

View File

@ -1,3 +1,4 @@
export { ActionButton } from './ActionButton';
export { ConnectButton } from './ConnectButton';
export { IconButton } from './IconButton';
export { LogoutButton } from './LogoutButton';

View File

@ -1,8 +1,10 @@
import "@style/Card/Card.css";
const Card = ({ children, className, style }) => {
const Card = (props) => {
const { children, className, style } = props;
return (
<div className={`card ${className}`} style={style}>
<div className={`card ${className || ''}`} style={style} {...props}>
{children}
</div>
)

View File

@ -0,0 +1,41 @@
import { useNavigate } from "react-router-dom";
import { handleImageLoad } from "@hooks";
import { Card, ChainIcon, ImageLoader } from "@components";
import { IPFS_GATEWAY_URL } from "@static";
import { sliceAddress } from "@utils";
import "@style/Card/OrgCard.css"
const OrgCard = ({ org }) => {
const navigate = useNavigate();
return (
<Card onClick={() => navigate(`/dashboard/organization/${org.id}`)}>
<div className="home__card__image" />
<div className="text">
<div className="subtext">
<ChainIcon chain={org.chain} />
<strong>{sliceAddress(org.ethereum_address)}</strong>
</div>
<div className="title">
<h2>
<ImageLoader className="viewImage"
src={IPFS_GATEWAY_URL + org.image_hash}
onLoad={handleImageLoad}
/>
{org.name}
</h2>
</div>
</div>
</Card>
)
}
export { OrgCard }

View File

@ -1,2 +1,3 @@
export { Card } from './Card';
export { ErrorCard } from './ErrorCard';
export { ErrorCard } from './ErrorCard';
export { OrgCard } from './OrgCard';

View File

@ -1,8 +1,6 @@
const Dashboard = ({ children }) => {
return (
<div className="dashboard__content" style={{
marginInline: "20px"
}}>
<div className="dashboard__content" style={{ marginInline: "20px" }}>
{children}
</div>
)

View File

@ -0,0 +1,23 @@
import { Link } from "react-router-dom";
import { IconButton } from "@components";
const Empty = ({
title,
body,
button,
url
}) => {
return (
<div className="org__container empty" style={{ gridColumn: "span 3" }}>
<h1>{title}</h1>
<p style={{ marginBottom: "40px" }}>{body}</p>
{url && button && <Link className="internal-link" to={url}>
<IconButton icon={['fal', 'arrow-right']} text={button} />
</Link>}
</div>
)
}
export { Empty }

View File

@ -15,16 +15,16 @@ const Header = ({ back, actions }) => {
}
</div>
<div className="header__actions">
{actions && actions.map((action, index) => (
{actions && <div className="header__actions">
{actions.map((action, index) => (
<ActionButton
key={index}
onClick={action.event}
onClick={action.event || action.onClick}
icon={action.icon}
afterText={action.text}
/>
))}
</div>
</div>}
</header>
);
}

View File

@ -0,0 +1,16 @@
import { localhost, polygon } from "@static"
import "@style/Icon/ChainIcon.css"
const icons = {
localhost,
polygon,
}
const ChainIcon = ({ chain }) => {
const icon = chain && icons[chain.toLowerCase()] || '🚨'
return <img className="chainIcon" src={icon} alt={chain} />
}
export { ChainIcon }

View File

@ -9,7 +9,7 @@ const ActionTitle = ({ title, actions }) => {
}}>
<h2>{title}</h2>
<div style={{
{actions && <div style={{
display: "flex",
justifyContent: "flex-end",
}}>
@ -19,10 +19,10 @@ const ActionTitle = ({ title, actions }) => {
className={action.className}
onClick={action.onClick}
icon={action.icon}
afterText={action.afterText}
afterText={action.text || action.afterText}
/>
))}
</div>
</div>}
</div>
)

View File

@ -13,7 +13,7 @@ const WalletWrapper = ({ children }) => {
);
const { connectors } = getDefaultWallets({
appName: 'My RainbowKit App',
appName: 'Badger',
chains
});

View File

@ -1,12 +1,14 @@
export { Avatar } from './Avatar/Avatar';
export { ActionBar, FormActionBar } from './Bar';
export { ActionButton, IconButton, LogoutButton } from './Button';
export { Card, ErrorCard } from './Card';
export { ActionButton, ConnectButton, IconButton, LogoutButton } from './Button';
export { Card, ErrorCard, OrgCard } from './Card';
export { BadgeDangerZone, OrgDangerZone } from './DangerZone';
export { Dashboard } from './Dashboard/Dashboard';
export { Empty } from './Empty/Empty';
export { Checkbox, Input, InputAddress, InputListCSV, InputListKeyValue, Select, Switch, FormDrawer, FormReducer, initialBadgeForm, initialOrgForm } from './Form';
export { Header } from './Header/Header';
export { Help, HelpCopy, HelpSidebar } from './Help';
export { ChainIcon } from './Icon/ChainIcon';
export { OrgStats } from './Org/OrgStats';
export { SEO } from './SEO/SEO';
export { StatusIndicators } from './StatusIndicators/StatusIndicators';

View File

@ -1,15 +1,20 @@
import { createContext } from "react"
import { useAccount } from "wagmi"
import { useSocket } from "@hooks";
const BadgeContext = createContext();
const BadgeContextProvider = ({ children }) => {
const { address } = useAccount();
const enabled = !!address;
const {
connected,
data: badges,
send
} = useSocket({ url: 'ws://localhost:8000/ws/badge/' })
} = useSocket({ enabled, url: 'ws://localhost:8000/ws/badge/' })
return (
<BadgeContext.Provider value={{

View File

@ -1,4 +1,5 @@
import { createContext } from "react"
import { useAccount } from "wagmi"
import { BadgeContextProvider } from "@contexts"
@ -7,11 +8,15 @@ import { useSocket } from "@hooks"
const OrgContext = createContext();
const OrgContextProvider = ({ children }) => {
const { address } = useAccount();
const enabled = !!address;
const {
connected,
data: organizations,
send
} = useSocket({ url: 'ws://localhost:8000/ws/organization/' })
} = useSocket({ enabled, url: 'ws://localhost:8000/ws/organization/' })
return (
<OrgContext.Provider value={{

View File

@ -1,4 +1,4 @@
import { createContext, useState, useContext, useEffect, useCallback } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import { useAccount, useNetwork, useSigner } from "wagmi";
import { BadgeContext, OrgContext } from "@contexts";
@ -7,51 +7,51 @@ import { getAuthentication, getAuthenticationMessage } from "@utils";
const UserContext = createContext();
// TODO: Prompt signature just once on page load, but only if there is not an existing
// authentication token for the connected signer. (I think this is what getAuthenticationStatus() was being used for)
const getAuthenticatedAddress = () => {
return document.cookie.split(';').find(c => c.includes('authenticatedAddress'))?.split('=')[1];
}
const UserContextProvider = ({ children }) => {
const { chain } = useNetwork();
const { signer } = useSigner();
const { data: signer } = useSigner();
const { address } = useAccount();
const { address, isConnected } = useAccount();
const { organizations } = useContext(OrgContext);
const { badges } = useContext(BadgeContext);
const [authenticatedAddress, setAuthenticatedAddress] = useState(null);
const [authenticatedAddress, setAuthenticatedAddress] = useState(getAuthenticatedAddress());
const isAuthenticated = signer?._address ? authenticatedAddress === signer._address : false;
const isAuthenticated = isConnected && address === authenticatedAddress;
const isLoaded = organizations && badges;
useEffect(() => {
const tryAuthentication = async ({ address, chainId, signer }) => {
// Clear any prior authentication token and prompt a signature to authenticate.
// TODO: This should not be here.
// document.cookie = 'csrftoken=; Path=/; Expires=Sat, 01 Jan 2000 00:00:001 GMT;';
// Make the call to the backend.
const { message } = await getAuthenticationMessage(address, chainId);
const tryAuthentication = async ({ chainId, signer }) => {
const { message } = await getAuthenticationMessage(signer._address, chainId);
const signature = await signer.signMessage(message.prepareMessage());
const response = await getAuthentication(message, signature);
// TODO: Doing nothing with this response? how is that possible?
if (!response.success) return
setAuthenticatedAddress(address);
setAuthenticatedAddress(signer._address);
};
if (!address || !chain || !signer) return;
if (!signer || !chain || isAuthenticated) return;
tryAuthentication({ address, chainId: chain.chainId, signer });
}, [address, chain, signer])
tryAuthentication({ chainId: chain.id, signer });
}, [signer, chain])
return (
<UserContext.Provider value={{
isAuthenticated,
authenticatedAddress,
organizations,
badges
badges,
isConnected,
isAuthenticated,
isLoaded
}}>
{children}
</UserContext.Provider>

View File

@ -2,13 +2,15 @@ import { useEffect, useMemo, useState } from 'react';
import ReconnectingWebSocket from 'reconnecting-websocket';
const useSocket = ({ url }) => {
const useSocket = ({ enabled, url }) => {
const [connected, setConnected] = useState(false);
const [objects, setObjects] = useState([]);
const [objects, setObjects] = useState(null);
const client = useMemo(() => {
if(!enabled) return null;
return new ReconnectingWebSocket(url);
}, [url]);
}, [enabled, url]);
const callbacks = useMemo(() => ({}), []);
@ -64,6 +66,8 @@ const useSocket = ({ url }) => {
}
useEffect(() => {
if (!enabled) return;
client.onopen = () => {
client.send(JSON.stringify({
action: 'list',
@ -77,7 +81,7 @@ const useSocket = ({ url }) => {
handleAction(message);
}
}, []);
}, [enabled]);
return {
connected,

View File

@ -1,53 +1,58 @@
import { useState } from "react";
import { useContext, useState } from "react";
import { Route, Routes } from "react-router-dom";
import { useAccount, useSigner } from "wagmi";
import { ErrorContextProvider, OrgContextProvider, UserContextProvider } from "@contexts"
import { UserContext } from "@contexts"
import { ActionBar, Dashboard as DashboardContent, HelpSidebar } from "@components";
import { ActionBar, Dashboard as DashboardContent, Empty, HelpSidebar } from "@components";
import { Badge, BadgeForm, Home, Org, OrgForm } from "@pages";
import "@style/Dashboard/Dashboard.css";
const Dashboard = () => {
const { data: signer } = useSigner();
const { address } = useAccount();
// This syntax is mad weird, but it may work :shrug:
const [collapsed, setCollapsed] = useState(false);
// TODO: If they go to the dashboard and they aren't authenticated, are they told that they have no organizations?
// ... fuck yes rip me
const { isAuthenticated, isConnected, isLoaded } = useContext(UserContext);
return (
<>
<ErrorContextProvider>
<OrgContextProvider>
<UserContextProvider>
<div className={collapsed ? "dashboard collapsed" : "dashboard"}>
<div className="dashboard__contents">
<ActionBar address={address} collapsed={collapsed} setCollapsed={setCollapsed} />
<div className={collapsed ? "dashboard collapsed" : "dashboard"}>
<div className="dashboard__contents">
<ActionBar collapsed={collapsed} setCollapsed={setCollapsed} />
<DashboardContent>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/organization/new" element={<OrgForm />} />
<Route path="/organization/:orgId" element={<Org />} />
<Route path="/organization/:orgId/edit" element={<OrgForm isEdit={true} />} />
<Route path="/organization/:orgId/badge/new" element={<BadgeForm />} />
<Route path="/organization/:orgId/badge/:badgeId" element={<Badge />} />
<Route path="/organization/:orgId/badge/:badgeId/edit" element={<BadgeForm isEdit={true} />} />
</Routes>
</DashboardContent>
</div>
{!isConnected && <Empty
title="Connect your wallet to view your Organizations!"
body="Connecting your wallet is simple and secure. Using Sign in with Ethereum, you can sign and create, manage, and share your Organizations and Badges in seconds just by signing a message."
button="CONNECT WALLET"
url="/login"
/>}
<HelpSidebar collapsed={collapsed} />
</div>
</UserContextProvider>
</OrgContextProvider>
</ErrorContextProvider>
</>
{isConnected && (!isAuthenticated && <Empty
title="Authenticate your wallet to view your Organizations!"
body="Authentication is simple and secure. Using Sign in with Ethereum, you can sign and create, manage, and share your Organizations and Badges in seconds just by signing a message."
button="SIGN IN"
url="/login"
/>)}
{isAuthenticated && !isLoaded && <Empty
title="Loading Organizations and Badges..."
body="This may take a few seconds. If this takes longer than 10 seconds, please refresh the page."
/>}
{isLoaded && <DashboardContent>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/organization/new" element={<OrgForm />} />
<Route path="/organization/:orgId" element={<Org />} />
<Route path="/organization/:orgId/edit" element={<OrgForm isEdit={true} />} />
<Route path="/organization/:orgId/badge/new" element={<BadgeForm />} />
<Route path="/organization/:orgId/badge/:badgeId" element={<Badge />} />
<Route path="/organization/:orgId/badge/:badgeId/edit" element={<BadgeForm isEdit={true} />} />
</Routes>
</DashboardContent>}
</div>
<HelpSidebar collapsed={collapsed} />
</div>
)
}

View File

@ -1,83 +1,42 @@
import { useContext } from "react"
import { Link, useNavigate } from "react-router-dom";
import { useContext, Suspense } from "react"
import { useNavigate } from "react-router-dom";
import { UserContext } from "@contexts";
import { handleImageLoad } from "@hooks";
import { ActionTitle, Card, IconButton, ImageLoader } from "@components"
import { sliceAddress } from "@utils";
import { IPFS_GATEWAY_URL } from "@static";
import { ActionTitle, Empty, OrgCard } from "@components"
import "@style/pages/Home.css";
const Home = () => {
const navigate = useNavigate();
const { userData } = useContext(UserContext);
const { isAuthenticated, organizations } = useContext(UserContext);
const titleActions = isAuthenticated && [{
className: "home__action-button",
text: "Create Organization",
icon: ['fal', 'plus'],
onClick: () => navigate(`/dashboard/organization/new/`)
}];
return (
<div className="home">
<ActionTitle
title="Organizations"
actions={[
{
className: "home__action-button",
icon: ['fal', 'plus'],
onClick: () => navigate('/dashboard/organization/new'),
afterText: "Create organization"
}
]}
actions={titleActions}
/>
<div className="home__cards">
{userData?.organizations?.length > 0
? userData?.organizations?.map((org, index) => (
<div
key={index}
onClick={() => navigate(`/dashboard/organization/${org.id}`)}
>
<Card
className="home__card"
>
<div className="home__card__image" />
<div className="home__card__text">
<div className="home__card__subtext">
<small><strong><span style={{ marginRight: "10px" }}>
{org.chain.slice(0, 5)}
</span> {sliceAddress(org.ethereum_address)}</strong></small>
</div>
{organizations && organizations.length === 0 && <Empty
title="No Organizations yet!"
body="Creating the Badges for your first Organization is easy. Choose and customize your Organization's name, logo, and description and your organization is live!"
button="CREATE ORGANIZATION"
url="/dashboard/organization/new/"
/>}
<div className="home__card__title">
<h2>
<ImageLoader
className="home__card__view__image"
src={IPFS_GATEWAY_URL + org.image_hash}
onLoad={handleImageLoad}
/>
{org.name}
</h2>
</div>
</div>
</Card>
</div>
))
: <div className="org__container empty" style={{
gridColumn: "span 3"
}}>
<h1>No Organizations yet!</h1>
<p>
Creating the Badges for your first Organization is easy.
Choose and customize your Organization's name, logo, and description and your organization is live!
</p>
<Link className="internal-link" to={`/dashboard/organization/new`}>
<IconButton icon={['fal', 'arrow-right']} text="CREATE ORGANIZATION" style={{ marginTop: "40px" }} />
</Link>
</div>}
</div>
</div>
{organizations && organizations.length > 0 && <div className="home__cards">
{organizations?.map((org, index) => <OrgCard key={index} org={org} />)}
</div>}
</div >
)
}

View File

@ -1,60 +1,49 @@
import { useContext, useMemo } from "react";
import { useNavigate, Link, useParams } from "react-router-dom";
import { useAccount } from "wagmi";
import { useContext } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { OrgContext } from "@contexts";
import { UserContext } from "@contexts";
import { ActionTitle, BadgeTable, IconButton, Header } from "@components";
import { ActionTitle, BadgeTable, Empty, Header } from "@components";
const Org = () => {
const navigate = useNavigate();
const { address } = useAccount();
const { orgData } = useContext(OrgContext);
const { orgId } = useParams();
const isOwner = useMemo(() => {
return orgData.owner.ethereum_address === address;
}, [orgData, address])
const { authenticatedAddress, organizations } = useContext(UserContext);
const org = organizations && organizations.find(org => String(org.id) === orgId);
const isOwner = org && org.owner.ethereum_address === authenticatedAddress;
const headerActions = isOwner && [{
text: "Settings",
icon: ['fal', 'fa-gear'],
onClick: () => navigate(`/dashboard/organization/${orgId}/edit/`)
}];
const titleActions = isOwner && [{
text: "Create Badge",
icon: ['fal', 'plus'],
onClick: () => navigate(`/dashboard/organization/${orgId}/badge/new/`)
}];
return (
<>
<Header
back={() => navigate("/dashboard")}
actions={isOwner ?
[{
text: "Settings",
icon: ['fal', 'fa-gear'],
event: () => navigate(`/dashboard/organization/${orgId}/edit`)
}] : []
}
/>
<Header back={() => navigate("/dashboard/")} actions={headerActions} />
<div className="dashboard__content">
<ActionTitle
title="Organization Badges"
actions={isOwner ?
[{
className: "home__action-button",
icon: ['fal', 'plus'],
onClick: () => navigate(`/dashboard/organization/${orgId}/badge/new`),
afterText: "Create badge"
}] : []
}
/>
<ActionTitle title="Organization Badges" actions={titleActions} />
{orgData?.badges?.length > 0
? <BadgeTable orgId={orgData?.id} badges={orgData?.badges} />
: <div className="org__container empty">
<h1>No Badges in {orgData?.name ? orgData?.name : "the Organization"} yet!</h1>
<p>
You are one step closer to having the credentials of your on-chain Organization.
Now you can create and distribute your badges that act as keys throughout the ecosystem in a matter of seconds.
</p>
<Link className="internal-link" to={`/dashboard/organization/${orgId}/badge/new`}>
<IconButton icon={['fal', 'arrow-right']} text="CREATE BADGE" style={{ marginTop: "40px" }} />
</Link>
</div>}
{org && org.badges.length === 0 && <Empty
title="No Badges in the Organization yet!"
body="You are one step closer to having the credentials of your on-chain Organization.
Now you can create and distribute your badges that act as keys throughout the ecosystem in a matter of seconds."
button="CREATE BADGE"
url={`/dashboard/organization/${orgId}/badge/new/`}
/>}
{org && org.badges.length > 0 && <BadgeTable orgId={org?.id} badges={org?.badges} />}
</div>
</>
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -5,4 +5,7 @@ export { default as ImageErrorFallback } from './images/imgerror.svg'
export { default as key } from './images/key.gif'
export { default as logo } from './images/logo.png'
export { default as opengraph } from './images/opengraph.png'
export { default as tutorial } from './images/tutorial.mp4'
export { default as tutorial } from './images/tutorial.mp4'
export { default as localhost } from './images/networks/localhost.png'
export { default as polygon } from './images/networks/polygon.png'

View File

@ -0,0 +1,46 @@
.home__cards .card {
border-radius: 4px;
height: 150px;
padding: 20px;
position: relative;
cursor: pointer;
}
.home__cards .card .home__card__image {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-radius: 4px;
width: 100%;
filter: brightness(1.75);
}
.home__cards .card .text {
display: grid;
grid-template-rows: auto 4fr;
height: 100%;
}
.home__cards .card .subtext {
display: flex;
font-size: 12px;
align-items: center;
column-gap: 10px;
}
.home__cards .card .text .title {
margin-top: auto;
display: grid;
align-self: center;
}
.home__cards .card .text .title .viewImage {
border-radius: 50%;
height: 24px;
width: 24px;
margin-bottom: -5px;
margin-right: 10px;
}

View File

@ -8,6 +8,7 @@
display: grid;
grid-template-rows: 75px auto;
grid-row-gap: 20px;
margin-bottom: 40px;
}
.dashboard.collapsed {

View File

@ -0,0 +1,5 @@
.chainIcon {
height: 20px;
width: 20px;
border-radius: 50%;
}

View File

@ -27,41 +27,8 @@
grid-gap: 20px;
}
.home__cards .card {
border-radius: 4px;
height: 200px;
padding: 20px;
position: relative;
cursor: pointer;
}
.home__cards .home__card .home__card__image {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-radius: 4px;
width: 100%;
}
.home__cards .home__card__text {
display: grid;
grid-template-rows: 4fr 8fr;
height: 100%;
}
.home__cards .home__card__text .home__card__title {
margin-top: auto;
display: grid;
align-self: center;
}
.home__cards .home__card__text .home__card__title .home__card__view__image {
border-radius: 50%;
height: 24px;
width: 24px;
margin-bottom: -5px;
margin-right: 10px;
@media screen and (max-width: 990px) {
.home__cards {
grid-template-columns: 1fr 1fr;
}
}

View File

@ -44,6 +44,8 @@ async function getAuthenticationMessage(address, chainId) {
async function getAuthentication(message, signature) {
const csrfToken = getCSRFToken();
if (!csrfToken) throw new Error(ERRORS["API_CSRF_TOKEN_NOT_FOUND"]);
const response = await fetch(`${API_URL}/api/auth/login`, {
method: "POST",
headers: {
@ -54,7 +56,14 @@ async function getAuthentication(message, signature) {
credentials: 'include'
})
if (response.ok) return response.json();
if (response.ok) {
// save the address as a cookie
const address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
document.cookie = `authenticatedAddress=${address}; path=/; max-age=31536000; SameSite=Lax; Secure`;
return response.json()
};
throw new Error(ERRORS["API_AUTHENTICATION_FAILED"]);
};