diff --git a/.env b/.env index 419107808db520055938c6abe10f00e8d77e20a7..d4d6490a51e02a07bf4d5aaed3a6d31a2cbcfae3 100644 --- a/.env +++ b/.env @@ -2,4 +2,10 @@ DAY1_AUTH_HOST_API=http://localhost:8000 # ASSETS -DAY1_AUTH_ASSETS_API= \ No newline at end of file +DAY1_AUTH_ASSETS_API= + +# HOSTNAME +DAY1_AUTH_HOSTNAME=locahost + +# PUBLIC_KEY_PATH +DAY1_AUTH_PUBLIC_KEY_PATH=public.pem diff --git a/.gitignore b/.gitignore index aeb2efc94999298cf7c6c506c1316a74f79267a1..8e1c2b8df0b136e704c193c0128a7a83bbf2e475 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ # misc .DS_Store -*.pem # debug npm-debug.log* diff --git a/package.json b/package.json index fdb1da828d1e071b617a18f6bae56db2608bb6ae..e0f0698dc41227b60cb4a551183c13d7b1600c0e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "@mui/lab": "^5.0.0-alpha.163", "@mui/material": "^5.14.20", "@hookform/resolvers": "^3.3.4", + "axios": "^1.6.7", "framer-motion": "^11.0.3", + "jose": "^5.2.2", + "js-cookie": "^3.0.5", "mapbox-gl": "^3.1.2", "next": "14.1.0", "notistack": "^3.0.1", @@ -33,6 +36,7 @@ }, "devDependencies": { "typescript": "^5", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/nprogress": "^0.2.3", "@types/react": "^18", diff --git a/public.pem b/public.pem new file mode 100644 index 0000000000000000000000000000000000000000..f0082d0c84a147953f4b8ecd8df35be0d0fad703 --- /dev/null +++ b/public.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw1w5JbiWaLNey50rnXJl +GDik50s7ndz6bTPhCYb5YEmbpllp4SF2pwotxOI08vQbdvUWQXpS0yxa7iTRmMgD +UAo4Lw1+CmEAXT/pNtHhN34wlqPfJghsN1DBqAKLO3nzze/nBg/j/uDYTvRbr/Ck +XywR/YaRFqr9tia040g1ND5xQa73Aw3r9nGoHxc8tcP1ppajtM5gHtfHmhRiRWes +ZdjStPbWfUl0924m4GzWrafnqVm54uWLXyFiMsK1X1bYdLVmpO/Nxk/FKoSWYsCf +OK323WPntlH1k4KChQxYti+6ck8wb71stZRcOFRS64teQeAl6sThyRxahC2M9Rad +iOcpfv4Z0oVAEU8xn6C+2mpFSbWTOxWF3Wh87okE92mzTrq/4L1YPN2hENrUYQ5y +Zb+A+5p40Jyvz4LdKezd66xeax9PBGlviy5Ee40Kn39PhQyUmcSIiK+xZdzFVuGp +2JEBH/8RGoVzODmRt198tlb2+12+BzMK6bjMHKo5MnmBsPqHXD5nCFkho8WZ2hca +9L8nXmzwPB8JMjUu2mtzE3OxK/7OAzFZRfyulWvLZ8By9lLs2wIFmW6ztNJMV7Os +J5DU+kCI7+y843Nh3mBiDFm31SZZX+QkWHYMv4OuXLMkiYfygELcoRB24NxHJVFF +6Kx2LuvcqxXetpfWP3GhXFMCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..25990aea65b5c75afbf0bd4325ce259a50c08e5d --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,91 @@ +import * as Yup from "yup"; + +import ssrAxios, {endpoints, ErrorResponse, isErrorResponse} from "@/utils/server/axios"; + + +export const dynamic = 'force-dynamic'; + +interface FormDataObject { + [key: string]: string | File | Blob | FormDataEntryValue; +} + +export async function POST(request: Request) { + const formData = await request.formData(); + const data: FormDataObject = {}; + formData.forEach((value, key) => { + data[key] = value; + }) + + const loginSchema = Yup.object().shape({ + email: Yup.string() + .required('Email is required') + .max(50, 'Email must be at most 50 characters') + .email('Email must be a valid email address'), + password: Yup.string() + .required('Password is required') + .min(8, 'Password must be at least 8 characters') + .max(20, 'Password must be at most 20 characters') + .matches( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/, + 'Password must contain at least one letter, one number and one special character' + ), + }); + + // æ£€æŸ¥æ˜¯å¦æœ‰å¤šä½™çš„å—æ®µ + const extraFields = Object.keys(data).filter(key => !(key in (loginSchema.fields as any))); + if (extraFields.length > 0) { + return Response.json({ + code: -1, + message: `Error: Unexpected fields found: ${extraFields.join(', ')}`, + jwt: "", + }); + } + + // éªŒè¯æ•°æ® + loginSchema.validate(data).catch(error => { + return Response.json({ + code: -1, + message: error.message, + jwt: "", + }); + }); + + const URL = endpoints.login; + try { + const postData = new FormData(); + postData.append('email', data['email']); + postData.append('password', data['password']); + + const response = await ssrAxios.post(URL, postData, { + headers: { + "Content-Type": "multipart/form-data", + } + }); + + const message = response.data.code !== 0? "ç”¨æˆ·åæˆ–密ç 错误":""; + + return Response.json({ + code: response.data.code, + message: message, + jwt: response.data.jwt, + }); + } catch(error) { + console.error("api/auth/login post form-data failed"); + if (isErrorResponse(error)) { + const errorResponse = error as ErrorResponse; + console.error("Reason: " + errorResponse.name + ": " + errorResponse.message); + console.error("Request headers: " + JSON.stringify(errorResponse.config.headers)); + console.error("Request baseURL: " + errorResponse.config.baseURL); + console.error("Request method: " + errorResponse.config.method); + console.error("Request url: " + errorResponse.config.url); + } else { + console.error("Error: ", error); + } + + return Response.json({ + code: -1, + message: "Error: Internal server failed", + jwt: "", + }); + } +} \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index ecd5f937198188fb8455f33d5ca90b434e5c12f3..12ea10e2c57de8305825495358e0a2791f704c62 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,4 +1,5 @@ -import { LoginView } from '@/sections/auth'; +import { LoginView } from '@/views/login'; +import {Suspense} from "react"; // ---------------------------------------------------------------------- @@ -7,5 +8,9 @@ export const metadata = { }; export default function LoginPage() { - return <LoginView />; + return ( + <Suspense> + <LoginView /> + </Suspense> + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 8bdd57f3316c5722f37cd80128d93930ac5be12a..fffbf271f9a3d51a59e612146655356ef37e07c9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,6 @@ +import {redirect} from "next/navigation"; +import JWTValidate from "@/utils/server/jwt-validate"; // ---------------------------------------------------------------------- @@ -6,6 +8,10 @@ export const metadata = { title: 'Hinko.dev 默认页é¢', }; -export default function AppPage() { - return <div>测试页é¢</div>; +export default async function AppPage() { + const valid = await JWTValidate(); + if (valid) + return <div>æˆåŠŸé¡µé¢</div>; + else + return redirect("/login"); } \ No newline at end of file diff --git a/src/config-global.ts b/src/config-global.ts index 3c90f6b4111da78b12f5bb0557ea9182b717bd5e..2440dac7f885a2e0e3d47e7a2ef050f85766a7e5 100644 --- a/src/config-global.ts +++ b/src/config-global.ts @@ -1,5 +1,9 @@ // API // ---------------------------------------------------------------------- -export const HOST_API = process.env.DAY1_AUTH_HOST_API; +export const HOST_API = process.env.DAY1_AUTH_HOST_API ?? "http://localhost:8000"; export const ASSETS_API = process.env.DAY1_AUTH_ASSETS_API; + +export const HOSTNAME = process.env.DAY1_AUTH_HOSTNAME ?? "localhost"; + +export const PUBLIC_KEY_PATH = process.env.DAY1_AUTH_PUBLIC_KEY_PATH ?? "public.pem"; diff --git a/src/sections/auth/login-view.tsx b/src/sections/auth/login-view.tsx deleted file mode 100644 index 39198260e8a5f0ad5fd89237c935cfb168cf38c6..0000000000000000000000000000000000000000 --- a/src/sections/auth/login-view.tsx +++ /dev/null @@ -1,102 +0,0 @@ -'use client'; - -import * as Yup from 'yup'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; - -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; -import LoadingButton from '@mui/lab/LoadingButton'; -import InputAdornment from '@mui/material/InputAdornment'; - -import RouterLink from '@/components/router-link'; - -import { useBoolean } from '@/hooks/use-boolean'; - -import Iconify from '@/components/iconify'; -import FormProvider, { RHFTextField } from '@/components/hook-form'; - -// ---------------------------------------------------------------------- - -export default function ModernLoginView() { - const password = useBoolean(); - - const LoginSchema = Yup.object().shape({ - email: Yup.string().required('Email is required').email('Email must be a valid email address'), - password: Yup.string().required('Password is required'), - }); - - const defaultValues = { - email: '', - password: '', - }; - - const methods = useForm({ - resolver: yupResolver(LoginSchema), - defaultValues, - }); - - const { - handleSubmit, - formState: { isSubmitting }, - } = methods; - - const onSubmit = handleSubmit(async (data) => { - try { - await new Promise((resolve) => setTimeout(resolve, 500)); - console.info('DATA', data); - } catch (error) { - console.error(error); - } - }); - - const renderHead = ( - <Stack spacing={2} sx={{ mb: 5 }}> - <Typography variant="h4">Sign in to Hinko.dev</Typography> - </Stack> - ); - - const renderForm = ( - <Stack spacing={2.5}> - <RHFTextField name="email" label="Email address" /> - - <RHFTextField - name="password" - label="Password" - type={password.value ? 'text' : 'password'} - InputProps={{ - endAdornment: ( - <InputAdornment position="end"> - <IconButton onClick={password.onToggle} edge="end"> - <Iconify icon={password.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} /> - </IconButton> - </InputAdornment> - ), - }} - /> - - <LoadingButton - fullWidth - color="inherit" - size="large" - type="submit" - variant="contained" - loading={isSubmitting} - endIcon={<Iconify icon="eva:arrow-ios-forward-fill" />} - sx={{ justifyContent: 'space-between', pl: 2, pr: 1.5 }} - > - Login - </LoadingButton> - </Stack> - ); - - return ( - <FormProvider methods={methods} onSubmit={onSubmit}> - {renderHead} - - {renderForm} - </FormProvider> - ); -} diff --git a/src/utils/client/axios.ts b/src/utils/client/axios.ts new file mode 100644 index 0000000000000000000000000000000000000000..0808a17ba0e37b72b1e26101a9328728f89bfdac --- /dev/null +++ b/src/utils/client/axios.ts @@ -0,0 +1,35 @@ +import axios, {AxiosRequestConfig} from 'axios'; + +// ---------------------------------------------------------------------- + +const csrAxios = axios.create({ + timeout: 5000, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + } +}); + +// 错误拦截 +csrAxios.interceptors.response.use( + (res) => res, + (error) => Promise.reject(error) +); + +export default csrAxios; + +// ---------------------------------------------------------------------- + +export const fetcher = async (args: string | [string, AxiosRequestConfig]) => { + const [url, config] = Array.isArray(args) ? args : [args]; + + const res = await csrAxios.get(url, {...config}); + + return res.data; +}; + +// ---------------------------------------------------------------------- + +export const endpoints = { + login: '/api/auth/login', +}; diff --git a/src/utils/server/axios.ts b/src/utils/server/axios.ts new file mode 100644 index 0000000000000000000000000000000000000000..11f998410af0c9d51306ee22ddeacf360c60da2c --- /dev/null +++ b/src/utils/server/axios.ts @@ -0,0 +1,83 @@ +import axios, {AxiosRequestConfig} from 'axios'; + +import {HOST_API} from '@/config-global'; + +// ---------------------------------------------------------------------- + +export interface ErrorResponse { + name: string; + message: string; + config: { + headers: any; + baseURL: string; + method: string; + url: string; + } +} + +function isErrorConfig(obj: any): boolean { + return obj + && typeof obj === 'object' + && 'headers' in obj + && 'baseURL' in obj + && 'method' in obj + && 'url' in obj; +} + +export function isErrorResponse(obj: any): obj is ErrorResponse { + return obj + && typeof obj === 'object' + && 'name' in obj + && 'message' in obj + && 'config' in obj + && isErrorConfig(obj.config); +} + +const ssrAxios = axios.create({ + baseURL: HOST_API, + timeout: 5000, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + } +}); + +// 错误拦截 +ssrAxios.interceptors.response.use( + (res) => res, + (error) => { + const errorResponse: ErrorResponse = { + name: error.name, + message: error.message, + config: { + headers: error.config.headers, + baseURL: error.config.baseURL, + method: error.config.method, + url: error.config.url, + } + }; + return Promise.reject(errorResponse); + } +); + +export default ssrAxios; + +// ---------------------------------------------------------------------- + +export const fetcher = async (args: string | [string, AxiosRequestConfig]) => { + const [url, config] = Array.isArray(args) ? args : [args]; + + try { + const res = await ssrAxios.get(url, {...config}); + const data = res.data; + return {data, undefined}; + } catch (error) { + return {undefined, error}; + } +}; + +// ---------------------------------------------------------------------- + +export const endpoints = { + login: '/api/auth/login', +}; diff --git a/src/utils/server/jwt-validate.ts b/src/utils/server/jwt-validate.ts new file mode 100644 index 0000000000000000000000000000000000000000..09ca786ccc400e24dda1c38d4682d6c92915a666 --- /dev/null +++ b/src/utils/server/jwt-validate.ts @@ -0,0 +1,45 @@ +import {cookies} from "next/headers"; +import * as jose from "jose"; +import fs from 'fs'; +import {PUBLIC_KEY_PATH} from "@/config-global"; + +// ---------------------------------------------------------------------- + +export default async function JWTValidate() { + + const authorization = cookies().get('auth.jwt'); + + if (authorization === undefined) + return false; + const jwt = authorization.value; + + const publicKeyString: string = fs.readFileSync(PUBLIC_KEY_PATH, 'utf8'); + + const alg = 'RS512'; + + const publicKey = await jose.importSPKI(publicKeyString, alg); + + try { + const {payload, protectedHeader} = await jose.jwtVerify(jwt, publicKey, { + subject: 'sso.hinko.dev', + }); + + if (typeof payload.name !== "string") + return false; + + if (typeof payload.admin !== "boolean") + return false; + + const currentTimestamp = new Date().getTime() / 1000; + if (typeof payload.nbf !== "number") + return false; + if (typeof payload.exp !== "number") + return false; + + } catch (error) { + console.error("error: " + error); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/sections/auth/index.ts b/src/views/login/index.ts similarity index 100% rename from src/sections/auth/index.ts rename to src/views/login/index.ts diff --git a/src/views/login/login-view.tsx b/src/views/login/login-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..057e5dae279d3f6a05febbc60abb80a1d4a46441 --- /dev/null +++ b/src/views/login/login-view.tsx @@ -0,0 +1,142 @@ +'use client'; + +import Cookies from "js-cookie"; +import {useRouter, useSearchParams} from "next/navigation"; +import {useState} from "react"; +import * as Yup from 'yup'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; + +import Alert from "@mui/material/Alert"; +import Stack from '@mui/material/Stack'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import InputAdornment from '@mui/material/InputAdornment'; + +import { useBoolean } from '@/hooks/use-boolean'; + +import Iconify from '@/components/iconify'; +import FormProvider, { RHFTextField } from '@/components/hook-form'; +import csrAxios, {endpoints} from "@/utils/client/axios"; +import {HOSTNAME} from "@/config-global"; + + +// ---------------------------------------------------------------------- + +export default function LoginView() { + const router = useRouter(); + + const [errorMsg, setErrorMsg] = useState(''); + + const searchParams = useSearchParams(); + + const redirectURL = searchParams.get('redirect_url'); + + const password = useBoolean(); + + const LoginSchema = Yup.object().shape({ + email: Yup.string().required('Email is required').email('Email must be a valid email address'), + password: Yup.string().required('Password is required'), + }); + + const defaultValues = { + email: '', + password: '', + }; + + const methods = useForm({ + resolver: yupResolver(LoginSchema), + defaultValues, + }); + + const { + reset, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const onSubmit = handleSubmit(async (data) => { + try { + const URL = endpoints.login; + + const response = await csrAxios.post(URL, data, { + headers: { + "Content-Type": "multipart/form-data", + } + }); + if (response.data.code == 0) { + const in30m = 1/48; + Cookies.set("auth.jwt", response.data.jwt, { + domain: HOSTNAME, + expires: in30m, + path: '/' + }); + router.push(redirectURL || '/'); + } else { + reset(); + setErrorMsg(response.data.message); + } + } catch (error) { + console.error(error); + reset(); + setErrorMsg(typeof error === 'string' ? error : "未知错误"); + } + }); + + const renderHead = ( + <Stack spacing={2} sx={{ mb: 5 }}> + <Typography variant="h4">Sign in to Hinko.dev</Typography> + </Stack> + ); + + const renderForm = ( + <Stack spacing={2.5}> + <RHFTextField name="email" label="Email address" /> + + <RHFTextField + name="password" + label="Password" + type={password.value ? 'text' : 'password'} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton onClick={password.onToggle} edge="end"> + <Iconify icon={password.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} /> + </IconButton> + </InputAdornment> + ), + }} + /> + + <LoadingButton + fullWidth + color="inherit" + size="large" + type="submit" + variant="contained" + loading={isSubmitting} + endIcon={<Iconify icon="eva:arrow-ios-forward-fill" />} + sx={{ justifyContent: 'space-between', pl: 2, pr: 1.5 }} + > + Login + </LoadingButton> + </Stack> + ); + + return ( + <> + {renderHead} + + {!!errorMsg && ( + <Alert severity="error" sx={{ mb: 3}}> + {errorMsg} + </Alert> + )} + + <FormProvider methods={methods} onSubmit={onSubmit}> + {renderForm} + </FormProvider> + </> + ); +} diff --git a/yarn.lock b/yarn.lock index 5770ed59e59f92ba8d383d9ab5654f5d59946624..e86d6599244653506d0a7569c48847977ee4effb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -525,6 +525,11 @@ dependencies: tslib "^2.4.0" +"@types/js-cookie@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95" + integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -822,6 +827,11 @@ asynciterator.prototype@^1.0.0: dependencies: has-symbols "^1.0.3" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -834,6 +844,15 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -976,6 +995,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1075,6 +1101,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -1559,6 +1590,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -1574,6 +1610,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + framer-motion@^11.0.3: version "11.0.6" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.0.6.tgz#97c3076d9d718486eb533b58b88db800139d5fc3" @@ -2049,6 +2094,16 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jose@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0" + integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg== + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2228,6 +2283,18 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimatch@9.0.3, minimatch@^9.0.1: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" @@ -2541,6 +2608,11 @@ protocol-buffers-schema@^3.3.1: resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"