mirror of
https://github.com/FlipsideCrypto/badger.git
synced 2026-02-06 10:57:46 +00:00
fix: auth rebuild
This commit is contained in:
parent
9064711b93
commit
082be01ef9
@ -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',
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 >
|
||||
)
|
||||
|
||||
31
frontend/src/components/Button/ConnectButton.js
Normal file
31
frontend/src/components/Button/ConnectButton.js
Normal 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 }
|
||||
@ -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 (
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export { ActionButton } from './ActionButton';
|
||||
export { ConnectButton } from './ConnectButton';
|
||||
export { IconButton } from './IconButton';
|
||||
export { LogoutButton } from './LogoutButton';
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
41
frontend/src/components/Card/OrgCard.js
Normal file
41
frontend/src/components/Card/OrgCard.js
Normal 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 }
|
||||
@ -1,2 +1,3 @@
|
||||
export { Card } from './Card';
|
||||
export { ErrorCard } from './ErrorCard';
|
||||
export { ErrorCard } from './ErrorCard';
|
||||
export { OrgCard } from './OrgCard';
|
||||
@ -1,8 +1,6 @@
|
||||
const Dashboard = ({ children }) => {
|
||||
return (
|
||||
<div className="dashboard__content" style={{
|
||||
marginInline: "20px"
|
||||
}}>
|
||||
<div className="dashboard__content" style={{ marginInline: "20px" }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
23
frontend/src/components/Empty/Empty.js
Normal file
23
frontend/src/components/Empty/Empty.js
Normal 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 }
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
16
frontend/src/components/Icon/ChainIcon.js
Normal file
16
frontend/src/components/Icon/ChainIcon.js
Normal 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 }
|
||||
@ -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>
|
||||
)
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const WalletWrapper = ({ children }) => {
|
||||
);
|
||||
|
||||
const { connectors } = getDefaultWallets({
|
||||
appName: 'My RainbowKit App',
|
||||
appName: 'Badger',
|
||||
chains
|
||||
});
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
BIN
frontend/src/static/images/networks/localhost.png
Normal file
BIN
frontend/src/static/images/networks/localhost.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src/static/images/networks/polygon.png
Normal file
BIN
frontend/src/static/images/networks/polygon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@ -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'
|
||||
46
frontend/src/style/Card/OrgCard.css
Normal file
46
frontend/src/style/Card/OrgCard.css
Normal 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;
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
display: grid;
|
||||
grid-template-rows: 75px auto;
|
||||
grid-row-gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.dashboard.collapsed {
|
||||
|
||||
5
frontend/src/style/Icon/ChainIcon.css
Normal file
5
frontend/src/style/Icon/ChainIcon.css
Normal file
@ -0,0 +1,5 @@
|
||||
.chainIcon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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"]);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user