feat: implement OIDC settings

This commit is contained in:
Liyang Zhu
2022-06-12 00:58:05 +08:00
parent cf09bfea3b
commit 585dc59a1d
10 changed files with 243 additions and 120 deletions
+9 -6
View File
@@ -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
+7 -2
View File
@@ -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
View File
@@ -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) => {
+65
View 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>
)}
</>
);
}
+4 -5
View File
@@ -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>
); );
} }
+2 -3
View File
@@ -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>
+49 -8
View File
@@ -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 {
+9 -1
View File
@@ -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>