From 31759111056d4ea97cf8f7844bf210a86a25e47a Mon Sep 17 00:00:00 2001 From: diced Date: Thu, 17 Nov 2022 22:13:17 -0800 Subject: [PATCH] feat: totp --- package.json | 3 + .../20221118014033_totp_secret/migration.sql | 2 + prisma/schema.prisma | 1 + src/components/pages/Manage/TotpModal.tsx | 141 ++++++++++++ src/components/pages/Manage/index.tsx | 27 ++- src/lib/config/Config.ts | 6 + src/lib/config/readConfig.ts | 3 + src/lib/config/validateConfig.ts | 18 +- src/lib/middleware/getServerSideProps.ts | 25 ++- src/lib/middleware/withZipline.ts | 3 + src/lib/recoil/user.ts | 21 +- src/lib/utils/totp.ts | 16 ++ src/pages/api/auth/login.ts | 29 ++- src/pages/api/user/mfa/totp.ts | 82 +++++++ src/pages/auth/login.tsx | 59 ++++- src/pages/dashboard/manage.tsx | 9 +- yarn.lock | 202 ++++++++++++++++++ 17 files changed, 615 insertions(+), 32 deletions(-) create mode 100644 prisma/migrations/20221118014033_totp_secret/migration.sql create mode 100644 src/components/pages/Manage/TotpModal.tsx create mode 100644 src/lib/utils/totp.ts create mode 100644 src/pages/api/user/mfa/totp.ts diff --git a/package.json b/package.json index 7dce153..ec56b9e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "ms": "canary", "multer": "^1.4.5-lts.1", "next": "^13.0.0", + "otplib": "^12.0.1", "prisma": "^4.5.0", + "qrcode": "^1.5.1", "react": "^18.2.0", "react-chartjs-2": "^4.3.1", "react-dom": "^18.2.0", @@ -68,6 +70,7 @@ "@types/minio": "^7.0.14", "@types/multer": "^1.4.7", "@types/node": "^18.11.7", + "@types/qrcode": "^1.5.0", "@types/react": "^18.0.24", "@types/sharp": "^0.31.0", "cross-env": "^7.0.3", diff --git a/prisma/migrations/20221118014033_totp_secret/migration.sql b/prisma/migrations/20221118014033_totp_secret/migration.sql new file mode 100644 index 0000000..b7841d7 --- /dev/null +++ b/prisma/migrations/20221118014033_totp_secret/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "totpSecret" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7af8590..8c5bf4e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { embedColor String @default("#2f3136") embedSiteName String? @default("{image.file} • {user.name}") ratelimit DateTime? + totpSecret String? domains String[] oauth OAuth[] images Image[] diff --git a/src/components/pages/Manage/TotpModal.tsx b/src/components/pages/Manage/TotpModal.tsx new file mode 100644 index 0000000..5096a69 --- /dev/null +++ b/src/components/pages/Manage/TotpModal.tsx @@ -0,0 +1,141 @@ +import { Button, Center, Image, Modal, NumberInput, Text, Title } from '@mantine/core'; +import { showNotification } from '@mantine/notifications'; +import { CheckIcon, CrossIcon } from 'components/icons'; +import useFetch from 'hooks/useFetch'; +import { useEffect, useState } from 'react'; + +export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) { + const [secret, setSecret] = useState(''); + const [qrCode, setQrCode] = useState(''); + const [disabled, setDisabled] = useState(false); + const [code, setCode] = useState(undefined); + const [error, setError] = useState(''); + + useEffect(() => { + (async () => { + if (opened && !deleteTotp) { + const data = await useFetch('/api/user/mfa/totp'); + if (!data.data_url) { + onClose(); + showNotification({ + title: 'Error', + message: "Can't generate code as you are already using MFA", + color: 'red', + icon: , + }); + } else { + setSecret(data.secret); + setQrCode(data.data_url); + setError(''); + } + } + })(); + }, [opened]); + + const disableTotp = async () => { + setDisabled(true); + const str = code.toString(); + if (str.length !== 6) { + return setError('Code must be 6 digits'); + } + + const resp = await useFetch('/api/user/mfa/totp', 'DELETE', { + code: str, + }); + + if (resp.error) { + setError(resp.error); + } else { + showNotification({ + title: 'Success', + message: 'Successfully disabled MFA', + color: 'green', + icon: , + }); + + setTotpEnabled(false); + + onClose(); + } + + setDisabled(false); + }; + + const verifyCode = async () => { + setDisabled(true); + const str = code.toString(); + if (str.length !== 6) { + return setError('Code must be 6 digits'); + } + + const resp = await useFetch('/api/user/mfa/totp', 'POST', { + secret, + code: str, + register: true, + }); + + if (resp.error) { + setError(resp.error); + } else { + showNotification({ + title: 'Success', + message: 'Successfully enabled MFA', + color: 'green', + icon: , + }); + + setTotpEnabled(true); + + onClose(); + } + + setDisabled(false); + }; + + return ( + Two-Factor Authentication} + size='lg' + > + {deleteTotp ? ( + Verify your code to disable Two-Factor Authentication + ) : ( + <> + + Scan the QR Code below in Authy, Google Authenticator, or any other supported + client. + +
+ QR Code +
+ QR Code not working? Try manually entering the code into your app: {secret} + + )} + + setCode(e)} + error={error} + /> + + +
+ ); +} diff --git a/src/components/pages/Manage/index.tsx b/src/components/pages/Manage/index.tsx index 70a92fe..157572d 100644 --- a/src/components/pages/Manage/index.tsx +++ b/src/components/pages/Manage/index.tsx @@ -43,6 +43,7 @@ import { useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; import Flameshot from './Flameshot'; import ShareX from './ShareX'; +import { TotpModal } from './TotpModal'; function ExportDataTooltip({ children }) { return ( @@ -56,7 +57,7 @@ function ExportDataTooltip({ children }) { ); } -export default function Manage({ oauth_registration, oauth_providers: raw_oauth_providers }) { +export default function Manage({ oauth_registration, oauth_providers: raw_oauth_providers, totp_enabled }) { const oauth_providers = JSON.parse(raw_oauth_providers); const icons = { Discord: DiscordIcon, @@ -71,11 +72,13 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_ const [user, setUser] = useRecoilState(userSelector); const modals = useModals(); + const [totpOpen, setTotpOpen] = useState(false); const [shareXOpen, setShareXOpen] = useState(false); const [flameshotOpen, setFlameshotOpen] = useState(false); const [exports, setExports] = useState([]); const [file, setFile] = useState(null); const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null); + const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret); const getDataURL = (f: File): Promise => { return new Promise((res, rej) => { @@ -372,6 +375,28 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_ + {totp_enabled && ( + + Two Factor Authentication + + {user.totpSecret + ? 'You have two factor authentication enabled.' + : 'You do not have two factor authentication enabled.'} + + + + + setTotpOpen(false)} + deleteTotp={totpEnabled} + setTotpEnabled={setTotpEnabled} + /> + + )} + {oauth_registration && ( OAuth diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index 5190ffb..f3c631b 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -123,6 +123,11 @@ export interface ConfigChunks { chunks_size: number; } +export interface ConfigMfa { + totp_enabled: boolean; + totp_issuer: string; +} + export interface Config { core: ConfigCore; uploader: ConfigUploader; @@ -134,4 +139,5 @@ export interface Config { oauth: ConfigOAuth; features: ConfigFeatures; chunks: ConfigChunks; + mfa: ConfigMfa; } diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index 33e585d..116e3f9 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -143,6 +143,9 @@ export default function readConfig() { map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'), map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'), + + map('MFA_TOTP_ISSUER', 'string', 'mfa.totp_issuer'), + map('MFA_TOTP_ENABLED', 'boolean', 'mfa.totp_enabled'), ]; const config = {}; diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 822c71a..c3e7907 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -172,7 +172,12 @@ const validator = s.object({ oauth_registration: s.boolean.default(false), user_registration: s.boolean.default(false), }) - .default({ invites: false, invites_length: 6, oauth_registration: false, user_registration: false }), + .default({ + invites: false, + invites_length: 6, + oauth_registration: false, + user_registration: false, + }), chunks: s .object({ max_size: s.number.default(humanToBytes('90MB')), @@ -182,6 +187,15 @@ const validator = s.object({ max_size: humanToBytes('90MB'), chunks_size: humanToBytes('20MB'), }), + mfa: s + .object({ + totp_issuer: s.string.default('Zipline'), + totp_enabled: s.boolean.default(false), + }) + .default({ + totp_issuer: 'Zipline', + totp_enabled: false, + }), }); export default function validate(config): Config { @@ -224,6 +238,8 @@ export default function validate(config): Config { } catch (e) { if (process.env.ZIPLINE_DOCKER_BUILD) return null; + logger.debug(`config error: ${inspect(e, { depth: Infinity })}`); + e.stack = ''; Logger.get('config') diff --git a/src/lib/middleware/getServerSideProps.ts b/src/lib/middleware/getServerSideProps.ts index 6fda0a1..2ca1f6d 100644 --- a/src/lib/middleware/getServerSideProps.ts +++ b/src/lib/middleware/getServerSideProps.ts @@ -2,13 +2,31 @@ import config from 'lib/config'; import { notNull } from 'lib/util'; import { GetServerSideProps } from 'next'; -export const getServerSideProps: GetServerSideProps = async (ctx) => { - // this entire thing will also probably change before the stable release +export type OauthProvider = { + name: string; + url: string; + link_url: string; +}; + +export type ServerSideProps = { + title: string; + external_links: string; + disable_media_preview: boolean; + invites: boolean; + user_registration: boolean; + oauth_registration: boolean; + oauth_providers: string; + chunks_size: number; + max_size: number; + totp_enabled: boolean; +}; + +export const getServerSideProps: GetServerSideProps = async () => { const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret); const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret); const googleEnabled = notNull(config.oauth?.google_client_id, config.oauth?.google_client_secret); - const oauth_providers = []; + const oauth_providers: OauthProvider[] = []; if (ghEnabled) oauth_providers.push({ @@ -41,6 +59,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { oauth_providers: JSON.stringify(oauth_providers), chunks_size: config.chunks.chunks_size, max_size: config.chunks.max_size, + totp_enabled: config.mfa.totp_enabled, }, }; }; diff --git a/src/lib/middleware/withZipline.ts b/src/lib/middleware/withZipline.ts index 8ba69c6..00546ec 100644 --- a/src/lib/middleware/withZipline.ts +++ b/src/lib/middleware/withZipline.ts @@ -7,6 +7,7 @@ import { HTTPMethod } from 'find-my-way'; import config from 'lib/config'; import prisma from 'lib/prisma'; import { sign64, unsign64 } from 'lib/utils/crypto'; +import Logger from 'lib/logger'; export interface NextApiFile { fieldname: string; @@ -180,6 +181,8 @@ export const withZipline = const signed = sign64(String(value), config.core.secret); + Logger.get('api').debug(`headers(${JSON.stringify(req.headers)}): cookie(${name}, ${value})`); + res.setHeader('Set-Cookie', serialize(name, signed, options)); }; diff --git a/src/lib/recoil/user.ts b/src/lib/recoil/user.ts index 5587515..0e57774 100644 --- a/src/lib/recoil/user.ts +++ b/src/lib/recoil/user.ts @@ -1,27 +1,12 @@ -import { OAuth } from '@prisma/client'; +import type { UserExtended } from 'middleware/withZipline'; import { atom, selector } from 'recoil'; -export interface User { - username: string; - token: string; - embedTitle: string; - embedColor: string; - embedSiteName: string; - systemTheme: string; - domains: string[]; - avatar?: string; - administrator: boolean; - superAdmin: boolean; - oauth: OAuth[]; - id: number; -} - export const userState = atom({ key: 'userState', - default: null as User, + default: null as UserExtended, }); -export const userSelector = selector({ +export const userSelector = selector({ key: 'userSelector', get: ({ get }) => get(userState), set: ({ set }, newValue) => set(userState, newValue), diff --git a/src/lib/utils/totp.ts b/src/lib/utils/totp.ts new file mode 100644 index 0000000..3032d8b --- /dev/null +++ b/src/lib/utils/totp.ts @@ -0,0 +1,16 @@ +import { authenticator } from 'otplib'; +import { toDataURL } from 'qrcode'; + +export function generate_totp_secret() { + return authenticator.generateSecret(32); +} + +export function verify_totp_code(secret: string, code: string) { + return authenticator.check(code, secret); +} + +export async function totp_qrcode(issuer: string, username: string, secret: string): Promise { + const url = authenticator.keyuri(username, issuer, secret); + + return toDataURL(url); +} diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index e1e91fc..8441cf5 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -1,14 +1,17 @@ -import prisma from 'lib/prisma'; -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; -import { checkPassword, createToken, hashPassword } from 'lib/util'; +import config from 'lib/config'; import Logger from 'lib/logger'; +import prisma from 'lib/prisma'; +import { checkPassword, createToken, hashPassword } from 'lib/util'; +import { verify_totp_code } from 'lib/utils/totp'; +import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; async function handler(req: NextApiReq, res: NextApiRes) { const logger = Logger.get('login'); - const { username, password } = req.body as { + const { username, password, code } = req.body as { username: string; password: string; + code?: string; }; const users = await prisma.user.findMany(); @@ -33,9 +36,25 @@ async function handler(req: NextApiReq, res: NextApiRes) { if (!user) return res.notFound('user not found'); - const valid = await checkPassword(password, user.password); + let valid = false; + if (user.token === password) valid = true; + else if (await checkPassword(password, user.password)) valid = true; + else valid = false; + + logger.debug(`body(${JSON.stringify(req.body)}): checkPassword(${password}, argon2-str) => ${valid}`); + if (!valid) return res.unauthorized('Wrong password'); + if (user.totpSecret && config.mfa.totp_enabled) { + if (!code) return res.unauthorized('TOTP required', { totp: true }); + + const success = verify_totp_code(user.totpSecret, code); + logger.debug( + `body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${code}) => ${success}` + ); + if (!success) return res.badRequest('Invalid code', { totp: true }); + } + res.setUserCookie(user.id); logger.info(`User ${user.username} (${user.id}) logged in`); diff --git a/src/pages/api/user/mfa/totp.ts b/src/pages/api/user/mfa/totp.ts new file mode 100644 index 0000000..44bdfe8 --- /dev/null +++ b/src/pages/api/user/mfa/totp.ts @@ -0,0 +1,82 @@ +import config from 'lib/config'; +import Logger from 'lib/logger'; +import prisma from 'lib/prisma'; +import { generate_totp_secret, totp_qrcode, verify_totp_code } from 'lib/utils/totp'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; + +const logger = Logger.get('user::mfa::totp'); + +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { + if (!config.mfa.totp_enabled) return res.forbidden('totp is disabled'); + + if (req.method === 'POST') { + const { secret, code } = req.body as { secret: string; code: string }; + + if (!secret) return res.badRequest('no secret'); + if (!code) return res.badRequest('no code'); + + if (code.length !== 6) return res.badRequest('invalid code (code.length != 6)'); + + const success = verify_totp_code(secret, code); + logger.debug(`body(${JSON.stringify(req.body)}): verify_totp_code(${secret}, ${code}) => ${success}`); + + if (!success) return res.badRequest('Invalid code'); + if (user.totpSecret) return res.badRequest('totp already registered'); + + logger.debug(`registering totp(${secret}) ${user.id}`); + await prisma.user.update({ + where: { id: user.id }, + data: { + totpSecret: secret, + }, + }); + + delete user.password; + return res.json(user); + } else if (req.method === 'DELETE') { + const { code } = req.body as { code: string }; + + if (!code) return res.badRequest('no code'); + if (code.length !== 6) return res.badRequest('invalid code (code.length != 6)'); + + const success = verify_totp_code(user.totpSecret, req.body.code); + + logger.debug( + `body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${ + req.body.code + }) => ${success}` + ); + + if (!success) return res.badRequest('Invalid code'); + + logger.debug(`unregistering totp ${user.id}`); + await prisma.user.update({ + where: { id: user.id }, + data: { + totpSecret: null, + }, + }); + + delete user.password; + return res.json(user); + } else { + if (!user.totpSecret) { + const secret = generate_totp_secret(); + const data_url = await totp_qrcode(config.mfa.totp_issuer, user.username, secret); + + return res.json({ + secret, + data_url, + }); + } + + return res.json({ + secret: user.totpSecret, + }); + } +} + +export default withZipline(handler, { + methods: ['GET', 'POST', 'DELETE'], + user: true, +}); diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 64e9ca5..a0944cf 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -1,16 +1,32 @@ -import { Button, Center, Divider, PasswordInput, TextInput, Title } from '@mantine/core'; +import { + Button, + Center, + CheckIcon, + Divider, + Modal, + NumberInput, + PasswordInput, + TextInput, + Title, +} from '@mantine/core'; import { useForm } from '@mantine/form'; import { DiscordIcon, GitHubIcon, GoogleIcon } from 'components/icons'; import useFetch from 'hooks/useFetch'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; export { getServerSideProps } from 'middleware/getServerSideProps'; export default function Login({ title, user_registration, oauth_registration, oauth_providers: unparsed }) { const router = useRouter(); + // totp modal + const [totpOpen, setTotpOpen] = useState(false); + const [code, setCode] = useState(undefined); + const [error, setError] = useState(''); + const [disabled, setDisabled] = useState(false); + const oauth_providers = JSON.parse(unparsed); const icons = { @@ -31,6 +47,7 @@ export default function Login({ title, user_registration, oauth_registration, oa }); const onSubmit = async (values) => { + setError(''); const username = values.username.trim(); const password = values.password.trim(); @@ -39,11 +56,20 @@ export default function Login({ title, user_registration, oauth_registration, oa const res = await useFetch('/api/auth/login', 'POST', { username, password, + code: code?.toString() || null, }); if (res.error) { if (res.code === 403) { form.setFieldError('password', 'Invalid password'); + } else if (res.totp) { + if (res.code === 400) { + setError('Invalid code'); + } else { + setError(''); + } + + setTotpOpen(true); } else { form.setFieldError('username', 'Invalid username'); form.setFieldError('password', 'Invalid password'); @@ -66,6 +92,35 @@ export default function Login({ title, user_registration, oauth_registration, oa {full_title} + setTotpOpen(false)} + title={Two-Factor Authentication Required} + size='lg' + > + setCode(e)} + error={error} + /> + + +
diff --git a/src/pages/dashboard/manage.tsx b/src/pages/dashboard/manage.tsx index 531f646..eba4436 100644 --- a/src/pages/dashboard/manage.tsx +++ b/src/pages/dashboard/manage.tsx @@ -2,10 +2,11 @@ import { LoadingOverlay } from '@mantine/core'; import Layout from 'components/Layout'; import Manage from 'components/pages/Manage'; import useLogin from 'hooks/useLogin'; +import type { ServerSideProps } from 'middleware/getServerSideProps'; import Head from 'next/head'; export { getServerSideProps } from 'middleware/getServerSideProps'; -export default function ManagePage(props) { +export default function ManagePage(props: ServerSideProps) { const { loading } = useLogin(); if (loading) return <LoadingOverlay visible={loading} />; @@ -17,7 +18,11 @@ export default function ManagePage(props) { <title>{title} - + ); diff --git a/yarn.lock b/yarn.lock index 05ea71d..e8a6159 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1090,6 +1090,54 @@ __metadata: languageName: node linkType: hard +"@otplib/core@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/core@npm:12.0.1" + checksum: b3c34bc20b31bc3f49cc0dc3c0eb070491c0101e8c1efa83cec48ca94158bd736aaca8187df667fc0c4a239d4ac52076bc44084bee04a50c80c3630caf77affa + languageName: node + linkType: hard + +"@otplib/plugin-crypto@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-crypto@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + checksum: 6867c74ee8aca6c2db9670362cf51e44f3648602c39318bf537421242e33f0012a172acd43bbed9a21d706e535dc4c66aff965380673391e9fd74cf685b5b13a + languageName: node + linkType: hard + +"@otplib/plugin-thirty-two@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-thirty-two@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + thirty-two: ^1.0.2 + checksum: 920099e40d3e8c2941291c84c70064c2d86d0d1ed17230d650445d5463340e406bc413ddf2e40c374ddc4ee988ef1e3facacab9b5248b1ff361fd13df52bf88f + languageName: node + linkType: hard + +"@otplib/preset-default@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-default@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 8133231384f6277f77eb8e42ef83bc32a8b01059bef147d1c358d9e9bfd292e1c239f581fe008367a48489dd68952b7ac0948e6c41412fc06079da2c91b71d16 + languageName: node + linkType: hard + +"@otplib/preset-v11@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-v11@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 367cb09397e617c21ec748d54e920ab43f1c5dfba70cbfd88edf73aecca399cf0c09fefe32518f79c7ee8a06e7058d14b200da378cc7d46af3cac4e22a153e2f + languageName: node + linkType: hard + "@phc/format@npm:^1.0.0": version: 1.0.0 resolution: "@phc/format@npm:1.0.0" @@ -1694,6 +1742,15 @@ __metadata: languageName: node linkType: hard +"@types/qrcode@npm:^1.5.0": + version: 1.5.0 + resolution: "@types/qrcode@npm:1.5.0" + dependencies: + "@types/node": "*" + checksum: b0ece3834ca5ba6171132928fd1ef764772dc619b45cb4123461ee05e377ad15553a330d234c69db0d0028c6639a99429e88d99192fbba9c5ee97c23f278c48b + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" @@ -2467,6 +2524,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^5.0.0": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001406": version: 1.0.30001423 resolution: "caniuse-lite@npm:1.0.30001423" @@ -2597,6 +2661,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^6.2.0 + checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42 + languageName: node + linkType: hard + "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -2964,6 +3039,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0": version: 0.2.0 resolution: "decode-uri-component@npm:0.2.0" @@ -3092,6 +3174,13 @@ __metadata: languageName: node linkType: hard +"dijkstrajs@npm:^1.0.1": + version: 1.0.2 + resolution: "dijkstrajs@npm:1.0.2" + checksum: 8cd822441a26f190da24d69bfab7b433d080b09e069e41e046ac84e152f182a1ed9478d531b34126e000adaa7b73114a0f85fcac117a7d25b3edf302d57c0d09 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -3235,6 +3324,13 @@ __metadata: languageName: node linkType: hard +"encode-utf8@npm:^1.0.3": + version: 1.0.3 + resolution: "encode-utf8@npm:1.0.3" + checksum: 550224bf2a104b1d355458c8a82e9b4ea07f9fc78387bc3a49c151b940ad26473de8dc9e121eefc4e84561cb0b46de1e4cd2bc766f72ee145e9ea9541482817f + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -4266,6 +4362,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.1": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3": version: 1.1.3 resolution: "get-intrinsic@npm:1.1.3" @@ -6346,6 +6449,17 @@ __metadata: languageName: node linkType: hard +"otplib@npm:^12.0.1": + version: 12.0.1 + resolution: "otplib@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/preset-default": ^12.0.1 + "@otplib/preset-v11": ^12.0.1 + checksum: 4a1b91cf1b8e920b50ad4bac2ef2a89126630c62daf68e9b32ff15106b2551db905d3b979955cf5f8f114da0a8883cec3d636901d65e793c1745bb4174e2a572 + languageName: node + linkType: hard + "p-filter@npm:2.1.0": version: 2.1.0 resolution: "p-filter@npm:2.1.0" @@ -6684,6 +6798,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^5.0.0": + version: 5.0.0 + resolution: "pngjs@npm:5.0.0" + checksum: 04e912cc45fb9601564e2284efaf0c5d20d131d9b596244f8a6789fc6cdb6b18d2975a6bbf7a001858d7e159d5c5c5dd7b11592e97629b7137f7f5cef05904c8 + languageName: node + linkType: hard + "postcss@npm:8.4.14": version: 8.4.14 resolution: "postcss@npm:8.4.14" @@ -6876,6 +6997,20 @@ __metadata: languageName: node linkType: hard +"qrcode@npm:^1.5.1": + version: 1.5.1 + resolution: "qrcode@npm:1.5.1" + dependencies: + dijkstrajs: ^1.0.1 + encode-utf8: ^1.0.3 + pngjs: ^5.0.0 + yargs: ^15.3.1 + bin: + qrcode: bin/qrcode + checksum: 842f899d95caaad2ac507408b5498be3197e1df16bc6b537b20069d2cb1330e4588b50f672ce4a9ccf01338f7c97b5732ff9b5caaa6eb2338187d3c25a973e79 + languageName: node + linkType: hard + "query-string@npm:^7.1.1": version: 7.1.1 resolution: "query-string@npm:7.1.1" @@ -7154,6 +7289,20 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -8043,6 +8192,13 @@ __metadata: languageName: node linkType: hard +"thirty-two@npm:^1.0.2": + version: 1.0.2 + resolution: "thirty-two@npm:1.0.2" + checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76 + languageName: node + linkType: hard + "through2@npm:^3.0.1": version: 3.0.2 resolution: "through2@npm:3.0.2" @@ -8468,6 +8624,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.0 + resolution: "which-module@npm:2.0.0" + checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.2": version: 1.1.8 resolution: "which-typed-array@npm:1.1.8" @@ -8578,6 +8741,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -8592,6 +8762,35 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: ^5.0.0 + decamelize: ^1.2.0 + checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9 + languageName: node + linkType: hard + +"yargs@npm:^15.3.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + 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 + checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" @@ -8635,6 +8834,7 @@ __metadata: "@types/minio": ^7.0.14 "@types/multer": ^1.4.7 "@types/node": ^18.11.7 + "@types/qrcode": ^1.5.0 "@types/react": ^18.0.24 "@types/sharp": ^0.31.0 argon2: ^0.30.1 @@ -8659,8 +8859,10 @@ __metadata: multer: ^1.4.5-lts.1 next: ^13.0.0 npm-run-all: ^4.1.5 + otplib: ^12.0.1 prettier: ^2.7.1 prisma: ^4.5.0 + qrcode: ^1.5.1 react: ^18.2.0 react-chartjs-2: ^4.3.1 react-dom: ^18.2.0