mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement sign in with SSO (#1119)
* feat: implement sign in with SSO * chore: update * chore: update * chore: update
This commit is contained in:
@ -5,6 +5,12 @@ type SignIn struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSOSignIn struct {
|
||||||
|
IdentityProviderID int `json:"identityProviderId"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
RedirectURI string `json:"redirectUri"`
|
||||||
|
}
|
||||||
|
|
||||||
type SignUp struct {
|
type SignUp struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -35,3 +37,24 @@ func Min(x, y int) int {
|
|||||||
}
|
}
|
||||||
return y
|
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)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,15 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/usememos/memos/api"
|
"github.com/usememos/memos/api"
|
||||||
"github.com/usememos/memos/common"
|
"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"
|
metric "github.com/usememos/memos/plugin/metrics"
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@ -50,6 +54,90 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
|||||||
return c.JSON(http.StatusOK, composeResponse(user))
|
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 {
|
g.POST("/auth/signup", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
signup := &api.SignUp{}
|
signup := &api.SignUp{}
|
||||||
|
@ -91,30 +91,33 @@ func (s *Server) registerIdentityProviderRoutes(g *echo.Group) {
|
|||||||
|
|
||||||
g.GET("/idp", func(c echo.Context) error {
|
g.GET("/idp", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
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{})
|
identityProviderMessageList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProviderMessage{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider list").SetInternal(err)
|
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{}
|
identityProviderList := []*api.IdentityProvider{}
|
||||||
for _, identityProviderMessage := range identityProviderMessageList {
|
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))
|
return c.JSON(http.StatusOK, composeResponse(identityProviderList))
|
||||||
})
|
})
|
||||||
|
@ -98,11 +98,9 @@ func (s *Store) CreateIdentityProvider(ctx context.Context, create *IdentityProv
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
identityProviderMessage := create
|
identityProviderMessage := create
|
||||||
s.idpCache.Store(identityProviderMessage.ID, identityProviderMessage)
|
s.idpCache.Store(identityProviderMessage.ID, identityProviderMessage)
|
||||||
return identityProviderMessage, nil
|
return identityProviderMessage, nil
|
||||||
@ -208,7 +206,9 @@ func (s *Store) UpdateIdentityProvider(ctx context.Context, update *UpdateIdenti
|
|||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("unsupported idp type %s", string(identityProviderMessage.Type))
|
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)
|
s.idpCache.Store(identityProviderMessage.ID, identityProviderMessage)
|
||||||
return &identityProviderMessage, nil
|
return &identityProviderMessage, nil
|
||||||
}
|
}
|
||||||
@ -234,6 +234,9 @@ func (s *Store) DeleteIdentityProvider(ctx context.Context, delete *DeleteIdenti
|
|||||||
if rows == 0 {
|
if rows == 0 {
|
||||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("idp not found")}
|
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)
|
s.idpCache.Delete(delete.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^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",
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"copy-to-clipboard": "^3.3.2",
|
"copy-to-clipboard": "^3.3.2",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 * as api from "../helpers/api";
|
||||||
|
import { UNKNOWN_ID } from "../helpers/consts";
|
||||||
|
import { absolutifyLink } from "../helpers/utils";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
@ -10,6 +12,72 @@ interface Props extends DialogProps {
|
|||||||
confirmCallback?: () => void;
|
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 CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { confirmCallback, destroy, identityProvider } = props;
|
const { confirmCallback, destroy, identityProvider } = props;
|
||||||
const [basicInfo, setBasicInfo] = useState({
|
const [basicInfo, setBasicInfo] = useState({
|
||||||
@ -31,6 +99,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [oauth2Scopes, setOAuth2Scopes] = useState<string>("");
|
const [oauth2Scopes, setOAuth2Scopes] = useState<string>("");
|
||||||
|
const [seletedTemplate, setSelectedTemplate] = useState<string>("GitHub");
|
||||||
const isCreating = identityProvider === undefined;
|
const isCreating = identityProvider === undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
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 = () => {
|
const handleCloseBtnClick = () => {
|
||||||
destroy();
|
destroy();
|
||||||
};
|
};
|
||||||
@ -84,6 +172,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
toastHelper.info(`SSO ${basicInfo.name} created`);
|
||||||
} else {
|
} else {
|
||||||
await api.patchIdentityProvider({
|
await api.patchIdentityProvider({
|
||||||
id: identityProvider?.id,
|
id: identityProvider?.id,
|
||||||
@ -96,6 +185,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
toastHelper.info(`SSO ${basicInfo.name} updated`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -124,14 +214,34 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<Typography className="!mb-1" level="body2">
|
{isCreating && (
|
||||||
Type
|
<>
|
||||||
</Typography>
|
<Typography className="!mb-1" level="body2">
|
||||||
<RadioGroup className="mb-2" value={type}>
|
Type
|
||||||
<List>
|
</Typography>
|
||||||
<Radio value="OAUTH2" label="OAuth 2.0" />
|
<RadioGroup className="mb-2" value={type}>
|
||||||
</List>
|
<div className="mt-2 w-full flex flex-row space-x-4">
|
||||||
</RadioGroup>
|
<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">
|
<Typography className="!mb-1" level="body2">
|
||||||
Name<span className="text-red-600">*</span>
|
Name<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -165,6 +275,11 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Divider className="!my-2" />
|
<Divider className="!my-2" />
|
||||||
{type === "OAUTH2" && (
|
{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">
|
<Typography className="!mb-1" level="body2">
|
||||||
Client ID<span className="text-red-600">*</span>
|
Client ID<span className="text-red-600">*</span>
|
||||||
</Typography>
|
</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) {
|
export function signup(username: string, password: string) {
|
||||||
return axios.post<ResponseObject<User>>("/api/auth/signup", {
|
return axios.post<ResponseObject<User>>("/api/auth/signup", {
|
||||||
username,
|
username,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.page-wrapper.auth {
|
.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 {
|
> .page-container {
|
||||||
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center ml-calc;
|
@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;
|
@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 {
|
&.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;
|
@apply py-2;
|
||||||
|
|
||||||
> input {
|
> 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 { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useGlobalStore, useUserStore } from "../store/module";
|
import { useGlobalStore, useUserStore } from "../store/module";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
|
import { absolutifyLink } from "../helpers/utils";
|
||||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
@ -20,7 +21,6 @@ const validateConfig: ValidatorConfig = {
|
|||||||
|
|
||||||
const Auth = () => {
|
const Auth = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const actionBtnLoadingState = useLoading(false);
|
const actionBtnLoadingState = useLoading(false);
|
||||||
@ -28,9 +28,17 @@ const Auth = () => {
|
|||||||
const mode = systemStatus.profile.mode;
|
const mode = systemStatus.profile.mode;
|
||||||
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
||||||
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
||||||
|
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userStore.doSignOut().catch();
|
userStore.doSignOut().catch();
|
||||||
|
const fetchIdentityProviderList = async () => {
|
||||||
|
const {
|
||||||
|
data: { data: identityProviderList },
|
||||||
|
} = await api.getIdentityProviderList();
|
||||||
|
setIdentityProviderList(identityProviderList);
|
||||||
|
};
|
||||||
|
fetchIdentityProviderList();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -73,7 +81,7 @@ const Auth = () => {
|
|||||||
await api.signin(username, password);
|
await api.signin(username, password);
|
||||||
const user = await userStore.doSignIn();
|
const user = await userStore.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/");
|
window.location.href = "/";
|
||||||
} else {
|
} else {
|
||||||
toastHelper.error(t("message.login-failed"));
|
toastHelper.error(t("message.login-failed"));
|
||||||
}
|
}
|
||||||
@ -106,7 +114,7 @@ const Auth = () => {
|
|||||||
await api.signup(username, password);
|
await api.signup(username, password);
|
||||||
const user = await userStore.doSignIn();
|
const user = await userStore.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/");
|
window.location.href = "/";
|
||||||
} else {
|
} else {
|
||||||
toastHelper.error(t("common.singup-failed"));
|
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 (
|
return (
|
||||||
<div className="page-wrapper auth">
|
<div className="page-wrapper auth">
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
@ -175,6 +197,25 @@ const Auth = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>}
|
{!systemStatus?.host && <p className="tip-text">{t("auth.host-tip")}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center w-full gap-2">
|
<div className="flex flex-row items-center justify-center w-full gap-2">
|
||||||
|
74
web/src/pages/AuthCallback.tsx
Normal file
74
web/src/pages/AuthCallback.tsx
Normal file
@ -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";
|
import { initialGlobalState, initialUserState } from "../store/module";
|
||||||
|
|
||||||
const Auth = lazy(() => import("../pages/Auth"));
|
const Auth = lazy(() => import("../pages/Auth"));
|
||||||
|
const AuthCallback = lazy(() => import("../pages/AuthCallback"));
|
||||||
const Explore = lazy(() => import("../pages/Explore"));
|
const Explore = lazy(() => import("../pages/Explore"));
|
||||||
const Home = lazy(() => import("../pages/Home"));
|
const Home = lazy(() => import("../pages/Home"));
|
||||||
const MemoDetail = lazy(() => import("../pages/MemoDetail"));
|
const MemoDetail = lazy(() => import("../pages/MemoDetail"));
|
||||||
@ -36,6 +37,10 @@ const router = createBrowserRouter([
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/callback",
|
||||||
|
element: <AuthCallback />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.18.6"
|
"@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"
|
version "7.20.13"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
||||||
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
||||||
@ -355,70 +355,70 @@
|
|||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||||
|
|
||||||
"@mui/base@5.0.0-alpha.115":
|
"@mui/base@5.0.0-alpha.118":
|
||||||
version "5.0.0-alpha.115"
|
version "5.0.0-alpha.118"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.115.tgz#582b147fda56fe52d561fe9f64406e036d882338"
|
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.118.tgz#335e7496ea605c9b7bda4164efb2da3f09f36dfc"
|
||||||
integrity sha512-OGQ84whT/yNYd6xKCGGS6MxqEfjVjk5esXM7HP6bB2Rim7QICUapxZt4nm8q39fpT08rNDkv3xPVqDDwRdRg1g==
|
integrity sha512-GAEpqhnuHjRaAZLdxFNuOf2GDTp9sUawM46oHZV4VnYPFjXJDkIYFWfIQLONb0nga92OiqS5DD/scGzVKCL0Mw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.7"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@emotion/is-prop-valid" "^1.2.0"
|
"@emotion/is-prop-valid" "^1.2.0"
|
||||||
"@mui/types" "^7.2.3"
|
"@mui/types" "^7.2.3"
|
||||||
"@mui/utils" "^5.11.2"
|
"@mui/utils" "^5.11.9"
|
||||||
"@popperjs/core" "^2.11.6"
|
"@popperjs/core" "^2.11.6"
|
||||||
clsx "^1.2.1"
|
clsx "^1.2.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
react-is "^18.2.0"
|
react-is "^18.2.0"
|
||||||
|
|
||||||
"@mui/core-downloads-tracker@^5.11.6":
|
"@mui/core-downloads-tracker@^5.11.9":
|
||||||
version "5.11.6"
|
version "5.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.6.tgz#79a60c0d95a08859cccd62a8d9a5336ef477a840"
|
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.9.tgz#0d3b20c2ef7704537c38597f9ecfc1894fe7c367"
|
||||||
integrity sha512-lbD3qdafBOf2dlqKhOcVRxaPAujX+9UlPC6v8iMugMeAXe0TCgU3QbGXY3zrJsu6ex64WYDpH4y1+WOOBmWMuA==
|
integrity sha512-YGEtucQ/Nl91VZkzYaLad47Cdui51n/hW+OQm4210g4N3/nZzBxmGeKfubEalf+ShKH4aYDS86XTO6q/TpZnjQ==
|
||||||
|
|
||||||
"@mui/joy@^5.0.0-alpha.63":
|
"@mui/joy@^5.0.0-alpha.67":
|
||||||
version "5.0.0-alpha.64"
|
version "5.0.0-alpha.67"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/joy/-/joy-5.0.0-alpha.64.tgz#e60d7ff9ba07b780f1726622cc99d67025fac38a"
|
resolved "https://registry.yarnpkg.com/@mui/joy/-/joy-5.0.0-alpha.67.tgz#b9a0a15e82eb8a810b297a29d9e0c500fc3dbd6e"
|
||||||
integrity sha512-IC5/pRbkn0J0QtbkKDPU3mpqUZOQL4uC/N8E831p1wS78xoZUxTr2PXLtOXIpbOuadZjzMeC46+urvFObMl9ZQ==
|
integrity sha512-Hol7tYXtSPcl1pApn6fpVdr2NFbftlXWaP5ql2AJ2VGo/MfInIatHYBR+QtGbn+XLDuOnhSYh/wHDL9u3xzlaQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.7"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@mui/base" "5.0.0-alpha.115"
|
"@mui/base" "5.0.0-alpha.118"
|
||||||
"@mui/core-downloads-tracker" "^5.11.6"
|
"@mui/core-downloads-tracker" "^5.11.9"
|
||||||
"@mui/system" "^5.11.5"
|
"@mui/system" "^5.11.9"
|
||||||
"@mui/types" "^7.2.3"
|
"@mui/types" "^7.2.3"
|
||||||
"@mui/utils" "^5.11.2"
|
"@mui/utils" "^5.11.9"
|
||||||
clsx "^1.2.1"
|
clsx "^1.2.1"
|
||||||
csstype "^3.1.1"
|
csstype "^3.1.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
react-is "^18.2.0"
|
react-is "^18.2.0"
|
||||||
|
|
||||||
"@mui/private-theming@^5.11.2":
|
"@mui/private-theming@^5.11.9":
|
||||||
version "5.11.2"
|
version "5.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.2.tgz#93eafb317070888a988efa8d6a9ec1f69183a606"
|
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.9.tgz#ce3f7b7fa7de3e8d6b2a3132a22bffd6bfaabe9b"
|
||||||
integrity sha512-qZwMaqRFPwlYmqwVKblKBGKtIjJRAj3nsvX93pOmatsXyorW7N/0IPE/swPgz1VwChXhHO75DwBEx8tB+aRMNg==
|
integrity sha512-XMyVIFGomVCmCm92EvYlgq3zrC9K+J6r7IKl/rBJT2/xVYoRY6uM7jeB+Wxh7kXxnW9Dbqsr2yL3cx6wSD1sAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.7"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@mui/utils" "^5.11.2"
|
"@mui/utils" "^5.11.9"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/styled-engine@^5.11.0":
|
"@mui/styled-engine@^5.11.9":
|
||||||
version "5.11.0"
|
version "5.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.0.tgz#79afb30c612c7807c4b77602cf258526d3997c7b"
|
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.9.tgz#105da848163b993522de0deaada82e10ad357194"
|
||||||
integrity sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==
|
integrity sha512-bkh2CjHKOMy98HyOc8wQXEZvhOmDa/bhxMUekFX5IG0/w4f5HJ8R6+K6nakUUYNEgjOWPYzNPrvGB8EcGbhahQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.6"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@emotion/cache" "^11.10.5"
|
"@emotion/cache" "^11.10.5"
|
||||||
csstype "^3.1.1"
|
csstype "^3.1.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/system@^5.11.5":
|
"@mui/system@^5.11.9":
|
||||||
version "5.11.5"
|
version "5.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.5.tgz#c880199634708c866063396f88d3fdd4c1dfcb48"
|
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.9.tgz#61f83c538cb4bb9383bcfb39734d9d22ae11c3e7"
|
||||||
integrity sha512-KNVsJ0sgRRp2XBqhh4wPS5aacteqjwxgiYTVwVnll2fgkgunZKo3DsDiGMrFlCg25ZHA3Ax58txWGE9w58zp0w==
|
integrity sha512-h6uarf+l3FO6l75Nf7yO+qDGrIoa1DM9nAMCUFZQsNCDKOInRzcptnm8M1w/Z3gVetfeeGoIGAYuYKbft6KZZA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.7"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@mui/private-theming" "^5.11.2"
|
"@mui/private-theming" "^5.11.9"
|
||||||
"@mui/styled-engine" "^5.11.0"
|
"@mui/styled-engine" "^5.11.9"
|
||||||
"@mui/types" "^7.2.3"
|
"@mui/types" "^7.2.3"
|
||||||
"@mui/utils" "^5.11.2"
|
"@mui/utils" "^5.11.9"
|
||||||
clsx "^1.2.1"
|
clsx "^1.2.1"
|
||||||
csstype "^3.1.1"
|
csstype "^3.1.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
@ -428,12 +428,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
|
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
|
||||||
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
|
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
|
||||||
|
|
||||||
"@mui/utils@^5.11.2":
|
"@mui/utils@^5.11.9":
|
||||||
version "5.11.2"
|
version "5.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.2.tgz#29764311acb99425159b159b1cb382153ad9be1f"
|
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.9.tgz#8fab9cf773c63ad916597921860d2344b5d4b706"
|
||||||
integrity sha512-AyizuHHlGdAtH5hOOXBW3kriuIwUIKUIgg0P7LzMvzf6jPhoQbENYqY6zJqfoZ7fAWMNNYT8mgN5EftNGzwE2w==
|
integrity sha512-eOJaqzcEs4qEwolcvFAmXGpln+uvouvOS9FUX6Wkrte+4I8rZbjODOBDVNlK+V6/ziTfD4iNKC0G+KfOTApbqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.7"
|
"@babel/runtime" "^7.20.13"
|
||||||
"@types/prop-types" "^15.7.5"
|
"@types/prop-types" "^15.7.5"
|
||||||
"@types/react-is" "^16.7.1 || ^17.0.0"
|
"@types/react-is" "^16.7.1 || ^17.0.0"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
Reference in New Issue
Block a user