feat: implement sign in with SSO (#1119)

* feat: implement sign in with SSO

* chore: update

* chore: update

* chore: update
pull/1120/head
boojack 2 years ago committed by GitHub
parent 708049bb89
commit d0b8b076cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ type SignIn struct {
Password string `json:"password"`
}
type SSOSignIn struct {
IdentityProviderID int `json:"identityProviderId"`
Code string `json:"code"`
RedirectURI string `json:"redirectUri"`
}
type SignUp struct {
Username string `json:"username"`
Password string `json:"password"`

@ -1,6 +1,8 @@
package common
import (
"crypto/rand"
"math/big"
"net/mail"
"strings"
@ -35,3 +37,24 @@ func Min(x, y int) int {
}
return y
}
var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// RandomString returns a random string with length n.
func RandomString(n int) (string, error) {
var sb strings.Builder
sb.Grow(n)
for i := 0; i < n; i++ {
// The reason for using crypto/rand instead of math/rand is that
// the former relies on hardware to generate random numbers and
// thus has a stronger source of random numbers.
randNum, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
if _, err := sb.WriteRune(letters[randNum.Uint64()]); err != nil {
return "", err
}
}
return sb.String(), nil
}

@ -81,7 +81,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
}
}
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id", "/api/memo") && c.Request().Method == http.MethodGet {
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/idp", "/api/user/:id", "/api/memo") && c.Request().Method == http.MethodGet {
return next(c)
}

@ -4,11 +4,15 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/store"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@ -50,6 +54,90 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
return c.JSON(http.StatusOK, composeResponse(user))
})
g.POST("/auth/signin/sso", func(c echo.Context) error {
ctx := c.Request().Context()
signin := &api.SSOSignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
identityProviderMessage, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProviderMessage{
ID: &signin.IdentityProviderID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
}
var userInfo *idp.IdentityProviderUserInfo
if identityProviderMessage.Type == store.IdentityProviderOAuth2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProviderMessage.Config.OAuth2Config)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
}
token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
}
}
identifierFilter := identityProviderMessage.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
}
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
Username: &userInfo.Identifier,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", userInfo.Identifier)).SetInternal(err)
}
if user == nil {
userCreate := &api.UserCreate{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: api.NormalUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
Password: userInfo.Email,
OpenID: common.GenUUID(),
}
password, err := common.RandomString(20)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err = s.Store.CreateUser(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
}
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
}
if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
}
if err := s.createUserAuthSignInActivity(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(user))
})
g.POST("/auth/signup", func(c echo.Context) error {
ctx := c.Request().Context()
signup := &api.SignUp{}

@ -91,30 +91,33 @@ func (s *Server) registerIdentityProviderRoutes(g *echo.Group) {
g.GET("/idp", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
// We should only show identity provider list to host user.
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderMessageList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProviderMessage{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider list").SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
isHostUser := false
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user != nil && user.Role == api.Host {
isHostUser = true
}
}
identityProviderList := []*api.IdentityProvider{}
for _, identityProviderMessage := range identityProviderMessageList {
identityProviderList = append(identityProviderList, convertIdentityProviderFromStore(identityProviderMessage))
identityProvider := convertIdentityProviderFromStore(identityProviderMessage)
// data desensitize
if !isHostUser {
identityProvider.Config.OAuth2Config.ClientSecret = ""
}
identityProviderList = append(identityProviderList, identityProvider)
}
return c.JSON(http.StatusOK, composeResponse(identityProviderList))
})

@ -98,11 +98,9 @@ func (s *Store) CreateIdentityProvider(ctx context.Context, create *IdentityProv
); err != nil {
return nil, FormatError(err)
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
identityProviderMessage := create
s.idpCache.Store(identityProviderMessage.ID, identityProviderMessage)
return identityProviderMessage, nil
@ -208,7 +206,9 @@ func (s *Store) UpdateIdentityProvider(ctx context.Context, update *UpdateIdenti
} else {
return nil, fmt.Errorf("unsupported idp type %s", string(identityProviderMessage.Type))
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
s.idpCache.Store(identityProviderMessage.ID, identityProviderMessage)
return &identityProviderMessage, nil
}
@ -234,6 +234,9 @@ func (s *Store) DeleteIdentityProvider(ctx context.Context, delete *DeleteIdenti
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("idp not found")}
}
if err := tx.Commit(); err != nil {
return err
}
s.idpCache.Delete(delete.ID)
return nil
}

@ -8,7 +8,7 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/joy": "^5.0.0-alpha.63",
"@mui/joy": "^5.0.0-alpha.67",
"@reduxjs/toolkit": "^1.8.1",
"axios": "^0.27.2",
"copy-to-clipboard": "^3.3.2",

@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { Button, Divider, Input, List, Radio, RadioGroup, Typography } from "@mui/joy";
import { Alert, Button, Divider, Input, Radio, RadioGroup, Typography } from "@mui/joy";
import * as api from "../helpers/api";
import { UNKNOWN_ID } from "../helpers/consts";
import { absolutifyLink } from "../helpers/utils";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
import toastHelper from "./Toast";
@ -10,6 +12,72 @@ interface Props extends DialogProps {
confirmCallback?: () => void;
}
const templateList: IdentityProvider[] = [
{
id: UNKNOWN_ID,
name: "GitHub",
type: "OAUTH2",
identifierFilter: "",
config: {
oauth2Config: {
clientId: "",
clientSecret: "",
authUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: ["user"],
fieldMapping: {
identifier: "login",
displayName: "name",
email: "email",
},
},
},
},
{
id: UNKNOWN_ID,
name: "Google",
type: "OAUTH2",
identifierFilter: "",
config: {
oauth2Config: {
clientId: "",
clientSecret: "",
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
scopes: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
fieldMapping: {
identifier: "email",
displayName: "name",
email: "email",
},
},
},
},
{
id: UNKNOWN_ID,
name: "Custom",
type: "OAUTH2",
identifierFilter: "",
config: {
oauth2Config: {
clientId: "",
clientSecret: "",
authUrl: "",
tokenUrl: "",
userInfoUrl: "",
scopes: [],
fieldMapping: {
identifier: "",
displayName: "",
email: "",
},
},
},
},
];
const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
const { confirmCallback, destroy, identityProvider } = props;
const [basicInfo, setBasicInfo] = useState({
@ -31,6 +99,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
},
});
const [oauth2Scopes, setOAuth2Scopes] = useState<string>("");
const [seletedTemplate, setSelectedTemplate] = useState<string>("GitHub");
const isCreating = identityProvider === undefined;
useEffect(() => {
@ -47,6 +116,25 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
}
}, []);
useEffect(() => {
if (!isCreating) {
return;
}
const template = templateList.find((t) => t.name === seletedTemplate);
if (template) {
setBasicInfo({
name: template.name,
identifierFilter: template.identifierFilter,
});
setType(template.type);
if (template.type === "OAUTH2") {
setOAuth2Config(template.config.oauth2Config);
setOAuth2Scopes(template.config.oauth2Config.scopes.join(" "));
}
}
}, [seletedTemplate]);
const handleCloseBtnClick = () => {
destroy();
};
@ -84,6 +172,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
},
},
});
toastHelper.info(`SSO ${basicInfo.name} created`);
} else {
await api.patchIdentityProvider({
id: identityProvider?.id,
@ -96,6 +185,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
},
},
});
toastHelper.info(`SSO ${basicInfo.name} updated`);
}
} catch (error: any) {
console.error(error);
@ -124,14 +214,34 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
</button>
</div>
<div className="dialog-content-container">
<Typography className="!mb-1" level="body2">
Type
</Typography>
<RadioGroup className="mb-2" value={type}>
<List>
<Radio value="OAUTH2" label="OAuth 2.0" />
</List>
</RadioGroup>
{isCreating && (
<>
<Typography className="!mb-1" level="body2">
Type
</Typography>
<RadioGroup className="mb-2" value={type}>
<div className="mt-2 w-full flex flex-row space-x-4">
<Radio value="OAUTH2" label="OAuth 2.0" />
</div>
</RadioGroup>
<Typography className="mb-2" level="body2">
Template
</Typography>
<RadioGroup className="mb-2" value={seletedTemplate}>
<div className="mt-2 w-full flex flex-row space-x-4">
{templateList.map((template) => (
<Radio
key={template.name}
value={template.name}
label={template.name}
onChange={(e) => setSelectedTemplate(e.target.value)}
/>
))}
</div>
</RadioGroup>
<Divider className="!my-2" />
</>
)}
<Typography className="!mb-1" level="body2">
Name<span className="text-red-600">*</span>
</Typography>
@ -165,6 +275,11 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
<Divider className="!my-2" />
{type === "OAUTH2" && (
<>
{isCreating && (
<Alert variant="outlined" color="neutral" className="w-full mb-2">
Redirect URL: {absolutifyLink("/auth/callback")}
</Alert>
)}
<Typography className="!mb-1" level="body2">
Client ID<span className="text-red-600">*</span>
</Typography>

@ -25,6 +25,14 @@ export function signin(username: string, password: string) {
});
}
export function signinWithSSO(identityProviderId: IdentityProviderId, code: string, redirectUri: string) {
return axios.post<ResponseObject<User>>("/api/auth/signin/sso", {
identityProviderId,
code,
redirectUri,
});
}
export function signup(username: string, password: string) {
return axios.post<ResponseObject<User>>("/api/auth/signup", {
username,

@ -1,5 +1,5 @@
.page-wrapper.auth {
@apply flex flex-row justify-center items-center w-full h-full bg-zinc-100 dark:bg-zinc-800;
@apply flex flex-row justify-center items-center w-full h-full dark:bg-zinc-800;
> .page-container {
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center ml-calc;
@ -37,7 +37,7 @@
@apply absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 bg-transparent transition-all select-none pointer-events-none;
&.not-null {
@apply text-sm top-0 z-10 leading-4 bg-zinc-100 dark:bg-zinc-800 rounded;
@apply text-sm top-0 z-10 leading-4 bg-white dark:bg-zinc-800 rounded;
}
}
@ -45,7 +45,7 @@
@apply py-2;
> input {
@apply w-full py-3 px-3 text-base rounded-lg bg-zinc-100 dark:bg-zinc-800;
@apply w-full py-3 px-3 text-base rounded-lg dark:bg-zinc-800;
}
}
}

@ -1,8 +1,9 @@
import { Button, Divider } from "@mui/joy";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useGlobalStore, useUserStore } from "../store/module";
import * as api from "../helpers/api";
import { absolutifyLink } from "../helpers/utils";
import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon";
@ -20,7 +21,6 @@ const validateConfig: ValidatorConfig = {
const Auth = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const actionBtnLoadingState = useLoading(false);
@ -28,9 +28,17 @@ const Auth = () => {
const mode = systemStatus.profile.mode;
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
useEffect(() => {
userStore.doSignOut().catch();
const fetchIdentityProviderList = async () => {
const {
data: { data: identityProviderList },
} = await api.getIdentityProviderList();
setIdentityProviderList(identityProviderList);
};
fetchIdentityProviderList();
}, []);
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -73,7 +81,7 @@ const Auth = () => {
await api.signin(username, password);
const user = await userStore.doSignIn();
if (user) {
navigate("/");
window.location.href = "/";
} else {
toastHelper.error(t("message.login-failed"));
}
@ -106,7 +114,7 @@ const Auth = () => {
await api.signup(username, password);
const user = await userStore.doSignIn();
if (user) {
navigate("/");
window.location.href = "/";
} else {
toastHelper.error(t("common.singup-failed"));
}
@ -123,6 +131,20 @@ const Auth = () => {
}
};
const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => {
const stateQueryParameter = `auth.signin.${identityProvider.name}-${identityProvider.id}`;
if (identityProvider.type === "OAUTH2") {
const redirectUri = absolutifyLink("/auth/callback");
const oauth2Config = identityProvider.config.oauth2Config;
const authUrl = `${oauth2Config.authUrl}?client_id=${
oauth2Config.clientId
}&redirect_uri=${redirectUri}&state=${stateQueryParameter}&response_type=code&scope=${encodeURIComponent(
oauth2Config.scopes.join(" ")
)}`;
window.location.href = authUrl;
}
};
return (
<div className="page-wrapper auth">
<div className="page-container">
@ -175,6 +197,25 @@ const Auth = () => {
</>
)}
</div>
{identityProviderList.length > 0 && (
<>
<Divider className="!my-4">or</Divider>
<div className="w-full flex flex-col space-y-2">
{identityProviderList.map((identityProvider) => (
<Button
key={identityProvider.id}
variant="outlined"
color="neutral"
className="w-full"
size="md"
onClick={() => handleSignInWithIdentityProvider(identityProvider)}
>
Sign in with {identityProvider.name}
</Button>
))}
</div>
</>
)}
{!systemStatus?.host && <p className="tip-text">{t("auth.host-tip")}</p>}
</div>
<div className="flex flex-row items-center justify-center w-full gap-2">

@ -0,0 +1,74 @@
import { last } from "lodash-es";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import * as api from "../helpers/api";
import toastHelper from "../components/Toast";
import { absolutifyLink } from "../helpers/utils";
import { useUserStore } from "../store/module";
import Icon from "../components/Icon";
interface State {
loading: boolean;
errorMessage: string;
}
const AuthCallback = () => {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const userStore = useUserStore();
const [state, setState] = useState<State>({
loading: true,
errorMessage: "",
});
useEffect(() => {
const code = searchParams.get("code");
const state = searchParams.get("state");
if (code && state) {
const redirectUri = absolutifyLink("/auth/callback");
const identityProviderId = Number(last(state.split("-")));
if (identityProviderId) {
api
.signinWithSSO(identityProviderId, code, redirectUri)
.then(async () => {
setState({
loading: false,
errorMessage: "",
});
const user = await userStore.doSignIn();
if (user) {
window.location.href = "/";
} else {
toastHelper.error(t("message.login-failed"));
}
})
.catch((error: any) => {
console.error(error);
setState({
loading: false,
errorMessage: JSON.stringify(error.response.data, null, 2),
});
});
}
} else {
setState({
loading: false,
errorMessage: "Failed to authorize. Invalid state passed to the auth callback.",
});
}
}, [searchParams]);
return (
<div className="p-4 w-full h-full flex justify-center items-center">
{state.loading ? (
<Icon.Loader className="animate-spin dark:text-gray-200" />
) : (
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
)}
</div>
);
};
export default AuthCallback;

@ -5,6 +5,7 @@ import store from "../store";
import { initialGlobalState, initialUserState } from "../store/module";
const Auth = lazy(() => import("../pages/Auth"));
const AuthCallback = lazy(() => import("../pages/AuthCallback"));
const Explore = lazy(() => import("../pages/Explore"));
const Home = lazy(() => import("../pages/Home"));
const MemoDetail = lazy(() => import("../pages/MemoDetail"));
@ -36,6 +37,10 @@ const router = createBrowserRouter([
return null;
},
},
{
path: "/auth/callback",
element: <AuthCallback />,
},
{
path: "/",
element: <Home />,

@ -47,7 +47,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.9.2":
version "7.20.13"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
@ -355,70 +355,70 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@mui/base@5.0.0-alpha.115":
version "5.0.0-alpha.115"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.115.tgz#582b147fda56fe52d561fe9f64406e036d882338"
integrity sha512-OGQ84whT/yNYd6xKCGGS6MxqEfjVjk5esXM7HP6bB2Rim7QICUapxZt4nm8q39fpT08rNDkv3xPVqDDwRdRg1g==
"@mui/base@5.0.0-alpha.118":
version "5.0.0-alpha.118"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.118.tgz#335e7496ea605c9b7bda4164efb2da3f09f36dfc"
integrity sha512-GAEpqhnuHjRaAZLdxFNuOf2GDTp9sUawM46oHZV4VnYPFjXJDkIYFWfIQLONb0nga92OiqS5DD/scGzVKCL0Mw==
dependencies:
"@babel/runtime" "^7.20.7"
"@babel/runtime" "^7.20.13"
"@emotion/is-prop-valid" "^1.2.0"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.2"
"@mui/utils" "^5.11.9"
"@popperjs/core" "^2.11.6"
clsx "^1.2.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/core-downloads-tracker@^5.11.6":
version "5.11.6"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.6.tgz#79a60c0d95a08859cccd62a8d9a5336ef477a840"
integrity sha512-lbD3qdafBOf2dlqKhOcVRxaPAujX+9UlPC6v8iMugMeAXe0TCgU3QbGXY3zrJsu6ex64WYDpH4y1+WOOBmWMuA==
"@mui/core-downloads-tracker@^5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.9.tgz#0d3b20c2ef7704537c38597f9ecfc1894fe7c367"
integrity sha512-YGEtucQ/Nl91VZkzYaLad47Cdui51n/hW+OQm4210g4N3/nZzBxmGeKfubEalf+ShKH4aYDS86XTO6q/TpZnjQ==
"@mui/joy@^5.0.0-alpha.63":
version "5.0.0-alpha.64"
resolved "https://registry.yarnpkg.com/@mui/joy/-/joy-5.0.0-alpha.64.tgz#e60d7ff9ba07b780f1726622cc99d67025fac38a"
integrity sha512-IC5/pRbkn0J0QtbkKDPU3mpqUZOQL4uC/N8E831p1wS78xoZUxTr2PXLtOXIpbOuadZjzMeC46+urvFObMl9ZQ==
"@mui/joy@^5.0.0-alpha.67":
version "5.0.0-alpha.67"
resolved "https://registry.yarnpkg.com/@mui/joy/-/joy-5.0.0-alpha.67.tgz#b9a0a15e82eb8a810b297a29d9e0c500fc3dbd6e"
integrity sha512-Hol7tYXtSPcl1pApn6fpVdr2NFbftlXWaP5ql2AJ2VGo/MfInIatHYBR+QtGbn+XLDuOnhSYh/wHDL9u3xzlaQ==
dependencies:
"@babel/runtime" "^7.20.7"
"@mui/base" "5.0.0-alpha.115"
"@mui/core-downloads-tracker" "^5.11.6"
"@mui/system" "^5.11.5"
"@babel/runtime" "^7.20.13"
"@mui/base" "5.0.0-alpha.118"
"@mui/core-downloads-tracker" "^5.11.9"
"@mui/system" "^5.11.9"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.2"
"@mui/utils" "^5.11.9"
clsx "^1.2.1"
csstype "^3.1.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/private-theming@^5.11.2":
version "5.11.2"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.2.tgz#93eafb317070888a988efa8d6a9ec1f69183a606"
integrity sha512-qZwMaqRFPwlYmqwVKblKBGKtIjJRAj3nsvX93pOmatsXyorW7N/0IPE/swPgz1VwChXhHO75DwBEx8tB+aRMNg==
"@mui/private-theming@^5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.9.tgz#ce3f7b7fa7de3e8d6b2a3132a22bffd6bfaabe9b"
integrity sha512-XMyVIFGomVCmCm92EvYlgq3zrC9K+J6r7IKl/rBJT2/xVYoRY6uM7jeB+Wxh7kXxnW9Dbqsr2yL3cx6wSD1sAg==
dependencies:
"@babel/runtime" "^7.20.7"
"@mui/utils" "^5.11.2"
"@babel/runtime" "^7.20.13"
"@mui/utils" "^5.11.9"
prop-types "^15.8.1"
"@mui/styled-engine@^5.11.0":
version "5.11.0"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.0.tgz#79afb30c612c7807c4b77602cf258526d3997c7b"
integrity sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==
"@mui/styled-engine@^5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.9.tgz#105da848163b993522de0deaada82e10ad357194"
integrity sha512-bkh2CjHKOMy98HyOc8wQXEZvhOmDa/bhxMUekFX5IG0/w4f5HJ8R6+K6nakUUYNEgjOWPYzNPrvGB8EcGbhahQ==
dependencies:
"@babel/runtime" "^7.20.6"
"@babel/runtime" "^7.20.13"
"@emotion/cache" "^11.10.5"
csstype "^3.1.1"
prop-types "^15.8.1"
"@mui/system@^5.11.5":
version "5.11.5"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.5.tgz#c880199634708c866063396f88d3fdd4c1dfcb48"
integrity sha512-KNVsJ0sgRRp2XBqhh4wPS5aacteqjwxgiYTVwVnll2fgkgunZKo3DsDiGMrFlCg25ZHA3Ax58txWGE9w58zp0w==
"@mui/system@^5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.9.tgz#61f83c538cb4bb9383bcfb39734d9d22ae11c3e7"
integrity sha512-h6uarf+l3FO6l75Nf7yO+qDGrIoa1DM9nAMCUFZQsNCDKOInRzcptnm8M1w/Z3gVetfeeGoIGAYuYKbft6KZZA==
dependencies:
"@babel/runtime" "^7.20.7"
"@mui/private-theming" "^5.11.2"
"@mui/styled-engine" "^5.11.0"
"@babel/runtime" "^7.20.13"
"@mui/private-theming" "^5.11.9"
"@mui/styled-engine" "^5.11.9"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.2"
"@mui/utils" "^5.11.9"
clsx "^1.2.1"
csstype "^3.1.1"
prop-types "^15.8.1"
@ -428,12 +428,12 @@
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
"@mui/utils@^5.11.2":
version "5.11.2"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.2.tgz#29764311acb99425159b159b1cb382153ad9be1f"
integrity sha512-AyizuHHlGdAtH5hOOXBW3kriuIwUIKUIgg0P7LzMvzf6jPhoQbENYqY6zJqfoZ7fAWMNNYT8mgN5EftNGzwE2w==
"@mui/utils@^5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.9.tgz#8fab9cf773c63ad916597921860d2344b5d4b706"
integrity sha512-eOJaqzcEs4qEwolcvFAmXGpln+uvouvOS9FUX6Wkrte+4I8rZbjODOBDVNlK+V6/ziTfD4iNKC0G+KfOTApbqg==
dependencies:
"@babel/runtime" "^7.20.7"
"@babel/runtime" "^7.20.13"
"@types/prop-types" "^15.7.5"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.8.1"

Loading…
Cancel
Save