feat: implement OIDC settings
This commit is contained in:
@@ -62,17 +62,20 @@ const StyledForm = styled.form`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const VALUE_NOT_SET = {};
|
||||||
|
const VALUES_NOT_SET = {};
|
||||||
|
|
||||||
export default function Radio({
|
export default function Radio({
|
||||||
options,
|
options,
|
||||||
values = undefined,
|
values = VALUES_NOT_SET,
|
||||||
value = undefined,
|
value = VALUE_NOT_SET,
|
||||||
defaultValue = undefined,
|
defaultValue = undefined,
|
||||||
onChange = undefined
|
onChange = undefined
|
||||||
}) {
|
}) {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
const [fallbackValue, setFallbackValue] = useState(defaultValue);
|
const [fallbackValue, setFallbackValue] = useState(defaultValue);
|
||||||
const _value = value !== undefined ? value : fallbackValue;
|
const _value = value !== VALUE_NOT_SET ? value : fallbackValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledForm>
|
<StyledForm>
|
||||||
@@ -80,11 +83,11 @@ export default function Radio({
|
|||||||
<div className="option" key={index}>
|
<div className="option" key={index}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={(values !== undefined ? values.indexOf(_value) : _value) === index}
|
checked={(values !== VALUES_NOT_SET ? values.indexOf(_value) : _value) === index}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
const valueToSet = values === undefined ? index : values[index];
|
const valueToSet = values === VALUES_NOT_SET ? index : values[index];
|
||||||
// Set fallback value if not in controlled mode
|
// Set fallback value if not in controlled mode
|
||||||
if (value === undefined) {
|
if (value === VALUE_NOT_SET) {
|
||||||
setFallbackValue(valueToSet);
|
setFallbackValue(valueToSet);
|
||||||
}
|
}
|
||||||
// Invoke `onChange` handler if defined
|
// Invoke `onChange` handler if defined
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ const Styled = styled.div`
|
|||||||
height: 20px !important;
|
height: 20px !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export default function Select({ options = [], updateSelect = null }) {
|
|
||||||
|
const CURRENT_NOT_SET = {};
|
||||||
|
|
||||||
|
export default function Select({ options = [], updateSelect = null, current = CURRENT_NOT_SET }) {
|
||||||
const [optionsVisible, setOptionsVisible] = useState(false);
|
const [optionsVisible, setOptionsVisible] = useState(false);
|
||||||
const [curr, setCurr] = useState(undefined);
|
const [curr, setCurr] = useState(undefined);
|
||||||
const toggleVisible = () => {
|
const toggleVisible = () => {
|
||||||
@@ -62,7 +65,9 @@ export default function Select({ options = [], updateSelect = null }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Styled onClick={toggleVisible}>
|
<Styled onClick={toggleVisible}>
|
||||||
<span className="txt">{curr?.title || `Select`}</span>
|
<span className="txt">
|
||||||
|
{(current !== CURRENT_NOT_SET ? current : curr)?.title || `Select`}
|
||||||
|
</span>
|
||||||
<IconArrow className="icon" />
|
<IconArrow className="icon" />
|
||||||
</Styled>
|
</Styled>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
|
|||||||
+2
-6
@@ -10,12 +10,8 @@ export const isImage = (file_type = "", size = 0) => {
|
|||||||
return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE;
|
return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE;
|
||||||
};
|
};
|
||||||
export const isObjectEqual = (obj1, obj2) => {
|
export const isObjectEqual = (obj1, obj2) => {
|
||||||
let o1 = Object.entries(obj1 ?? {})
|
let o1 = JSON.stringify(obj1 ?? {});
|
||||||
.sort()
|
let o2 = JSON.stringify(obj2 ?? {});
|
||||||
.toString();
|
|
||||||
let o2 = Object.entries(obj2 ?? {})
|
|
||||||
.sort()
|
|
||||||
.toString();
|
|
||||||
return o1 === o2;
|
return o1 === o2;
|
||||||
};
|
};
|
||||||
export const isTreatAsImage = (file) => {
|
export const isTreatAsImage = (file) => {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
import { useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import StyledModal from "../../common/component/styled/Modal";
|
||||||
|
import Modal from "../../common/component/Modal";
|
||||||
|
import { StyledSocialButton } from "./styled";
|
||||||
|
import solidSvg from "../../assets/icons/solid.svg?url";
|
||||||
|
import StyledButton from "../../common/component/styled/Button";
|
||||||
|
import SolidLoginButton from "./SolidLoginButton";
|
||||||
|
|
||||||
|
const StyledOicdLoginModal = styled(StyledModal)`
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 32px 16px;
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
> .icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&Cancel {
|
||||||
|
color: #8f8f8f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function OidcLoginButton({ issuers }) {
|
||||||
|
const [modal, setModal] = useState(true);
|
||||||
|
if (!issuers) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledSocialButton
|
||||||
|
onClick={() => {
|
||||||
|
setModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={solidSvg} className="icon" alt="solid icon" />
|
||||||
|
Sign in with OIDC
|
||||||
|
</StyledSocialButton>
|
||||||
|
{modal && (
|
||||||
|
<Modal id="modal-modal">
|
||||||
|
<StyledOicdLoginModal title="Login with OIDC">
|
||||||
|
{issuers
|
||||||
|
.filter((issuer) => issuer.enable)
|
||||||
|
.map((issuer, index) => (
|
||||||
|
<SolidLoginButton issuer={issuer} key={index} />
|
||||||
|
))}
|
||||||
|
<StyledButton
|
||||||
|
className="border_less ghost buttonCancel"
|
||||||
|
onClick={() => {
|
||||||
|
setModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</StyledButton>
|
||||||
|
</StyledOicdLoginModal>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useGetOpenidMutation } from "../../app/services/auth";
|
import { useGetOpenidMutation } from "../../app/services/auth";
|
||||||
import solidSvg from "../../assets/icons/solid.svg?url";
|
|
||||||
import { StyledSocialButton } from "./styled";
|
import { StyledSocialButton } from "./styled";
|
||||||
|
|
||||||
export default function SolidLoginButton({ issuers }) {
|
export default function SolidLoginButton({ issuer }) {
|
||||||
const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation();
|
const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation();
|
||||||
|
|
||||||
const handleSolidLogin = () => {
|
const handleSolidLogin = () => {
|
||||||
getOpenId({
|
getOpenId({
|
||||||
// issuer: "solidweb.org",
|
// issuer: "solidweb.org",
|
||||||
issuer: issuers[0],
|
issuer,
|
||||||
redirect_uri: `${location.origin}/#/login`
|
redirect_uri: `${location.origin}/#/login`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -24,8 +23,8 @@ export default function SolidLoginButton({ issuers }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSocialButton disabled={isLoading} onClick={handleSolidLogin}>
|
<StyledSocialButton disabled={isLoading} onClick={handleSolidLogin}>
|
||||||
<img src={solidSvg} className="icon" alt="solid icon" />
|
<img src={issuer.favicon} className="icon" alt="icon" />
|
||||||
{isLoading ? `Redirecting...` : `Sign in with Solid`}
|
Login with {issuer.domain}
|
||||||
</StyledSocialButton>
|
</StyledSocialButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import BASE_URL from "../../app/config";
|
|||||||
// import web3 from "web3";
|
// import web3 from "web3";
|
||||||
import StyledWrapper from "./styled";
|
import StyledWrapper from "./styled";
|
||||||
import MetamaskLoginButton from "./MetamaskLoginButton";
|
import MetamaskLoginButton from "./MetamaskLoginButton";
|
||||||
import SolidLoginButton from "./SolidLoginButton";
|
import OidcLoginButton from "./OidcLoginButton";
|
||||||
import Input from "../../common/component/styled/Input";
|
import Input from "../../common/component/styled/Input";
|
||||||
import Button from "../../common/component/styled/Button";
|
import Button from "../../common/component/styled/Button";
|
||||||
import GoogleLoginButton from "./GoogleLoginButton";
|
import GoogleLoginButton from "./GoogleLoginButton";
|
||||||
@@ -16,7 +16,6 @@ import { useGetLoginConfigQuery, useGetSMTPStatusQuery } from "../../app/service
|
|||||||
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
||||||
import GithubLoginButton from "./GithubLoginButton";
|
import GithubLoginButton from "./GithubLoginButton";
|
||||||
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { data: enableSMTP } = useGetSMTPStatusQuery();
|
const { data: enableSMTP } = useGetSMTPStatusQuery();
|
||||||
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
|
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
|
||||||
@@ -171,7 +170,7 @@ export default function LoginPage() {
|
|||||||
{googleLogin && <GoogleLoginButton login={login} clientId={clientId} />}
|
{googleLogin && <GoogleLoginButton login={login} clientId={clientId} />}
|
||||||
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
|
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
|
||||||
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
|
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
|
||||||
{oidc.length > 0 && <SolidLoginButton issuers={oidc} />}
|
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
|
||||||
{whoCanSignUp === "EveryOne" && <SignUpLink />}
|
{whoCanSignUp === "EveryOne" && <SignUpLink />}
|
||||||
</div>
|
</div>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import Styled from "./styled";
|
|||||||
// import IconPlus from "../../../../assets/icons/plus.circle.svg";
|
// import IconPlus from "../../../../assets/icons/plus.circle.svg";
|
||||||
import IconMinus from "../../../../assets/icons/minus.circle.svg";
|
import IconMinus from "../../../../assets/icons/minus.circle.svg";
|
||||||
|
|
||||||
export default function IssuerList({ issuers = [] }) {
|
export default function IssuerList({ issuers = [], onChange }) {
|
||||||
const [select, setSelect] = useState(undefined);
|
const [select, setSelect] = useState({});
|
||||||
const [newDomain, setNewDomain] = useState("");
|
const [newDomain, setNewDomain] = useState("");
|
||||||
const handleNewDomain = (evt) => {
|
const handleNewDomain = (evt) => {
|
||||||
setNewDomain(evt.target.value);
|
setNewDomain(evt.target.value);
|
||||||
};
|
};
|
||||||
const disableBtn = !(newDomain || !!select?.value);
|
const disableBtn =
|
||||||
|
(!newDomain && !select?.value) ||
|
||||||
|
!select?.title ||
|
||||||
|
issuers.some((issuer) => issuer.domain === newDomain);
|
||||||
return (
|
return (
|
||||||
<Styled>
|
<Styled>
|
||||||
<ul className="issuers">
|
<ul className="issuers">
|
||||||
@@ -22,9 +25,14 @@ export default function IssuerList({ issuers = [] }) {
|
|||||||
return (
|
return (
|
||||||
<li key={domain} className="issuer">
|
<li key={domain} className="issuer">
|
||||||
<div className="left">
|
<div className="left">
|
||||||
<IconMinus className="remove" />
|
<IconMinus
|
||||||
|
className="remove"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(issuers.filter((issuer) => issuer.domain !== domain));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<img src={favicon} alt="logo" className="icon" />
|
{Boolean(favicon) && <img src={favicon} alt="logo" className="icon" />}
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
readOnly
|
||||||
value={domain}
|
value={domain}
|
||||||
@@ -35,7 +43,17 @@ export default function IssuerList({ issuers = [] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="right">
|
<div className="right">
|
||||||
<Toggle data-checked={enable} />
|
<Toggle
|
||||||
|
data-checked={enable}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(
|
||||||
|
issuers.map((issuer) => ({
|
||||||
|
...issuer,
|
||||||
|
enable: issuer.domain === domain ? !enable : issuer.enable
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -43,7 +61,14 @@ export default function IssuerList({ issuers = [] }) {
|
|||||||
|
|
||||||
<li className="issuer add">
|
<li className="issuer add">
|
||||||
<div className="left">
|
<div className="left">
|
||||||
<Select options={options} updateSelect={setSelect} />
|
<Select
|
||||||
|
options={options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
selected: issuers.some((issuer) => issuer.domain === option.value)
|
||||||
|
}))}
|
||||||
|
current={select}
|
||||||
|
updateSelect={setSelect}
|
||||||
|
/>
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<Input
|
<Input
|
||||||
onChange={handleNewDomain}
|
onChange={handleNewDomain}
|
||||||
@@ -56,7 +81,23 @@ export default function IssuerList({ issuers = [] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="right">
|
<div className="right">
|
||||||
<Button disabled={disableBtn}>Add</Button>
|
<Button
|
||||||
|
disabled={disableBtn}
|
||||||
|
onClick={() => {
|
||||||
|
const { icon, value } = options.find((option) => option.value === select.value);
|
||||||
|
onChange(
|
||||||
|
issuers.concat({
|
||||||
|
enable: true,
|
||||||
|
favicon: icon || "",
|
||||||
|
domain: value || newDomain
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setSelect({});
|
||||||
|
setNewDomain("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -18,14 +18,13 @@ const Styled = styled.div`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
.remove {
|
.remove {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.data {
|
.data {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
> .icon {
|
> .icon {
|
||||||
|
|||||||
@@ -182,7 +182,15 @@ export default function Logins() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<IssuerList issuers={oidc} />
|
<IssuerList
|
||||||
|
issuers={oidc}
|
||||||
|
onChange={(newOidc) => {
|
||||||
|
setValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
oidc: newOidc
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user