diff --git a/keyserver/Dockerfile b/keyserver/Dockerfile index cb5558631..d1c568b29 100644 --- a/keyserver/Dockerfile +++ b/keyserver/Dockerfile @@ -1,167 +1,168 @@ FROM node:16.18.0-bullseye #------------------------------------------------------------------------------- # STEP 0: SET UP USER # Set up Linux user and group for the container #------------------------------------------------------------------------------- # We use bind mounts for our backups folder, which means Docker on Linux will # blindly match the UID/GID for the backups folder on the container with the # host. In order to make sure the container is able to create backups with the # right UID/GID, we need to do two things: # 1. Make sure that the user that runs the Docker container on the host has # permissions to write to the backups folder on the host. We rely on the host # to configure this properly # 2. Make sure we're running this container with the same UID/GID that the host # is using, so the UID/GID show up correctly on both sides of the bind mount # To handle 2 correctly, we have the host pass the UID/GID with which they're # running the container. Our approach is based on this one: # https://github.com/mhart/alpine-node/issues/48#issuecomment-430902787 ARG HOST_UID ARG HOST_GID +ARG COMM_ALCHEMY_KEY USER root RUN \ if [ -z "`getent group $HOST_GID`" ]; then \ addgroup --system --gid $HOST_GID comm; \ else \ groupmod --new-name comm `getent group $HOST_GID | cut -d: -f1`; \ fi && \ if [ -z "`getent passwd $HOST_UID`" ]; then \ adduser --system --uid $HOST_UID --ingroup comm --shell /bin/bash comm; \ else \ usermod --login comm --gid $HOST_GID --home /home/comm --move-home \ `getent passwd $HOST_UID | cut -d: -f1`; \ fi #------------------------------------------------------------------------------- # STEP 1: INSTALL PREREQS # Install prereqs first so we don't have to reinstall them if anything changes #------------------------------------------------------------------------------- # We need to add the MariaDB repo to apt in order to install mariadb-client RUN wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup \ && chmod +x mariadb_repo_setup \ && ./mariadb_repo_setup \ && rm mariadb_repo_setup # We need rsync in the prod-build yarn script # We need mariadb-client so we can use mysqldump for backups RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ rsync \ mariadb-client \ && rm -rf /var/lib/apt/lists/* #------------------------------------------------------------------------------- # STEP 2: DEVOLVE PRIVILEGES # Create another user to run the rest of the commands #------------------------------------------------------------------------------- USER comm WORKDIR /home/comm/app #------------------------------------------------------------------------------- # STEP 3: SET UP MYSQL BACKUPS # Prepare the system to properly handle mysqldump backups #------------------------------------------------------------------------------- # Prepare the directory that will hold the backups RUN mkdir /home/comm/backups #------------------------------------------------------------------------------- # STEP 4: SET UP CARGO (RUST PACKAGE MANAGER) # We use Cargo to build pre-compiled Node.js addons in Rust #------------------------------------------------------------------------------- # Install Rust and add Cargo's bin directory to the $PATH environment variable RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH /home/comm/.cargo/bin:$PATH #------------------------------------------------------------------------------- # STEP 5: SET UP NVM # We use nvm to make sure we're running the right Node version #------------------------------------------------------------------------------- # First we install nvm ENV NVM_DIR /home/comm/.nvm RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh \ | bash # Then we use nvm to install the right version of Node. We call this early so # Docker build caching saves us from re-downloading Node when any file changes COPY --chown=comm keyserver/.nvmrc keyserver/ COPY --chown=comm keyserver/bash/source-nvm.sh keyserver/bash/ RUN cd keyserver && . bash/source-nvm.sh #------------------------------------------------------------------------------- # STEP 6: YARN CLEANINSTALL # We run yarn cleaninstall before copying most of the files in for build caching #------------------------------------------------------------------------------- # Copy in package.json and yarn.lock files COPY --chown=comm package.json yarn.lock ./ COPY --chown=comm keyserver/package.json keyserver/.flowconfig keyserver/ COPY --chown=comm lib/package.json lib/.flowconfig lib/ COPY --chown=comm web/package.json web/.flowconfig web/ COPY --chown=comm native/package.json native/.flowconfig native/ COPY --chown=comm landing/package.json landing/.flowconfig landing/ COPY --chown=comm desktop/package.json desktop/ COPY --chown=comm keyserver/addons/opaque-ke-napi/package.json \ keyserver/addons/opaque-ke-napi/ COPY --chown=comm native/expo-modules/android-lifecycle/package.json \ native/expo-modules/android-lifecycle/ # Create empty Rust library and copy in Cargo.toml file RUN cargo init keyserver/addons/opaque-ke-napi --lib COPY --chown=comm keyserver/addons/opaque-ke-napi/Cargo.toml \ keyserver/addons/opaque-ke-napi/ # Copy in files needed for patch-package COPY --chown=comm patches patches/ # Actually run yarn RUN yarn cleaninstall #------------------------------------------------------------------------------- # STEP 7: WEBPACK BUILD # We do this first so Docker doesn't rebuild when only keyserver files change #------------------------------------------------------------------------------- COPY --chown=comm lib lib/ COPY --chown=comm landing landing/ RUN yarn workspace landing prod COPY --chown=comm web web/ RUN yarn workspace web prod #------------------------------------------------------------------------------- # STEP 8: COPY IN SOURCE FILES # We run this later so the above layers are cached if only source files change #------------------------------------------------------------------------------- COPY --chown=comm . . #------------------------------------------------------------------------------- # STEP 9: BUILD NODE ADDON # Now that source files have been copied in, build the opaque-ke-napi addon #------------------------------------------------------------------------------- RUN yarn workspace opaque-ke-napi build #------------------------------------------------------------------------------- # STEP 10: RUN BUILD SCRIPTS # We need to populate keyserver/dist, among other things #------------------------------------------------------------------------------- # Babel transpilation of keyserver src RUN yarn workspace keyserver prod-build #------------------------------------------------------------------------------- # STEP 11: RUN THE SERVER # Actually run the Node.js keyserver using nvm #------------------------------------------------------------------------------- EXPOSE 3000 WORKDIR /home/comm/app/keyserver CMD bash/run-prod.sh diff --git a/keyserver/docker-compose.yml b/keyserver/docker-compose.yml index 9bf97087e..a9a2b90f1 100644 --- a/keyserver/docker-compose.yml +++ b/keyserver/docker-compose.yml @@ -1,51 +1,52 @@ version: "3.9" services: node: build: dockerfile: keyserver/Dockerfile context: ../ args: - HOST_UID=${HOST_UID} - HOST_GID=${HOST_GID} + - COMM_ALCHEMY_KEY=${COMM_ALCHEMY_KEY} image: commapp/node-keyserver:1.0 restart: always ports: - "3000:3000" env_file: - .env environment: - REDIS_URL=redis://cache - COMM_LISTEN_ADDR=0.0.0.0 - COMM_DATABASE_HOST=${COMM_DATABASE_HOST:-database} - COMM_DATABASE_DATABASE - COMM_DATABASE_USER - COMM_DATABASE_PASSWORD - COMM_DATABASE_TYPE=mariadb10.8 depends_on: - cache - database database: image: mariadb:10.8.3-jammy restart: always expose: - "3306" command: > --max-allowed-packet=256M --local-infile=0 --sql-mode=STRICT_ALL_TABLES --innodb-buffer-pool-size=1600M environment: - MARIADB_RANDOM_ROOT_PASSWORD=yes - MARIADB_DATABASE=$COMM_DATABASE_DATABASE - MARIADB_USER=$COMM_DATABASE_USER - MARIADB_PASSWORD=$COMM_DATABASE_PASSWORD volumes: - mysqldata:/var/lib/mysql cache: image: redis:6.2.6-bullseye restart: always expose: - "6379" command: redis-server --loglevel warning volumes: mysqldata: diff --git a/landing/.eslintrc.json b/landing/.eslintrc.json index e5a34aec6..2ca7ba73b 100644 --- a/landing/.eslintrc.json +++ b/landing/.eslintrc.json @@ -1,5 +1,8 @@ { "env": { "browser": true + }, + "globals": { + "process": true } } diff --git a/landing/siwe.react.js b/landing/siwe.react.js index e22d6de63..11ce8a58a 100644 --- a/landing/siwe.react.js +++ b/landing/siwe.react.js @@ -1,196 +1,200 @@ // @flow import { useConnectModal, getDefaultWallets, RainbowKitProvider, darkTheme, useModalState, ConnectButton, } from '@rainbow-me/rainbowkit'; import invariant from 'invariant'; import _merge from 'lodash/fp/merge'; import * as React from 'react'; import { SiweMessage } from 'siwe'; import '@rainbow-me/rainbowkit/dist/index.css'; import { useAccount, useSigner, chain, configureChains, createClient, WagmiConfig, } from 'wagmi'; +import { alchemyProvider } from 'wagmi/providers/alchemy'; import { publicProvider } from 'wagmi/providers/public'; import type { SIWEWebViewMessage } from 'lib/types/siwe-types'; import { siweStatement } from 'lib/utils/siwe-utils.js'; import { SIWENonceContext } from './siwe-nonce-context.js'; import css from './siwe.css'; -// details can be found https://wagmi.sh/docs/providers/configuring-chains +// details can be found https://0.6.x.wagmi.sh/docs/providers/configuring-chains +const availableProviders = process.env.COMM_ALCHEMY_KEY + ? [alchemyProvider({ apiKey: process.env.COMM_ALCHEMY_KEY })] + : [publicProvider()]; const { chains, provider } = configureChains( [chain.mainnet], - [publicProvider()], + availableProviders, ); const { connectors } = getDefaultWallets({ appName: 'comm', chains, }); const wagmiClient = createClient({ autoConnect: true, connectors, provider, }); function createSiweMessage(address: string, statement: string, nonce: string) { invariant(nonce, 'nonce must be present in createSiweMessage'); const domain = window.location.host; const origin = window.location.origin; const message = new SiweMessage({ domain, address, statement, uri: origin, version: '1', chainId: '1', nonce, }); return message.prepareMessage(); } function postMessageToNativeWebView(message: SIWEWebViewMessage) { window.ReactNativeWebView?.postMessage?.(JSON.stringify(message)); } async function signInWithEthereum(address: string, signer, nonce: string) { invariant(nonce, 'nonce must be present in signInWithEthereum'); const message = createSiweMessage(address, siweStatement, nonce); const signature = await signer.signMessage(message); postMessageToNativeWebView({ type: 'siwe_success', address, message, signature, }); } function SIWE(): React.Node { const { address } = useAccount(); const { data: signer } = useSigner(); const { siweNonce } = React.useContext(SIWENonceContext); const onClick = React.useCallback(() => { invariant(siweNonce, 'nonce must be present during SIWE attempt'); signInWithEthereum(address, signer, siweNonce); }, [address, signer, siweNonce]); const { openConnectModal } = useConnectModal(); const hasNonce = siweNonce !== null && siweNonce !== undefined; React.useEffect(() => { if (hasNonce && openConnectModal) { openConnectModal(); } }, [hasNonce, openConnectModal]); const prevConnectModalOpen = React.useRef(false); const modalState = useModalState(); const { connectModalOpen } = modalState; React.useEffect(() => { if (!connectModalOpen && prevConnectModalOpen.current && !signer) { postMessageToNativeWebView({ type: 'siwe_closed' }); } prevConnectModalOpen.current = connectModalOpen; }, [connectModalOpen, signer]); const newModalAppeared = React.useCallback(mutationList => { for (const mutation of mutationList) { for (const addedNode of mutation.addedNodes) { if ( addedNode instanceof HTMLElement && addedNode.id === 'walletconnect-wrapper' ) { postMessageToNativeWebView({ type: 'walletconnect_modal_update', state: 'open', }); } } for (const addedNode of mutation.removedNodes) { if ( addedNode instanceof HTMLElement && addedNode.id === 'walletconnect-wrapper' ) { postMessageToNativeWebView({ type: 'walletconnect_modal_update', state: 'closed', }); } } } }, []); React.useEffect(() => { const observer = new MutationObserver(newModalAppeared); invariant(document.body, 'document.body should be set'); observer.observe(document.body, { childList: true }); return () => { observer.disconnect(); }; }, [newModalAppeared]); if (!hasNonce) { return (

Unable to proceed: nonce not found.

); } else if (!signer) { return null; } else { return (
Wallet Connected:

To complete the login process, you'll now be asked to sign a message using your wallet.

This signature will attest that your Ethereum identity is represented by your new Comm identity.

Sign in
); } } function SIWEWrapper(): React.Node { const theme = React.useMemo(() => { return _merge(darkTheme())({ radii: { modal: 0, modalMobile: 0, }, colors: { modalBackdrop: '#242529', }, }); }, []); return ( ); } export default SIWEWrapper; diff --git a/landing/webpack.config.cjs b/landing/webpack.config.cjs index 33ad66f8c..357316730 100644 --- a/landing/webpack.config.cjs +++ b/landing/webpack.config.cjs @@ -1,82 +1,104 @@ const AssetsPlugin = require('assets-webpack-plugin'); const path = require('path'); +const webpack = require('webpack'); const { createProdBrowserConfig, createDevBrowserConfig, createNodeServerRenderingConfig, } = require('lib/webpack/shared.cjs'); const babelConfig = require('./babel.config.cjs'); const baseBrowserConfig = { entry: { browser: ['./script.js'], }, output: { filename: 'prod.[hash:12].build.js', path: path.join(__dirname, 'dist'), }, resolve: { alias: { '../images': path.resolve('../keyserver/images'), }, }, }; const baseDevBrowserConfig = { ...baseBrowserConfig, output: { ...baseBrowserConfig.output, filename: 'dev.build.js', pathinfo: true, publicPath: 'http://localhost:8082/', }, devServer: { hot: true, port: 8082, contentBase: path.join(__dirname, 'dist'), headers: { 'Access-Control-Allow-Origin': '*' }, allowedHosts: ['all'], host: '0.0.0.0', }, }; const baseProdBrowserConfig = { ...baseBrowserConfig, plugins: [ new AssetsPlugin({ filename: 'assets.json', path: path.join(__dirname, 'dist'), }), ], }; const baseNodeServerRenderingConfig = { externals: ['react', 'react-dom', 'react-redux'], entry: { server: ['./landing-ssr.react.js'], }, output: { filename: 'landing.build.cjs', library: 'landing', libraryTarget: 'commonjs2', path: path.join(__dirname, 'dist'), }, }; +const alchemyKey = process.env.COMM_ALCHEMY_KEY; + module.exports = function (env) { - const browserConfig = + const rawBrowserConfig = env === 'prod' ? createProdBrowserConfig(baseProdBrowserConfig, babelConfig) : createDevBrowserConfig(baseDevBrowserConfig, babelConfig); - const nodeConfig = createNodeServerRenderingConfig( + const browserConfig = { + ...rawBrowserConfig, + plugins: [ + ...rawBrowserConfig.plugins, + new webpack.DefinePlugin({ + 'process.env': { + COMM_ALCHEMY_KEY: JSON.stringify(alchemyKey), + }, + }), + ], + }; + const baseNodeConfig = createNodeServerRenderingConfig( baseNodeServerRenderingConfig, babelConfig, ); - const nodeServerRenderingConfig = { - ...nodeConfig, + const nodeConfig = { + ...baseNodeConfig, mode: env === 'prod' ? 'production' : 'development', + plugins: [ + ...baseNodeConfig.plugins, + new webpack.DefinePlugin({ + 'process.env': { + COMM_ALCHEMY_KEY: JSON.stringify(alchemyKey), + }, + }), + ], }; - return [browserConfig, nodeServerRenderingConfig]; + return [browserConfig, nodeConfig]; };