From 5803876b6ab6672ce82ebe3e641a8729993743ef Mon Sep 17 00:00:00 2001 From: e-schneid <99349687+e-schneid@users.noreply.github.com> Date: Thu, 26 May 2022 15:12:12 -0700 Subject: [PATCH] feat: add user blocking functionality to web3 (#1322) --- packages/api/src/user.js | 14 +++- packages/api/src/utils/tags.js | 14 ++++ packages/db/index.js | 16 ++++- .../accountBlockedModal.js | 46 +++++++++++++ .../accountBlockedModal.scss | 54 ++++++++++++++++ .../components/account/ctaCard/CTACard.js | 29 +++++++-- .../components/account/ctaCard/CTACard.scss | 10 +++ .../account/filesManager/filesManager.js | 5 ++ packages/website/components/button/button.js | 13 ++-- .../website/components/button/button.scss | 64 +++++++++++++++---- .../components/contexts/userContext.js | 19 +++++- .../tokens/tokenCreator/tokenCreator.js | 8 +++ .../website/content/pages/app/account.json | 12 ++-- .../modules/zero/components/button/button.js | 55 +++++++--------- .../zero/components/tooltip/tooltip.js | 6 +- .../zero/components/tooltip/tooltip.scss | 4 +- packages/website/pages/account/index.js | 17 ++++- packages/website/pages/account/index.scss | 1 - packages/website/styles/global.scss | 1 + 19 files changed, 316 insertions(+), 72 deletions(-) create mode 100644 packages/api/src/utils/tags.js create mode 100644 packages/website/components/account/accountBlockedModal/accountBlockedModal.js create mode 100644 packages/website/components/account/accountBlockedModal/accountBlockedModal.scss diff --git a/packages/api/src/user.js b/packages/api/src/user.js index d82e4fd277..09159daeda 100644 --- a/packages/api/src/user.js +++ b/packages/api/src/user.js @@ -2,6 +2,7 @@ import * as JWT from './utils/jwt.js' import { JSONResponse } from './utils/json-response.js' import { JWT_ISSUER } from './constants.js' import { HTTPError } from './errors.js' +import { getTagValue, hasTag } from './utils/tags.js' /** * @typedef {{ _id: string, issuer: string }} User @@ -124,9 +125,18 @@ export async function userAccountGet (request, env) { * @param {import('./env').Env} env */ export async function userInfoGet (request, env) { - const info = await env.db.getUser(request.auth.user.issuer) + const user = await env.db.getUser(request.auth.user.issuer, { includeTags: true }) + return new JSONResponse({ - info + info: { + ...user, + tags: { + HasAccountRestriction: hasTag(user, 'HasAccountRestriction', 'true'), + HasPsaAccess: hasTag(user, 'HasPsaAccess', 'true'), + HasSuperHotAccess: hasTag(user, 'HasSuperHotAccess', 'true'), + StorageLimitBytes: getTagValue(user, 'StorageLimitBytes', '') + } + } }) } diff --git a/packages/api/src/utils/tags.js b/packages/api/src/utils/tags.js new file mode 100644 index 0000000000..8cfdbce3fa --- /dev/null +++ b/packages/api/src/utils/tags.js @@ -0,0 +1,14 @@ +export function getTagValue (user, tagName, defaultValue) { + return ( + user.tags?.find((tag) => tag.tag === tagName && !tag.deleted_at)?.value || + defaultValue + ) +} + +export function hasTag (user, tagName, value) { + return Boolean( + user.tags?.find( + (tag) => tag.tag === tagName && tag.value === value && !tag.deleted_at + ) + ) +} diff --git a/packages/db/index.js b/packages/db/index.js index d51cacc67e..b47227c74b 100644 --- a/packages/db/index.js +++ b/packages/db/index.js @@ -33,6 +33,18 @@ const userQuery = ` updated:updated_at ` +const userQueryWithTags = ` + _id:id::text, + issuer, + name, + email, + github, + publicAddress:public_address, + created:inserted_at, + updated:updated_at, + tags:user_tag_user_id_fkey(user_id,id,tag,value) +` + const psaPinRequestTableName = 'psa_pin_request' const pinRequestSelect = ` _id:id::text, @@ -126,11 +138,11 @@ export class DBClient { * @param {string} issuer * @return {Promise} */ - async getUser (issuer) { + async getUser (issuer, { includeTags } = { includeTags: false }) { /** @type {{ data: import('./db-client-types').UserOutput[], error: PostgrestError }} */ const { data, error } = await this._client .from('user') - .select(userQuery) + .select(includeTags ? userQueryWithTags : userQuery) .eq('issuer', issuer) if (error) { diff --git a/packages/website/components/account/accountBlockedModal/accountBlockedModal.js b/packages/website/components/account/accountBlockedModal/accountBlockedModal.js new file mode 100644 index 0000000000..fc07403c34 --- /dev/null +++ b/packages/website/components/account/accountBlockedModal/accountBlockedModal.js @@ -0,0 +1,46 @@ +import { useState, useEffect } from 'react'; +import Link from 'next/link'; + +import Modal from 'modules/zero/components/modal/modal'; +import CloseIcon from 'assets/icons/close'; +import Button from 'components/button/button.js'; +import GradientBackground from '../../gradientbackground/gradientbackground.js'; + +const AccountBlockedModal = ({ hasAccountRestriction }) => { + const modalState = useState(false); + + useEffect(() => { + if (hasAccountRestriction && !sessionStorage.hasSeenAccountBlockedModal) { + modalState[1](true); + sessionStorage.hasSeenAccountBlockedModal = true; + } + }, [hasAccountRestriction, modalState]); + + return ( +
+ } + modalState={modalState} + showCloseButton + > +
+ +
+
+

+ You may have been temporarily blocked from uploading new files. You may, however, continue to view and take + actions on existing uploads. If you feel this was a mistake please contact{' '} + support@web3.storage.com +

+ + +
+
+
+ ); +}; + +export default AccountBlockedModal; diff --git a/packages/website/components/account/accountBlockedModal/accountBlockedModal.scss b/packages/website/components/account/accountBlockedModal/accountBlockedModal.scss new file mode 100644 index 0000000000..dc655ca1f7 --- /dev/null +++ b/packages/website/components/account/accountBlockedModal/accountBlockedModal.scss @@ -0,0 +1,54 @@ +.account-blocked-container { + position: relative; + color: $ebony; + padding: 3.125rem 8.5rem 3.125rem 8.125rem; + + @include medium { + padding: 2.375rem 1.3125rem; + width: 100%; + } + + > * { + position: relative; + } + + .saturated-variant { + @include mini { + transform: translate(-5rem, 0) scaleY(2) rotate(-40deg) !important; + } + @include tiny { + width: 1000px !important; + } + } + + .content { + margin-bottom: 20px; + } +} + +.account-blocked-modal { + .modalContainer { + max-width: calc($containerWidth - 10%); + @include borderRadius_Large; + @include medium { + width: 90%; + } + } + .modalClose { + color: $ebony; + font-size: 2rem; + } + + svg { + color: $ebony; + } +} + +.background-view-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} diff --git a/packages/website/components/account/ctaCard/CTACard.js b/packages/website/components/account/ctaCard/CTACard.js index 4bcaa03b31..b7a0a294de 100644 --- a/packages/website/components/account/ctaCard/CTACard.js +++ b/packages/website/components/account/ctaCard/CTACard.js @@ -1,6 +1,7 @@ import clsx from 'clsx'; -import Button from 'components/button/button'; +import Button, { ButtonVariant } from 'components/button/button'; +import Tooltip from 'ZeroComponents/tooltip/tooltip'; export const CTAThemeType = { LIGHT: 'light', @@ -25,16 +26,30 @@ export const CTAThemeType = { const CTACard = ({ className = '', heading, description, ctas, theme = CTAThemeType.LIGHT, background }) => { return (
- {background} +
{background}

{heading}

{description} {!!ctas?.length && (
- {ctas.map(({ onClick = () => null, children: text, ...buttonProps }) => ( - - ))} + {ctas.map(({ onClick = () => null, children: text, ...buttonProps }) => { + const btn = ( + + ); + + if (buttonProps.tooltip) { + return {btn}; + } + + return btn; + })}
)}
diff --git a/packages/website/components/account/ctaCard/CTACard.scss b/packages/website/components/account/ctaCard/CTACard.scss index 7c9f903230..e69fcb23ed 100644 --- a/packages/website/components/account/ctaCard/CTACard.scss +++ b/packages/website/components/account/ctaCard/CTACard.scss @@ -22,6 +22,16 @@ $cta-card-padding-top: 5rem; padding-bottom: 1.5625rem; @include label_4; } + + &__background { + position: absolute; + border-radius: 0.625rem; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + } } .cta-buttons-container { diff --git a/packages/website/components/account/filesManager/filesManager.js b/packages/website/components/account/filesManager/filesManager.js index d7d9ffcd9d..7023d1ce53 100644 --- a/packages/website/components/account/filesManager/filesManager.js +++ b/packages/website/components/account/filesManager/filesManager.js @@ -48,6 +48,7 @@ const FilesManager = ({ className, content, onFileUpload }) => { } = useRouter(); const { storageData: { refetch }, + info, } = useUser(); const [filteredFiles, setFilteredFiles] = useState(files); const [sortedFiles, setSortedFiles] = useState(filteredFiles); @@ -192,6 +193,7 @@ const FilesManager = ({ className, content, onFileUpload }) => {
{content?.heading}
@@ -257,6 +260,8 @@ const FilesManager = ({ className, content, onFileUpload }) => { action: content?.table.cta.action, data: { isFirstFile: true }, }} + disabled={info?.tags?.['HasAccountRestriction']} + tooltip={info?.tags?.['HasAccountRestriction'] ? content?.table.cta.accountRestrictedText : ''} > {content?.table.cta.text} diff --git a/packages/website/components/button/button.js b/packages/website/components/button/button.js index 7d71b817c3..9af8a56583 100644 --- a/packages/website/components/button/button.js +++ b/packages/website/components/button/button.js @@ -3,8 +3,10 @@ import React, { useCallback } from 'react'; import ZeroButton from 'ZeroComponents/button/button'; import { trackEvent, events } from 'lib/countly'; +import Tooltip from 'ZeroComponents/tooltip/tooltip'; export const ButtonVariant = { + GRAY: 'gray', DARK: 'dark', LIGHT: 'light', PURPLE: 'purple', @@ -26,6 +28,7 @@ export const ButtonVariant = { * @prop {React.MouseEventHandler} [onClick] * @prop {string} [className] * @prop {string} [href] + * @prop {string} [tooltip] * @prop {TrackingProps} [tracking] * @prop {string} [variant] * @prop {React.ReactNode} [children] @@ -37,27 +40,29 @@ export const ButtonVariant = { * @param {ButtonProps & Partial, 'children'>>} props * @returns */ -const Button = ({ className, onClick, href, tracking, variant = ButtonVariant.DARK, children, ...props }) => { +const Button = ({ className, tooltip, onClick, tracking, variant = ButtonVariant.DARK, children, ...props }) => { const onClickHandler = useCallback( event => { tracking && trackEvent(tracking.event || events.CTA_LINK_CLICK, { ui: tracking.ui, action: tracking.action, - link: href || '', + link: props.href || '', ...(tracking.data || {}), }); onClick && onClick(event); }, - [href, onClick, tracking] + [props.href, onClick, tracking] ); - return ( + const btn = ( // @ts-ignore Ignoring ZeroButton as it is not properly typed {children} ); + + return tooltip ? {btn} : btn; }; export default Button; diff --git a/packages/website/components/button/button.scss b/packages/website/components/button/button.scss index 2170cca5c8..832fabcb59 100644 --- a/packages/website/components/button/button.scss +++ b/packages/website/components/button/button.scss @@ -3,8 +3,6 @@ position: relative; display: inline-block; @include borderRadius_Huge; - @include fontSize_Tiny; - @include fontWeight_Semibold; letter-spacing: 0.03em; text-transform: uppercase; line-height: 1; @@ -17,7 +15,16 @@ cursor: default; } - button { + &.disabled { + .button-contents { + cursor: not-allowed; + } + } + + .button-contents { + @include fontSize_Tiny; + @include fontWeight_Semibold; + text-transform: none; background: transparent; box-sizing: border-box; border: none; @@ -104,12 +111,12 @@ } &:active { - button { + .button-contents { color: $ebony; } } - button { + .button-contents { color: $white; } } @@ -122,12 +129,12 @@ } &:active { - button { + .button-contents { color: $white; } } - button { + .button-contents { color: $ebony; } } @@ -136,7 +143,7 @@ background-color: unset; padding-left: 0; position: relative; - button { + .button-contents { color: $malibu; @include fontSize_Small; @include fontWeight_Semibold; @@ -163,7 +170,7 @@ &.blue { background-color: unset; padding: 0; - button { + .button-contents { color: $blue; @include fontWeight_Semibold; @include fontSize_Small; @@ -188,6 +195,35 @@ } } + &.gray { + position: relative; + + &:before, + &:after { + @include borderRadius_Huge; + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + + &:after { + transition: all $transitionDuration ease-in; + border: 0.0625rem solid $alto; + background: $charade; + opacity: 0.6; + @include shadow4; + } + + .button-contents { + @include fontWeight_Regular; + position: relative; + z-index: 1; + } + } + &.pink-blue { position: relative; @@ -214,7 +250,7 @@ @include shadow4; } - button { + .button-contents { @include fontWeight_Regular; position: relative; z-index: 1; @@ -238,7 +274,7 @@ color: $cyan; text-transform: none; - button { + .button-contents { @include fontWeight_Regular; } } @@ -291,11 +327,11 @@ } &.outline-light { - button { + .button-contents { color: $white; } &:hover { - button { + .button-contents { color: $ebony; } &:after { @@ -309,7 +345,7 @@ } &.outline-dark { - button { + .button-contents { color: $ebony; } &:hover { diff --git a/packages/website/components/contexts/userContext.js b/packages/website/components/contexts/userContext.js index 207d0c22a2..26b94778ec 100644 --- a/packages/website/components/contexts/userContext.js +++ b/packages/website/components/contexts/userContext.js @@ -3,6 +3,17 @@ import { useQuery } from 'react-query'; import { getInfo, getStorage } from 'lib/api.js'; import { useAuthorization } from './authorizationContext.js'; +import AccountBlockedModal from 'components/account/accountBlockedModal/accountBlockedModal.js'; + +/** + * @typedef Tags + * @property {boolean} [HasAccountRestriction] + */ + +/** + * @typedef {Object} Info + * @property {Tags} tags + */ /** * @typedef {Object} UserContextProps @@ -11,6 +22,7 @@ import { useAuthorization } from './authorizationContext.js'; * @property {string} name * @property {string} email * @property {string} github + * @property {Info} info * @property {string} publicAddress * @property {string} created * @property {string} updated @@ -43,7 +55,12 @@ export const UserProvider = ({ children, loadStorage }) => { enabled: isLoggedIn && loadStorage, }); - return {children}; + return ( + + + {children} + + ); }; /** diff --git a/packages/website/components/tokens/tokenCreator/tokenCreator.js b/packages/website/components/tokens/tokenCreator/tokenCreator.js index 25a5780959..bed77fdbfc 100644 --- a/packages/website/components/tokens/tokenCreator/tokenCreator.js +++ b/packages/website/components/tokens/tokenCreator/tokenCreator.js @@ -5,6 +5,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react import countly from 'lib/countly'; import Button, { ButtonVariant } from 'components/button/button'; import { useTokens } from 'components/contexts/tokensContext'; +import { useUser } from 'hooks/use-user'; /** * @typedef {Object} TokenCreatorProps @@ -22,6 +23,7 @@ const TokenCreator = ({ content }) => { const { query, push, replace } = useRouter(); const { tokens, createToken, isCreating, getTokens } = useTokens(); + const user = useUser(); const onTokenCreate = useCallback( async e => { @@ -114,10 +116,16 @@ const TokenCreator = ({ content }) => { diff --git a/packages/website/content/pages/app/account.json b/packages/website/content/pages/app/account.json index 6037007fcb..43685a9313 100644 --- a/packages/website/content/pages/app/account.json +++ b/packages/website/content/pages/app/account.json @@ -21,7 +21,8 @@ "text": "Create a Token", "theme": "pink-blue", "ui": "PROFILE_GETTING_STARTED", - "action": "Create an API Token" + "action": "Create an API Token", + "accountRestrictedText": "You are unable to create new tokens when your account is blocked. Please contact support@web3.storage" }, { "link": "/tokens", @@ -56,7 +57,8 @@ "text": "Upload Files", "theme": "outline-dark", "ui": "FILES", - "action": "Upload File" + "action": "Upload File", + "accountRestrictedText": "You are unable to upload files when your account is blocked. Please contact support@web3.storage" } ] } @@ -93,7 +95,8 @@ "text": "Upload +", "theme": "text", "ui": "FILES", - "action": "Upload File" + "action": "Upload File", + "accountRestrictedText": "You are unable to upload files when your account is blocked. Please contact support@web3.storage" }, "table": { "message": "You don't have any files uploaded yet.", @@ -101,7 +104,8 @@ "text": "Upload your first file", "theme": "text", "ui": "FILES", - "action": "Upload File" + "action": "Upload File", + "accountRestrictedText": "You are unable to upload files when your account is blocked. Please contact support@web3.storage" }, "file_row_labels": { "date": { diff --git a/packages/website/modules/zero/components/button/button.js b/packages/website/modules/zero/components/button/button.js index 737b6f9434..e8e7b5f57f 100644 --- a/packages/website/modules/zero/components/button/button.js +++ b/packages/website/modules/zero/components/button/button.js @@ -1,5 +1,5 @@ -import Link from 'next/link' -import clsx from 'clsx' +import Link from 'next/link'; +import clsx from 'clsx'; /** * @typedef {Object} ButtonProps @@ -13,41 +13,34 @@ import clsx from 'clsx' */ /** - * - * @param {ButtonProps} props - * @returns + * + * @param {ButtonProps} props + * @returns */ -const Button = ({ - className, - onClick, - href, - type, - disabled, - openInNewWindow, - children -}) => { - +const Button = ({ className, onClick, href, type, disabled, openInNewWindow, children }) => { return ( -
- { href - ? ( - openInNewWindow - ? - {children} - : - {children} +
+ {href && !disabled ? ( + openInNewWindow ? ( + + {children} + + ) : ( + {{children}} ) - : - - } + ) : ( + + )}
- ) -} + ); +}; Button.defaultProps = { - type: "button", + type: 'button', openInNewWindow: false, disabled: false, -} +}; -export default Button +export default Button; diff --git a/packages/website/modules/zero/components/tooltip/tooltip.js b/packages/website/modules/zero/components/tooltip/tooltip.js index 53971f5a64..04e38b3242 100644 --- a/packages/website/modules/zero/components/tooltip/tooltip.js +++ b/packages/website/modules/zero/components/tooltip/tooltip.js @@ -6,6 +6,7 @@ import InfoAIcon from 'assets/icons/infoA'; * @property {string} content * @property {string} [position] * @property {React.ReactNode} [icon] + * @property {string|React.ReactNode} [children] * @prop {string} [className] */ @@ -14,10 +15,11 @@ import InfoAIcon from 'assets/icons/infoA'; * @param {InfoProps} props * @returns */ -const Tooltip = ({ content, icon = null, className, position }) => { +const Tooltip = ({ children, content, icon = null, className, position }) => { return (
- {icon || } + {!children && (icon || )} + {children}
); diff --git a/packages/website/modules/zero/components/tooltip/tooltip.scss b/packages/website/modules/zero/components/tooltip/tooltip.scss index 87d0594aef..4aebc84758 100644 --- a/packages/website/modules/zero/components/tooltip/tooltip.scss +++ b/packages/website/modules/zero/components/tooltip/tooltip.scss @@ -26,6 +26,8 @@ $info-tooltip-center: calc(50% - #{math.div($info-icon-size, 2)}); } .tooltip-content { + font-size: 1rem; + line-height: 1.4375rem; transition: opacity 250ms; opacity: 0; pointer-events: none; @@ -42,7 +44,7 @@ $info-tooltip-center: calc(50% - #{math.div($info-icon-size, 2)}); max-width: 30vw; } color: $white; - z-index: 1; + z-index: 2; &:before { content: ""; width: 0; diff --git a/packages/website/pages/account/index.js b/packages/website/pages/account/index.js index f5721b03e2..f0e30847bf 100644 --- a/packages/website/pages/account/index.js +++ b/packages/website/pages/account/index.js @@ -1,4 +1,3 @@ -import Link from 'next/link'; import { useCallback, useMemo, useState } from 'react'; import StorageManager from '../../components/account/storageManager/storageManager'; @@ -9,6 +8,7 @@ import GradientBackground from '../../components/gradientbackground/gradientback import countly from 'lib/countly'; import AppData from '../../content/pages/app/account.json'; import { useUploads } from 'components/contexts/uploadsContext'; +import { useUser } from 'hooks/use-user'; export const CTACardTypes = { API_TOKENS: 'API_TOKENS', @@ -20,6 +20,7 @@ const Account = () => { const uploadModalState = useState(false); const dashboard = AppData.page_content.dashboard; const { uploads } = useUploads(); + const user = useUser(); const onFileUpload = useCallback(() => { uploadModalState[1](true); @@ -31,10 +32,15 @@ const Account = () => { heading: dashboard.card_left.heading, description: dashboard.card_left.description, ctas: dashboard.card_left.ctas.map(cta => ({ + disabled: user?.info?.tags?.['HasAccountRestriction'] && cta.accountRestrictedText, href: cta.link, variant: cta.theme, tracking: { ui: countly.ui[cta.ui], action: cta.action }, - children: {cta.text}, + children: cta.text, + tooltip: + user?.info?.tags?.['HasAccountRestriction'] && cta.accountRestrictedText + ? cta.accountRestrictedText + : undefined, })), }, [CTACardTypes.READ_DOCS]: { @@ -55,14 +61,19 @@ const Account = () => { heading: !!uploads.length ? dashboard.card_right.heading.option_1 : dashboard.card_right.heading.option_2, description: dashboard.card_right.description, ctas: dashboard.card_right.ctas.map(cta => ({ + disabled: user?.info?.tags?.['HasAccountRestriction'] && cta.accountRestrictedText, onClick: onFileUpload, variant: cta.theme, tracking: { ui: countly.ui[cta.ui], action: cta.action, isFirstFile: !uploads.length }, children: cta.text, + tooltip: + user?.info?.tags?.['HasAccountRestriction'] && cta.accountRestrictedText + ? cta.accountRestrictedText + : undefined, })), }, }), - [uploads.length, onFileUpload, dashboard] + [uploads.length, onFileUpload, dashboard, user] ); return ( diff --git a/packages/website/pages/account/index.scss b/packages/website/pages/account/index.scss index b79988ab22..2878055372 100644 --- a/packages/website/pages/account/index.scss +++ b/packages/website/pages/account/index.scss @@ -46,7 +46,6 @@ } &-upload-cta { grid-area: upload-cta; - overflow: hidden; position: relative; border: none; color: $ebony; diff --git a/packages/website/styles/global.scss b/packages/website/styles/global.scss index e40ebd223a..947237093c 100644 --- a/packages/website/styles/global.scss +++ b/packages/website/styles/global.scss @@ -41,6 +41,7 @@ @import '../components/tabs/tabs.scss'; @import '../components/callout/callout.scss'; @import '../components/search/search.scss'; +@import '../components/account/accountBlockedModal/accountBlockedModal.scss'; // Pages @import '../pages/index.scss';