From 977205f910ae16752af8a7528a9ce8659d03205c Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Mon, 13 Feb 2023 16:07:42 +0000 Subject: [PATCH] 2fa enable/disable logic --- README.md | 1 + api/package.json | 2 + api/src/controllers/user.js | 119 +++++++++++++++++++++++ api/src/index.js | 6 ++ api/src/schema/user.js | 6 ++ api/src/utils/validateConfig.js | 1 + client/pages/account.js | 167 ++++++++++++++++++++++++++++++++ config.example.js | 1 + yarn.lock | 146 +++++++++++++++++++++++++++- 9 files changed, 447 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23058c2..04366b6 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ If your configuration is not valid, sqtracker will fail to start. |----------------------------|---------|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | SQ_SITE_NAME | envs | sqtracker demo | The name of your tracker site | | SQ_SITE_DESCRIPTION | envs | My very own private tracker | A short description of your tracker site | +| SQ_THEME_COLOUR | envs | #f45d48 | A hex colour code used as the main theme colour of your site | | SQ_ALLOW_REGISTER | envs | `invite` | Registration mode. Either `open`, `invite` or `closed` | | SQ_ALLOW_ANONYMOUS_UPLOADS | envs | `false` | Whether or not users can upload torrents anonymously. Either `true` or `false` | | SQ_MINIMUM_RATIO | envs | 0.75 | Minimum allowed ratio. Below this users will not be able to download | diff --git a/api/package.json b/api/package.json index e929a29..629f310 100644 --- a/api/package.json +++ b/api/package.json @@ -26,8 +26,10 @@ "multer": "^1.4.2", "node-fetch": "^2.6.1", "nodemailer": "^6.7.8", + "qrcode": "^1.5.1", "qs": "^6.11.0", "slugify": "^1.6.5", + "speakeasy": "^2.0.0", "yup": "^0.32.11" }, "devDependencies": { diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 4e7448a..4baacb6 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -1,6 +1,8 @@ import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' import crypto from 'crypto' +import speakeasy from 'speakeasy' +import qrcode from 'qrcode' import User from '../schema/user' import Invite from '../schema/invite' import Progress from '../schema/progress' @@ -101,6 +103,9 @@ export const register = async (req, res) => { remainingInvites: 0, emailVerified: false, bonusPoints: 0, + totp: { + enabled: false, + }, }) newUser.uid = crypto @@ -426,6 +431,7 @@ export const fetchUser = async (req, res) => { remainingInvites: 1, banned: 1, bonusPoints: 1, + 'totp.enabled': 1, }, }, { @@ -825,3 +831,116 @@ export const unbanUser = async (req, res) => { res.status(500).send(e.message) } } + +export const generateTotpSecret = async (req, res) => { + try { + const user = await User.findOne({ _id: req.userId }).lean() + if (user.totp.enabled) { + res.status(409).send('TOTP already enabled') + return + } + + const secret = speakeasy.generateSecret({ length: 20 }) + const url = speakeasy.otpauthURL({ + secret: secret.ascii, + label: `${process.env.SQ_SITE_NAME}: ${user.username}`, + }) + const imageDataUrl = await qrcode.toDataURL(url) + + await User.findOneAndUpdate( + { _id: req.userId }, + { + $set: { + 'totp.secret': secret.base32, + 'totp.qr': imageDataUrl, + }, + } + ) + + res.json({ qr: imageDataUrl, secret: secret.base32 }) + } catch (e) { + res.status(500).send(e.message) + } +} + +export const enableTotp = async (req, res) => { + if (req.body.token) { + try { + const user = await User.findOne({ _id: req.userId }).lean() + if (user.totp.enabled) { + res.status(409).send('TOTP already enabled') + return + } + + const validToken = speakeasy.totp.verify({ + secret: user.totp.secret, + encoding: 'base32', + token: req.body.token, + window: 1, + }) + + if (!validToken) { + res.status(400).send('Invalid TOTP code') + return + } + + const backupCodes = [...Array(10)].map(() => + crypto.randomBytes(32).toString('hex').slice(0, 10) + ) + + await User.findOneAndUpdate( + { _id: req.userId }, + { + $set: { + 'totp.enabled': true, + 'totp.backup': backupCodes, + }, + } + ) + + res.send(backupCodes.join(',')) + } catch (e) { + res.status(500).send(e.message) + } + } else { + res.status(400).send('Request must include token') + } +} + +export const disableTotp = async (req, res) => { + if (req.body.token) { + try { + const user = await User.findOne({ _id: req.userId }).lean() + + const validToken = speakeasy.totp.verify({ + secret: user.totp.secret, + encoding: 'base32', + token: req.body.token, + window: 1, + }) + + if (!validToken) { + res.status(400).send('Invalid TOTP code') + return + } + + await User.findOneAndUpdate( + { _id: req.userId }, + { + $set: { + 'totp.enabled': false, + 'totp.secret': '', + 'totp.qr': '', + 'totp.backup': [], + }, + } + ) + + res.sendStatus(200) + } catch (e) { + res.status(500).send(e.message) + } + } else { + res.status(400).send('Request must include token') + } +} diff --git a/api/src/index.js b/api/src/index.js index 2e180c4..ef33677 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -26,6 +26,9 @@ import { banUser, unbanUser, buyItems, + generateTotpSecret, + enableTotp, + disableTotp, } from './controllers/user' import { uploadTorrent, @@ -186,6 +189,9 @@ app.post('/account/buy', buyItems) app.get('/user/:username', fetchUser) app.post('/user/ban/:username', banUser) app.post('/user/unban/:username', unbanUser) +app.get('/account/totp/generate', generateTotpSecret) +app.post('/account/totp/enable', enableTotp) +app.post('/account/totp/disable', disableTotp) // torrent routes app.post('/torrent/upload', uploadTorrent) diff --git a/api/src/schema/user.js b/api/src/schema/user.js index 7c8f44a..97ccd94 100644 --- a/api/src/schema/user.js +++ b/api/src/schema/user.js @@ -13,6 +13,12 @@ const User = new mongoose.Schema({ remainingInvites: Number, emailVerified: Boolean, bonusPoints: Number, + totp: { + enabled: Boolean, + secret: String, + qr: String, + backup: [String], + }, }) export default mongoose.model('user', User) diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js index 2398f93..3c56d4f 100644 --- a/api/src/utils/validateConfig.js +++ b/api/src/utils/validateConfig.js @@ -10,6 +10,7 @@ const configSchema = yup .object({ SQ_SITE_NAME: yup.string().min(1).max(20).required(), SQ_SITE_DESCRIPTION: yup.string().min(1).max(80).required(), + SQ_THEME_COLOUR: yup.string().matches(/#([a-f0-9]){6}/i), SQ_ALLOW_REGISTER: yup .string() .oneOf(['open', 'invite', 'closed']) diff --git a/client/pages/account.js b/client/pages/account.js index 27d6b08..ce5aa38 100644 --- a/client/pages/account.js +++ b/client/pages/account.js @@ -71,6 +71,9 @@ const Account = ({ token, invites = [], user, userRole }) => { const [invitesList, setInvitesList] = useState(invites) const [showInviteModal, setShowInviteModal] = useState(false) const [bonusPoints, setBonusPoints] = useState(user.bonusPoints ?? 0) + const [totpEnabled, setTotpEnabled] = useState(user.totp.enabled) + const [totpQrData, setTotpQrData] = useState() + const [totpBackupCodes, setTotpBackupCodes] = useState() const { addNotification } = useContext(NotificationContext) const { setLoading } = useContext(LoadingContext) @@ -213,6 +216,72 @@ const Account = ({ token, invites = [], user, userRole }) => { setLoading(false) } + const handleToggleTotp = async (e) => { + e.preventDefault() + const form = new FormData(e.target) + + try { + if (!totpEnabled) { + const totpToken = form.get('token') + + if (totpToken) { + const enableRes = await fetch(`${SQ_API_URL}/account/totp/enable`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ token: totpToken }), + }) + if (enableRes.status === 200) { + const backupCodes = await enableRes.text() + setTotpBackupCodes(backupCodes) + setTotpQrData(undefined) + setTotpEnabled(true) + addNotification('success', '2FA enabled') + } else { + const message = await enableRes.text() + addNotification('error', message) + } + } else { + const generateRes = await fetch( + `${SQ_API_URL}/account/totp/generate`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + } + ) + const totpData = await generateRes.json() + setTotpQrData(totpData) + } + } else { + const totpToken = form.get('token') + + const disableRes = await fetch(`${SQ_API_URL}/account/totp/disable`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ token: totpToken }), + }) + + if (disableRes.status === 200) { + setTotpEnabled(false) + addNotification('success', '2FA disabled') + } else { + const message = await disableRes.text() + addNotification('error', message) + } + } + } catch (e) { + addNotification('error', `Could not toggle 2FA: ${e.message}`) + console.error(e) + } + } + return ( <> @@ -350,6 +419,104 @@ const Account = ({ token, invites = [], user, userRole }) => { ]} mb={5} /> + + + Two-factor authentication + + + Use an authenticator app to add another layer of security to your + account. + +
+ {totpQrData ? ( + + + + + {totpQrData.secret} + + + + + Scan the QR code with your authenticator app and enter the + one-time code + + + + + + + + + ) : ( + <> + {totpBackupCodes ? ( + <> + + 2FA enabled successfully. These backup codes can be used to + log in if you lose access to your authenticator app. Save + them now, they will not be visible again. + + + {totpBackupCodes.split(',').map((code) => ( + + {code} + + ))} + + + ) : ( + <> + {totpEnabled && ( + + )} + + + )} + + )} + +
Change password diff --git a/config.example.js b/config.example.js index e4f212e..bcffb99 100644 --- a/config.example.js +++ b/config.example.js @@ -7,6 +7,7 @@ module.exports = { envs: { SQ_SITE_NAME: 'sqtracker demo', SQ_SITE_DESCRIPTION: 'A short description for your tracker site', + SQ_THEME_COLOUR: '#f45d48', SQ_ALLOW_REGISTER: 'invite', SQ_ALLOW_ANONYMOUS_UPLOADS: false, SQ_MINIMUM_RATIO: 0.75, diff --git a/yarn.lock b/yarn.lock index 91bf951..4d86fd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1839,6 +1839,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base32.js@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba" + integrity sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2014,6 +2019,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelcase@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" @@ -2094,6 +2104,15 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2368,6 +2387,11 @@ debug@^4.0.0: dependencies: ms "2.1.2" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + decode-named-character-reference@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz#57b2bd9112659cacbc449d3577d7dadb8e1f3d1b" @@ -2452,6 +2476,11 @@ diff@^5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +dijkstrajs@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257" + integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2517,6 +2546,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encode-utf8@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" + integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3041,6 +3075,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3130,6 +3172,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" @@ -3883,6 +3930,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -4972,7 +5026,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.0.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -4993,6 +5047,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -5043,6 +5104,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5136,6 +5202,11 @@ pluralize@^8.0.0: resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + polished@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/polished/-/polished-4.1.3.tgz#7a3abf2972364e7d97770b827eec9a9e64002cfc" @@ -5260,6 +5331,16 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" +qrcode@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.1.tgz#0103f97317409f7bc91772ef30793a54cd59f0cb" + integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg== + dependencies: + dijkstrajs "^1.0.1" + encode-utf8 "^1.0.3" + pngjs "^5.0.0" + yargs "^15.3.1" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -5529,11 +5610,21 @@ require-at@^1.0.6: resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5843,6 +5934,13 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== +speakeasy@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a" + integrity sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw== + dependencies: + base32.js "0.0.1" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -5867,7 +5965,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6448,6 +6546,11 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -6481,6 +6584,15 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6515,11 +6627,41 @@ xtend@^4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yup@^0.32.11: version "0.32.11" resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"