diff --git a/api/auth.go b/api/auth.go index b0a98bdd..898042b4 100644 --- a/api/auth.go +++ b/api/auth.go @@ -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"` diff --git a/common/util.go b/common/util.go index 228d9f1f..571dbf44 100644 --- a/common/util.go +++ b/common/util.go @@ -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 +} diff --git a/server/acl.go b/server/acl.go index 8656a4e6..7c2a1a3b 100644 --- a/server/acl.go +++ b/server/acl.go @@ -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) } diff --git a/server/auth.go b/server/auth.go index 80c9fa7e..a5b68a1c 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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{} diff --git a/server/idp.go b/server/idp.go index 1b369b5c..cae936f4 100644 --- a/server/idp.go +++ b/server/idp.go @@ -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)) }) diff --git a/store/idp.go b/store/idp.go index 04d66182..72b0ca3d 100644 --- a/store/idp.go +++ b/store/idp.go @@ -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 } diff --git a/web/package.json b/web/package.json index 53ccfc77..600b7b31 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/components/CreateIdentityProviderDialog.tsx b/web/src/components/CreateIdentityProviderDialog.tsx index 76bbced0..1f2cdfd4 100644 --- a/web/src/components/CreateIdentityProviderDialog.tsx +++ b/web/src/components/CreateIdentityProviderDialog.tsx @@ -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) => { const { confirmCallback, destroy, identityProvider } = props; const [basicInfo, setBasicInfo] = useState({ @@ -31,6 +99,7 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { }, }); const [oauth2Scopes, setOAuth2Scopes] = useState(""); + const [seletedTemplate, setSelectedTemplate] = useState("GitHub"); const isCreating = identityProvider === undefined; useEffect(() => { @@ -47,6 +116,25 @@ const CreateIdentityProviderDialog: React.FC = (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) => { }, }, }); + toastHelper.info(`SSO ${basicInfo.name} created`); } else { await api.patchIdentityProvider({ id: identityProvider?.id, @@ -96,6 +185,7 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { }, }, }); + toastHelper.info(`SSO ${basicInfo.name} updated`); } } catch (error: any) { console.error(error); @@ -124,14 +214,34 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => {
- - Type - - - - - - + {isCreating && ( + <> + + Type + + +
+ +
+
+ + Template + + +
+ {templateList.map((template) => ( + setSelectedTemplate(e.target.value)} + /> + ))} +
+
+ + + )} Name* @@ -165,6 +275,11 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { {type === "OAUTH2" && ( <> + {isCreating && ( + + Redirect URL: {absolutifyLink("/auth/callback")} + + )} Client ID* diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 5aa675a4..ec262ae7 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -25,6 +25,14 @@ export function signin(username: string, password: string) { }); } +export function signinWithSSO(identityProviderId: IdentityProviderId, code: string, redirectUri: string) { + return axios.post>("/api/auth/signin/sso", { + identityProviderId, + code, + redirectUri, + }); +} + export function signup(username: string, password: string) { return axios.post>("/api/auth/signup", { username, diff --git a/web/src/less/auth.less b/web/src/less/auth.less index a88f60f5..ed1bb0e4 100644 --- a/web/src/less/auth.less +++ b/web/src/less/auth.less @@ -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; } } } diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index d8469d2e..0cb772e6 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -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([]); useEffect(() => { userStore.doSignOut().catch(); + const fetchIdentityProviderList = async () => { + const { + data: { data: identityProviderList }, + } = await api.getIdentityProviderList(); + setIdentityProviderList(identityProviderList); + }; + fetchIdentityProviderList(); }, []); const handleUsernameInputChanged = (e: React.ChangeEvent) => { @@ -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 (
@@ -175,6 +197,25 @@ const Auth = () => { )}
+ {identityProviderList.length > 0 && ( + <> + or +
+ {identityProviderList.map((identityProvider) => ( + + ))} +
+ + )} {!systemStatus?.host &&

{t("auth.host-tip")}

}
diff --git a/web/src/pages/AuthCallback.tsx b/web/src/pages/AuthCallback.tsx new file mode 100644 index 00000000..47a0d735 --- /dev/null +++ b/web/src/pages/AuthCallback.tsx @@ -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({ + 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 ( +
+ {state.loading ? ( + + ) : ( +
{state.errorMessage}
+ )} +
+ ); +}; + +export default AuthCallback; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 36a4e795..0563bf57 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -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: , + }, { path: "/", element: , diff --git a/web/yarn.lock b/web/yarn.lock index 1aef5879..8abe9789 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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"