diff --git a/lib/blacklist.js b/lib/blacklist.js index 15bd7b12..89a9037f 100644 --- a/lib/blacklist.js +++ b/lib/blacklist.js @@ -1,16 +1,16 @@ +const _ = require('lodash/fp') + const db = require('./db') const notifierQueries = require('./notifier/queries') -// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator const getBlacklist = () => { - return db.any(`SELECT * FROM blacklist`).then(res => - res.map(item => ({ - address: item.address - })) - ) + const blacklistSql = `SELECT * FROM blacklist` + const messagesSql = `SELECT * FROM blacklist_messages` + return Promise.all([db.any(blacklistSql), db.any(messagesSql)]) + .then(([blacklist, messages]) => Promise.all([_.map(_.mapKeys(_.camelCase), blacklist), _.map(_.mapKeys(_.camelCase), messages)])) + .then(([blacklist, messages]) => _.map(it => ({ ...it, blacklistMessage: _.find(ite => it.blacklistMessageId === ite.id, messages) }), blacklist)) } -// Delete row from blacklist table by crypto code and address const deleteFromBlacklist = address => { const sql = `DELETE FROM blacklist WHERE address = $1` notifierQueries.clearBlacklistNotification(address) @@ -38,10 +38,22 @@ function addToUsedAddresses (address) { return db.oneOrNone(sql, [address]) } +function getMessages () { + const sql = `SELECT * FROM blacklist_messages` + return db.any(sql) +} + +function editBlacklistMessage (id, content) { + const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id` + return db.oneOrNone(sql, [content, id]) +} + module.exports = { blocked, addToUsedAddresses, getBlacklist, deleteFromBlacklist, - insertIntoBlacklist + insertIntoBlacklist, + getMessages, + editBlacklistMessage } diff --git a/lib/new-admin/graphql/resolvers/blacklist.resolver.js b/lib/new-admin/graphql/resolvers/blacklist.resolver.js index 7cb825cc..e0b63d53 100644 --- a/lib/new-admin/graphql/resolvers/blacklist.resolver.js +++ b/lib/new-admin/graphql/resolvers/blacklist.resolver.js @@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist') const resolvers = { Query: { - blacklist: () => blacklist.getBlacklist() + blacklist: () => blacklist.getBlacklist(), + blacklistMessages: () => blacklist.getMessages() }, Mutation: { deleteBlacklistRow: (...[, { address }]) => blacklist.deleteFromBlacklist(address), insertBlacklistRow: (...[, { address }]) => - blacklist.insertIntoBlacklist(address) + blacklist.insertIntoBlacklist(address), + editBlacklistMessage: (...[, { id, content }]) => + blacklist.editBlacklistMessage(id, content) } } diff --git a/lib/new-admin/graphql/types/blacklist.type.js b/lib/new-admin/graphql/types/blacklist.type.js index ee9ee149..7cc34721 100644 --- a/lib/new-admin/graphql/types/blacklist.type.js +++ b/lib/new-admin/graphql/types/blacklist.type.js @@ -3,15 +3,25 @@ const { gql } = require('apollo-server-express') const typeDef = gql` type Blacklist { address: String! + blacklistMessage: BlacklistMessage! + } + + type BlacklistMessage { + id: ID + label: String + content: String + allowToggle: Boolean } type Query { blacklist: [Blacklist] @auth + blacklistMessages: [BlacklistMessage] @auth } type Mutation { deleteBlacklistRow(address: String!): Blacklist @auth insertBlacklistRow(address: String!): Blacklist @auth + editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth } ` diff --git a/migrations/1664916753772-advanced-blacklisting.js b/migrations/1664916753772-advanced-blacklisting.js new file mode 100644 index 00000000..915c1f33 --- /dev/null +++ b/migrations/1664916753772-advanced-blacklisting.js @@ -0,0 +1,26 @@ +const uuid = require('uuid') + +var db = require('./db') + +exports.up = function (next) { + const defaultMessageId = uuid.v4() + + var sql = [ + `CREATE TABLE blacklist_messages ( + id UUID PRIMARY KEY, + label TEXT NOT NULL, + content TEXT NOT NULL, + allow_toggle BOOLEAN NOT NULL DEFAULT true + )`, + `INSERT INTO blacklist_messages (id, label, content, allow_toggle) VALUES ('${defaultMessageId}', 'Suspicious address', 'This address may be associated with a deceptive offer or a prohibited group. Please make sure you''re using an address from your own wallet.', false)`, + `ALTER TABLE blacklist ADD COLUMN blacklist_message_id UUID REFERENCES blacklist_messages(id)`, + `UPDATE blacklist SET blacklist_message_id = '${defaultMessageId}'`, + `ALTER TABLE blacklist ALTER COLUMN blacklist_message_id SET NOT NULL` + ] + + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js index 7726ddba..59c97e64 100644 --- a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js @@ -1,7 +1,6 @@ import { useQuery, useMutation } from '@apollo/react-hooks' import { addressDetector } from '@lamassu/coins' import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core' -import Grid from '@material-ui/core/Grid' import { makeStyles } from '@material-ui/core/styles' import gql from 'graphql-tag' import * as R from 'ramda' @@ -13,9 +12,12 @@ import { Switch } from 'src/components/inputs' import TitleSection from 'src/components/layout/TitleSection' import { H2, Label2, P, Info3, Info2 } from 'src/components/typography' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' +import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg' +import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg' import { fromNamespace, toNamespace } from 'src/utils/config' import styles from './Blacklist.styles' +import BlackListAdvanced from './BlacklistAdvanced' import BlackListModal from './BlacklistModal' import BlacklistTable from './BlacklistTable' @@ -33,6 +35,11 @@ const GET_BLACKLIST = gql` query getBlacklistData { blacklist { address + blacklistMessage { + id + label + content + } } cryptoCurrencies { display @@ -61,6 +68,25 @@ const ADD_ROW = gql` } ` +const GET_BLACKLIST_MESSAGES = gql` + query getBlacklistMessages { + blacklistMessages { + id + label + content + allowToggle + } + } +` + +const EDIT_BLACKLIST_MESSAGE = gql` + mutation editBlacklistMessage($id: ID, $content: String) { + editBlacklistMessage(id: $id, content: $content) { + id + } + } +` + const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => { const classes = useStyles() @@ -106,10 +132,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => { const Blacklist = () => { const { data: blacklistResponse } = useQuery(GET_BLACKLIST) const { data: configData } = useQuery(GET_INFO) + const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES) const [showModal, setShowModal] = useState(false) const [errorMsg, setErrorMsg] = useState(null) + const [editMessageError, setEditMessageError] = useState(null) const [deleteDialog, setDeleteDialog] = useState(false) const [confirmDialog, setConfirmDialog] = useState(false) + const [advancedSettings, setAdvancedSettings] = useState(false) const [deleteEntry] = useMutation(DELETE_ROW, { onError: ({ message }) => { @@ -129,6 +158,11 @@ const Blacklist = () => { refetchQueries: () => ['getData'] }) + const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, { + onError: e => setEditMessageError(e), + refetchQueries: () => ['getBlacklistData'] + }) + const classes = useStyles() const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? [] @@ -184,6 +218,15 @@ const Blacklist = () => { } } + const editBlacklistMessage = r => { + editMessage({ + variables: { + id: r.id, + content: r.content + } + }) + } + return ( <> { setConfirmDialog(false) }} /> - - - -

Enable paper wallet (only)

- - enablePaperWalletOnly - ? addressReuseSave({ - enablePaperWalletOnly: e.target.checked - }) - : setConfirmDialog(true) - } - value={enablePaperWalletOnly} - /> - {enablePaperWalletOnly ? 'On' : 'Off'} - -

- The "Enable paper wallet (only)" option means that only paper - wallets will be printed for users, and they won't be permitted - to scan an address from their own wallet. -

-
+ + {!advancedSettings && ( + + +

Enable paper wallet (only)

+ + enablePaperWalletOnly + ? addressReuseSave({ + enablePaperWalletOnly: e.target.checked + }) + : setConfirmDialog(true) + } + value={enablePaperWalletOnly} + /> + {enablePaperWalletOnly ? 'On' : 'Off'} + +

+ The "Enable paper wallet (only)" option means that only paper + wallets will be printed for users, and they won't be permitted + to scan an address from their own wallet. +

+
+
+ +

Reject reused addresses

+ { + addressReuseSave({ rejectAddressReuse: event.target.checked }) + }} + value={rejectAddressReuse} + /> + {rejectAddressReuse ? 'On' : 'Off'} + +

+ The "Reject reused addresses" option means that all addresses + that are used once will be automatically rejected if there's + an attempt to use them again on a new transaction. +

+
+
+ setShowModal(true)}> + Blacklist new addresses +
- -

Reject reused addresses

- { - addressReuseSave({ rejectAddressReuse: event.target.checked }) - }} - value={rejectAddressReuse} - /> - {rejectAddressReuse ? 'On' : 'Off'} - -

- The "Reject reused addresses" option means that all addresses - that are used once will be automatically rejected if there's an - attempt to use them again on a new transaction. -

-
-
- setShowModal(true)}> - Blacklist new addresses - -
+ )}
- + {!advancedSettings && (
{ setDeleteDialog={setDeleteDialog} />
-
+ )} + {advancedSettings && ( + refetch()} + /> + )} {showModal && ( { diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js index 6e4a09e4..3cfc09ec 100644 --- a/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js @@ -9,6 +9,14 @@ const styles = { flexDirection: 'column', flex: 1 }, + advancedForm: { + '& > *': { + marginTop: 20 + }, + display: 'flex', + flexDirection: 'column', + height: '100%' + }, footer: { display: 'flex', flexDirection: 'row', @@ -58,6 +66,9 @@ const styles = { cancelButton: { marginRight: 8, padding: 0 + }, + resetToDefault: { + width: 145 } } diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js new file mode 100644 index 00000000..4c764b46 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js @@ -0,0 +1,177 @@ +import { makeStyles } from '@material-ui/core/styles' +import { Form, Formik, Field } from 'formik' +import * as R from 'ramda' +import React, { useState } from 'react' +import * as Yup from 'yup' + +import ErrorMessage from 'src/components/ErrorMessage' +import Modal from 'src/components/Modal' +import { ActionButton, IconButton, Button } from 'src/components/buttons' +import { TextInput } from 'src/components/inputs/formik' +import DataTable from 'src/components/tables/DataTable' +import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg' +import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' +import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' +import { ReactComponent as DefaultIconReverse } from 'src/styling/icons/button/retry/white.svg' +import { ReactComponent as DefaultIcon } from 'src/styling/icons/button/retry/zodiac.svg' + +import styles from './Blacklist.styles' + +const useStyles = makeStyles(styles) + +const DEFAULT_MESSAGE = `This address may be associated with a deceptive offer or a prohibited group. Please make sure you're using an address from your own wallet.` + +const getErrorMsg = (formikErrors, formikTouched, mutationError) => { + if (!formikErrors || !formikTouched) return null + if (mutationError) return 'Internal server error' + if (formikErrors.event && formikTouched.event) return formikErrors.event + if (formikErrors.message && formikTouched.message) return formikErrors.message + return null +} + +const BlacklistAdvanced = ({ + data, + editBlacklistMessage, + onClose, + mutationError +}) => { + const classes = useStyles() + const [selectedMessage, setSelectedMessage] = useState(null) + + const elements = [ + { + name: 'label', + header: 'Label', + width: 250, + textAlign: 'left', + size: 'sm', + view: it => R.path(['label'], it) + }, + { + name: 'content', + header: 'Content', + width: 690, + textAlign: 'left', + size: 'sm', + view: it => R.path(['content'], it) + }, + { + name: 'edit', + header: 'Edit', + width: 130, + textAlign: 'center', + size: 'sm', + view: it => ( + setSelectedMessage(it)}> + + + ) + }, + { + name: 'deleteButton', + header: 'Delete', + width: 130, + textAlign: 'center', + size: 'sm', + view: it => ( + + {R.path(['allowToggle'], it) ? ( + + ) : ( + + )} + + ) + } + ] + + const handleModalClose = () => { + setSelectedMessage(null) + } + + const handleSubmit = values => { + editBlacklistMessage(values) + handleModalClose() + !R.isNil(onClose) && onClose() + } + + const initialValues = { + label: !R.isNil(selectedMessage) ? selectedMessage.label : '', + content: !R.isNil(selectedMessage) ? selectedMessage.content : '' + } + + const validationSchema = Yup.object().shape({ + label: Yup.string().required('A label is required!'), + content: Yup.string() + .required('The message content is required!') + .trim() + }) + + return ( + <> + + {selectedMessage && ( + + + handleSubmit({ id: selectedMessage.id, ...values }) + }> + {({ errors, touched, setFieldValue }) => ( +
+ setFieldValue('content', DEFAULT_MESSAGE)}> + Reset to default + + +
+ {getErrorMsg(errors, touched, mutationError) && ( + + {getErrorMsg(errors, touched, mutationError)} + + )} + +
+ + )} +
+
+ )} + + ) +} + +export default BlacklistAdvanced