feat: totp

This commit is contained in:
diced 2022-11-17 22:13:17 -08:00
parent 00f26bdc75
commit 3175911105
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
17 changed files with 615 additions and 32 deletions

View file

@ -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",

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totpSecret" TEXT;

View file

@ -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[]

View file

@ -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: <CrossIcon />,
});
} 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: <CheckIcon />,
});
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: <CheckIcon />,
});
setTotpEnabled(true);
onClose();
}
setDisabled(false);
};
return (
<Modal
opened={opened}
onClose={onClose}
title={<Title order={3}>Two-Factor Authentication</Title>}
size='lg'
>
{deleteTotp ? (
<Text mb='md'>Verify your code to disable Two-Factor Authentication</Text>
) : (
<>
<Text mb='md'>
Scan the QR Code below in <b>Authy</b>, <b>Google Authenticator</b>, or any other supported
client.
</Text>
<Center>
<Image height={180} width={180} src={qrCode} alt='QR Code' withPlaceholder />
</Center>
<Text my='sm'>QR Code not working? Try manually entering the code into your app: {secret}</Text>
</>
)}
<NumberInput
placeholder='2FA Code'
label='Verify'
size='xl'
hideControls
maxLength={6}
minLength={6}
value={code}
onChange={(e) => setCode(e)}
error={error}
/>
<Button
disabled={disabled}
size='lg'
fullWidth
mt='md'
rightIcon={<CheckIcon />}
onClick={deleteTotp ? disableTotp : verifyCode}
>
Verify{deleteTotp ? ' and Disable' : ''}
</Button>
</Modal>
);
}

View file

@ -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<File>(null);
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret);
const getDataURL = (f: File): Promise<string> => {
return new Promise((res, rej) => {
@ -372,6 +375,28 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
</Group>
</form>
{totp_enabled && (
<Box my='md'>
<Title>Two Factor Authentication</Title>
<MutedText size='md'>
{user.totpSecret
? 'You have two factor authentication enabled.'
: 'You do not have two factor authentication enabled.'}
</MutedText>
<Button size='lg' my='sm' onClick={() => setTotpOpen(true)}>
{totpEnabled ? 'Disable' : 'Enable'} Two Factor Authentication
</Button>
<TotpModal
opened={totpOpen}
onClose={() => setTotpOpen(false)}
deleteTotp={totpEnabled}
setTotpEnabled={setTotpEnabled}
/>
</Box>
)}
{oauth_registration && (
<Box my='md'>
<Title>OAuth</Title>

View file

@ -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;
}

View file

@ -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 = {};

View file

@ -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')

View file

@ -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<ServerSideProps> = 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,
},
};
};

View file

@ -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));
};

View file

@ -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<User>({
export const userSelector = selector<UserExtended>({
key: 'userSelector',
get: ({ get }) => get(userState),
set: ({ set }, newValue) => set(userState, newValue),

16
src/lib/utils/totp.ts Normal file
View file

@ -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<string> {
const url = authenticator.keyuri(username, issuer, secret);
return toDataURL(url);
}

View file

@ -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`);

View file

@ -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,
});

View file

@ -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
<Head>
<title>{full_title}</title>
</Head>
<Modal
opened={totpOpen}
onClose={() => setTotpOpen(false)}
title={<Title order={3}>Two-Factor Authentication Required</Title>}
size='lg'
>
<NumberInput
placeholder='2FA Code'
label='Verify'
size='xl'
hideControls
maxLength={6}
minLength={6}
value={code}
onChange={(e) => setCode(e)}
error={error}
/>
<Button
disabled={disabled}
size='lg'
fullWidth
mt='md'
rightIcon={<CheckIcon />}
onClick={() => onSubmit(form.values)}
>
Verify &amp; Login
</Button>
</Modal>
<Center sx={{ height: '100vh' }}>
<div>
<Title size={70} align='center'>

View file

@ -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}</title>
</Head>
<Layout props={props}>
<Manage oauth_providers={props.oauth_providers} oauth_registration={props.oauth_registration} />
<Manage
oauth_providers={props.oauth_providers}
oauth_registration={props.oauth_registration}
totp_enabled={props.totp_enabled}
/>
</Layout>
</>
);

202
yarn.lock
View file

@ -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