This commit is contained in:
2025-08-25 20:24:23 +08:00
parent 30106e0129
commit 0ae8d7a709
1044 changed files with 321581 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 3.2.3 (2025-07-31)
**Note:** Version bump only for package decap-cms-ui-auth

View File

@@ -0,0 +1,28 @@
# decap-cms-ui-auth
Authentication UI pages used by the decap-cms-backend-* packages.
## Common Behavior
* An authenticator must return the following fields:
* email
* token?
* expires?
## Components
* **NetlifyAuthenticationPage**
* Username and password fields that are passed to Netlify Identity.
* Requires a static `authClient` value set before login will work, expected to be set by the backend implementation.
* Returns object that satisfies the GitGatewayUser type (and inherited Credentials type) from Netlify
* **PKCEAuthenticationPage**
* OAuth2 PKCE flow with optional OIDC auto-configuration.
* Returns object that satisfies the GitGatewayUser type (and inherited Credentials type), with additional data:
* token (part of Credentials definition): the access token
* idToken: if available
* claims: if available (decoded access token)
* idClaims: if available (decoded ID token)
* email: mapped email value from the token claims, if available
* user_metadata.full_name: mapped value from the token claims, if available
* user_metadata.avatar_url: mapped value from the token claims, if available

View File

@@ -0,0 +1,16 @@
declare module 'decap-cms-ui-auth' {
import React from 'react';
import type { Implementation } from 'decap-cms-lib-util/src';
class PKCEAuthenticationPage extends React.Component {
constructor({ backend }: { backend: Implementation });
handleLogin(e: ChangeEvent<HTMLInputElement>): void;
}
class NetlifyAuthenticationPage extends React.Component {
handleLogin(e: ChangeEvent<HTMLInputElement>): void;
static authClient: () => Promise;
}
export { PKCEAuthenticationPage, NetlifyAuthenticationPage };
}

View File

@@ -0,0 +1,33 @@
{
"name": "decap-cms-ui-auth",
"description": "UI auth library for Decap CMS",
"version": "3.2.3",
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-ui-auth",
"bugs": "https://github.com/decaporg/decap-cms/issues",
"module": "dist/esm/index.js",
"main": "dist/decap-cms-ui-auth.js",
"license": "MIT",
"keywords": [
"decap-cms",
"ui",
"auth"
],
"sideEffects": false,
"scripts": {
"develop": "npm run build:esm -- --watch",
"build": "cross-env NODE_ENV=production webpack",
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
},
"dependencies": {
"jwt-decode": "^3.0.0"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"decap-cms-lib-auth": "^3.0.0",
"decap-cms-ui-default": "^3.0.0",
"lodash": "^4.17.11",
"prop-types": "^15.7.2",
"react": "^18.2.0"
}
}

View File

@@ -0,0 +1,238 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from '@emotion/styled';
import partial from 'lodash/partial';
import {
AuthenticationPage,
buttons,
shadows,
colors,
colorsRaw,
lengths,
zIndex,
} from 'decap-cms-ui-default';
const LoginButton = styled.button`
${buttons.button};
${shadows.dropDeep};
${buttons.default};
${buttons.gray};
padding: 0 30px;
display: block;
margin-top: 20px;
margin-left: auto;
`;
const AuthForm = styled.form`
width: 350px;
margin-top: -80px;
`;
const AuthInput = styled.input`
background-color: ${colorsRaw.white};
border-radius: ${lengths.borderRadius};
font-size: 14px;
padding: 10px;
margin-bottom: 15px;
margin-top: 6px;
width: 100%;
position: relative;
z-index: ${zIndex.zIndex1};
&:focus {
outline: none;
box-shadow: inset 0 0 0 2px ${colors.active};
}
`;
const ErrorMessage = styled.p`
color: ${colors.errorText};
`;
let component = null;
if (window.netlifyIdentity) {
window.netlifyIdentity.on('login', user => {
component && component.handleIdentityLogin(user);
});
window.netlifyIdentity.on('logout', () => {
component && component.handleIdentityLogout();
});
window.netlifyIdentity.on('error', err => {
component && component.handleIdentityError(err);
});
}
export default class NetlifyAuthenticationPage extends React.Component {
static authClient;
static propTypes = {
onLogin: PropTypes.func.isRequired,
inProgress: PropTypes.bool.isRequired,
error: PropTypes.node,
config: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
component = this;
}
componentDidMount() {
// Manually validate PropTypes - React 19 breaking change
PropTypes.checkPropTypes(
NetlifyAuthenticationPage.propTypes,
this.props,
'prop',
'GitGatewayAuthenticationPage',
);
if (!this.loggedIn && window.netlifyIdentity && window.netlifyIdentity.currentUser()) {
this.props.onLogin(window.netlifyIdentity.currentUser());
window.netlifyIdentity.close();
}
}
componentWillUnmount() {
component = null;
}
handleIdentityLogin = user => {
this.props.onLogin(user);
window.netlifyIdentity.close();
};
handleIdentityLogout = () => {
window.netlifyIdentity.open();
};
handleIdentityError = err => {
if (err?.message?.match(/^Failed to load settings from.+\.netlify\/identity$/)) {
window.netlifyIdentity.close();
this.setState({
errors: { identity: this.props.t('auth.errors.identitySettings') },
});
}
};
handleIdentity = () => {
const user = window.netlifyIdentity.currentUser();
if (user) {
this.props.onLogin(user);
} else {
window.netlifyIdentity.open();
}
};
state = { email: '', password: '', errors: {} };
handleChange = (name, e) => {
this.setState({ ...this.state, [name]: e.target.value });
};
handleLogin = async e => {
e.preventDefault();
const { email, password } = this.state;
const { t } = this.props;
const errors = {};
if (!email) {
errors.email = t('auth.errors.email');
}
if (!password) {
errors.password = t('auth.errors.password');
}
if (Object.keys(errors).length > 0) {
this.setState({ errors });
return;
}
try {
const client = await NetlifyAuthenticationPage.authClient();
const user = await client.login(this.state.email, this.state.password, true);
this.props.onLogin(user);
} catch (error) {
this.setState({
errors: { server: error.description || error.msg || error },
loggingIn: false,
});
}
};
render() {
const { errors } = this.state;
const { error, inProgress, config, t } = this.props;
if (window.netlifyIdentity) {
if (errors.identity) {
return (
<AuthenticationPage
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
logo={config.logo}
siteUrl={config.site_url}
onLogin={this.handleIdentity}
renderPageContent={() => (
<a
href="https://docs.netlify.com/visitor-access/git-gateway/#setup-and-settings"
target="_blank"
rel="noopener noreferrer"
>
{errors.identity}
</a>
)}
t={t}
/>
);
} else {
return (
<AuthenticationPage
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
logo={config.logo}
siteUrl={config.site_url}
onLogin={this.handleIdentity}
renderButtonContent={() => t('auth.loginWithNetlifyIdentity')}
t={t}
/>
);
}
}
return (
<AuthenticationPage
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
logo={config.logo}
siteUrl={config.site_url}
renderPageContent={() => (
<AuthForm onSubmit={this.handleLogin}>
{!error ? null : <ErrorMessage>{error}</ErrorMessage>}
{!errors.server ? null : <ErrorMessage>{String(errors.server)}</ErrorMessage>}
<ErrorMessage>{errors.email || null}</ErrorMessage>
<AuthInput
type="text"
name="email"
placeholder="Email"
value={this.state.email}
onChange={partial(this.handleChange, 'email')}
/>
<ErrorMessage>{errors.password || null}</ErrorMessage>
<AuthInput
type="password"
name="password"
placeholder="Password"
value={this.state.password}
onChange={partial(this.handleChange, 'password')}
/>
<LoginButton disabled={inProgress}>
{inProgress ? t('auth.loggingIn') : t('auth.login')}
</LoginButton>
</AuthForm>
)}
t={t}
/>
);
}
}

View File

@@ -0,0 +1,153 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import jwtDecode from 'jwt-decode';
import { PkceAuthenticator } from 'decap-cms-lib-auth';
import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
const LoginButtonIcon = styled(Icon)`
margin-right: 18px;
`;
function normalizeClaimsToUser(
email_claim,
full_name_claim,
first_name_claim,
last_name_claim,
avatar_url_claim,
) {
return (user, claims) => {
if (!claims) return;
if (!user.email && claims[email_claim]) {
user.email = claims[email_claim];
}
if (!user.user_metadata.full_name && full_name_claim && claims[full_name_claim]) {
user.user_metadata.full_name = claims[full_name_claim];
}
if (!user.user_metadata.full_name && (first_name_claim || last_name_claim)) {
const name = [];
if (claims[first_name_claim]) name.push(claims[first_name_claim]);
if (claims[last_name_claim]) name.push(claims[last_name_claim]);
if (name.length) {
user.user_metadata.full_name = name.join(' ');
}
}
if (!user.user_metadata.avatar_url && avatar_url_claim && claims[avatar_url_claim]) {
user.user_metadata.avatar_url = claims[avatar_url_claim];
}
};
}
export default class PKCEAuthenticationPage extends React.Component {
static propTypes = {
inProgress: PropTypes.bool,
config: PropTypes.object.isRequired,
onLogin: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
state = {};
componentDidMount() {
// Old configuration options, available from the backend configuration
const {
base_url: backend_base_url = '',
app_id: backend_app_id = '',
auth_endpoint: backend_auth_endpoint = 'oauth2/authorize',
auth_token_endpoint: backend_auth_token_endpoint = 'oauth2/token',
} = this.props.config.backend;
// New configuration options, separately defined in the "auth" configuration
const {
use_oidc = false,
base_url = backend_base_url,
auth_endpoint = backend_auth_endpoint,
auth_token_endpoint = backend_auth_token_endpoint,
app_id = backend_app_id,
auth_token_endpoint_content_type = 'application/x-www-form-urlencoded; charset=utf-8',
email_claim = 'email',
full_name_claim,
first_name_claim,
last_name_claim,
avatar_url_claim,
} = this.props.config.auth || {};
const normalizeClaims = normalizeClaimsToUser(
email_claim,
full_name_claim,
first_name_claim,
last_name_claim,
avatar_url_claim,
);
this.auth = new PkceAuthenticator({
base_url,
app_id,
use_oidc,
auth_endpoint,
auth_token_endpoint,
auth_token_endpoint_content_type,
});
// Complete authentication if we were redirected back from the provider.
this.auth.completeAuth((err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
data.user_metadata = {};
if (data.access_token) {
data.token = data.access_token;
try {
data.claims = jwtDecode(data.access_token);
normalizeClaims(data, data.claims);
} catch {
/* Ignore */
}
}
if (data.id_token) {
try {
data.idClaims = jwtDecode(data.id_token);
normalizeClaims(data, data.idClaims);
} catch {
/* Ignore */
}
}
this.props.onLogin(data);
});
}
handleLogin = e => {
e.preventDefault();
const scope = this.props.config.auth?.scope || this.props.config.auth_scope || 'openid email';
this.auth.authenticate({ scope }, (err, data) => {
if (err) {
this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
});
};
render() {
const { inProgress, config, t } = this.props;
return (
<AuthenticationPage
onLogin={this.handleLogin}
loginDisabled={inProgress}
loginErrorMessage={this.state.loginError}
logoUrl={config.logo_url} // Deprecated, replaced by `logo.src`
logo={config.logo}
siteUrl={config.site_url}
renderButtonContent={() => (
<React.Fragment>
<LoginButtonIcon type="link" /> {inProgress ? t('auth.loggingIn') : t('auth.login')}
</React.Fragment>
)}
t={t}
/>
);
}
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { render } from '@testing-library/react';
import NetlifyAuthenticationPage from '../NetlifyAuthenticationPage';
window.netlifyIdentity = {
currentUser: jest.fn(),
on: jest.fn(),
close: jest.fn(),
};
describe('NetlifyAuthenticationPage', () => {
const props = {
config: { logo: { src: 'logo_url' } },
t: jest.fn(key => key),
onLogin: jest.fn(),
inProgress: false,
};
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
it('should render with identity error', () => {
// obtain mock calls
require('../NetlifyAuthenticationPage');
function TestComponent() {
const { asFragment } = render(<NetlifyAuthenticationPage {...props} />);
const errorCallback = window.netlifyIdentity.on.mock.calls.find(
call => call[0] === 'error',
)[1];
errorCallback(
new Error('Failed to load settings from https://site.netlify.com/.netlify/identity'),
);
expect(asFragment()).toMatchSnapshot();
}
TestComponent();
});
test('should render with no identity error', () => {
function TestComponent() {
const { asFragment } = render(<NetlifyAuthenticationPage {...props} />);
expect(asFragment()).toMatchSnapshot();
}
TestComponent();
});
});

View File

@@ -0,0 +1,281 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NetlifyAuthenticationPage should render with identity error 1`] = `
<DocumentFragment>
.emotion-0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex-flow: column nowrap;
-webkit-flex-flow: column nowrap;
-ms-flex-flow: column nowrap;
flex-flow: column nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
gap: 50px;
height: 100vh;
}
.emotion-2 {
width: 300px;
height: auto;
text-align: center;
margin-bottom: 1rem;
}
.emotion-4 {
border: 0;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15),0 1px 3px 0 rgba(68, 74, 87, 0.25);
height: 36px;
line-height: 36px;
font-weight: 500;
padding: 0 15px;
background-color: #798291;
color: #fff;
background-color: #313d3e;
color: #fff;
padding: 0 12px;
margin-top: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
}
.emotion-4:focus,
.emotion-4:hover {
color: #fff;
background-color: #555a65;
}
.emotion-4[disabled] {
background-color: #eff0f4;
color: #798291;
cursor: default;
}
.emotion-7 {
display: inline-block;
line-height: 0;
width: 100px;
height: 100px;
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
color: #c4c6d2;
position: absolute;
bottom: 10px;
}
.emotion-7 path:not(.no-fill),
.emotion-7 circle:not(.no-fill),
.emotion-7 polygon:not(.no-fill),
.emotion-7 rect:not(.no-fill) {
fill: currentColor;
}
.emotion-7 path.clipped {
fill: transparent;
}
.emotion-7 svg {
width: 100%;
height: 100%;
}
<section
class="emotion-0 emotion-1"
>
<span
class="emotion-2 emotion-3"
>
<img
alt="Logo"
src="logo_url"
/>
</span>
<button
class="emotion-4 emotion-5"
>
auth.loginWithNetlifyIdentity
</button>
<span
class="emotion-6 emotion-7 emotion-8"
>
<svg
fill="none"
height="90"
viewBox="0 0 335 90"
width="335"
xmlns="http://www.w3.org/2000/svg"
>
<path
class="no-fill"
d="M30.73.15 0 2.95l3.67 40.21 20.03-1.83-1.99-21.86 10.71-.98c10.61-.97 19.14 7.53 20.29 20.19l19.67-1.79C70.34 14.72 51.64-1.75 30.73.15ZM73.61 49.51c0 12.72-7.73 21.95-18.37 21.95H44.49V49.47H24.38v40.42h30.86c21.02 0 38.12-18.11 38.12-40.38v-.04H73.61v.04Z"
fill="#FF0082"
/>
<path
class="no-fill"
d="M131.65 23.71h20.01c14.41 0 24.29 9.09 24.29 23.06 0 13.97-9.88 23.06-24.29 23.06h-20.01V23.71Zm19.51 37.35c8.75 0 14.47-5.47 14.47-14.29s-5.73-14.29-14.47-14.29h-9.31v28.59h9.31v-.01ZM207.61 58.69l5.22 5.93c-3.15 3.75-7.87 5.73-13.97 5.73-11.7 0-19.32-7.71-19.32-18.25s7.68-18.25 18.12-18.25c9.56 0 17.43 6.59 17.49 17.92l-25.04 5.07c1.45 3.49 4.59 5.27 9 5.27 3.59 0 6.17-1.12 8.5-3.43v.01Zm-18.44-7.64 16.49-3.36c-.94-3.62-3.9-6.06-7.99-6.06-4.91 0-8.31 3.43-8.5 9.42ZM218.25 52.1c0-10.67 7.87-18.25 18.88-18.25 7.11 0 12.71 3.23 15.17 9.02l-7.61 4.28c-1.83-3.36-4.53-4.87-7.61-4.87-4.97 0-8.87 3.62-8.87 9.81s3.9 9.81 8.87 9.81c3.08 0 5.79-1.45 7.61-4.87l7.61 4.35c-2.45 5.67-8.05 8.96-15.17 8.96-11.01 0-18.88-7.58-18.88-18.25v.01ZM290.93 34.38v35.44h-9.38v-4.08c-2.45 3.1-6.04 4.61-10.57 4.61-9.57 0-16.93-7.11-16.93-18.25s7.36-18.25 16.93-18.25c4.15 0 7.68 1.38 10.13 4.28v-3.75h9.82ZM281.3 52.1c0-6.13-3.78-9.81-8.62-9.81S264 45.98 264 52.1c0 6.12 3.78 9.81 8.68 9.81s8.62-3.69 8.62-9.81ZM334.54 52.1c0 11.13-7.36 18.25-16.86 18.25-4.22 0-7.68-1.38-10.19-4.28V82.6h-9.82V34.38h9.38v4.08c2.45-3.1 6.1-4.61 10.63-4.61 9.5 0 16.86 7.11 16.86 18.25Zm-9.94 0c0-6.13-3.71-9.81-8.62-9.81-4.91 0-8.62 3.69-8.62 9.81 0 6.12 3.71 9.81 8.62 9.81 4.91 0 8.62-3.69 8.62-9.81Z"
fill="#000"
/>
</svg>
</span>
</section>
</DocumentFragment>
`;
exports[`NetlifyAuthenticationPage should render with no identity error 1`] = `
<DocumentFragment>
.emotion-0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex-flow: column nowrap;
-webkit-flex-flow: column nowrap;
-ms-flex-flow: column nowrap;
flex-flow: column nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
gap: 50px;
height: 100vh;
}
.emotion-2 {
width: 300px;
height: auto;
text-align: center;
margin-bottom: 1rem;
}
.emotion-4 {
border: 0;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15),0 1px 3px 0 rgba(68, 74, 87, 0.25);
height: 36px;
line-height: 36px;
font-weight: 500;
padding: 0 15px;
background-color: #798291;
color: #fff;
background-color: #313d3e;
color: #fff;
padding: 0 12px;
margin-top: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
}
.emotion-4:focus,
.emotion-4:hover {
color: #fff;
background-color: #555a65;
}
.emotion-4[disabled] {
background-color: #eff0f4;
color: #798291;
cursor: default;
}
.emotion-7 {
display: inline-block;
line-height: 0;
width: 100px;
height: 100px;
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
color: #c4c6d2;
position: absolute;
bottom: 10px;
}
.emotion-7 path:not(.no-fill),
.emotion-7 circle:not(.no-fill),
.emotion-7 polygon:not(.no-fill),
.emotion-7 rect:not(.no-fill) {
fill: currentColor;
}
.emotion-7 path.clipped {
fill: transparent;
}
.emotion-7 svg {
width: 100%;
height: 100%;
}
<section
class="emotion-0 emotion-1"
>
<span
class="emotion-2 emotion-3"
>
<img
alt="Logo"
src="logo_url"
/>
</span>
<button
class="emotion-4 emotion-5"
>
auth.loginWithNetlifyIdentity
</button>
<span
class="emotion-6 emotion-7 emotion-8"
>
<svg
fill="none"
height="90"
viewBox="0 0 335 90"
width="335"
xmlns="http://www.w3.org/2000/svg"
>
<path
class="no-fill"
d="M30.73.15 0 2.95l3.67 40.21 20.03-1.83-1.99-21.86 10.71-.98c10.61-.97 19.14 7.53 20.29 20.19l19.67-1.79C70.34 14.72 51.64-1.75 30.73.15ZM73.61 49.51c0 12.72-7.73 21.95-18.37 21.95H44.49V49.47H24.38v40.42h30.86c21.02 0 38.12-18.11 38.12-40.38v-.04H73.61v.04Z"
fill="#FF0082"
/>
<path
class="no-fill"
d="M131.65 23.71h20.01c14.41 0 24.29 9.09 24.29 23.06 0 13.97-9.88 23.06-24.29 23.06h-20.01V23.71Zm19.51 37.35c8.75 0 14.47-5.47 14.47-14.29s-5.73-14.29-14.47-14.29h-9.31v28.59h9.31v-.01ZM207.61 58.69l5.22 5.93c-3.15 3.75-7.87 5.73-13.97 5.73-11.7 0-19.32-7.71-19.32-18.25s7.68-18.25 18.12-18.25c9.56 0 17.43 6.59 17.49 17.92l-25.04 5.07c1.45 3.49 4.59 5.27 9 5.27 3.59 0 6.17-1.12 8.5-3.43v.01Zm-18.44-7.64 16.49-3.36c-.94-3.62-3.9-6.06-7.99-6.06-4.91 0-8.31 3.43-8.5 9.42ZM218.25 52.1c0-10.67 7.87-18.25 18.88-18.25 7.11 0 12.71 3.23 15.17 9.02l-7.61 4.28c-1.83-3.36-4.53-4.87-7.61-4.87-4.97 0-8.87 3.62-8.87 9.81s3.9 9.81 8.87 9.81c3.08 0 5.79-1.45 7.61-4.87l7.61 4.35c-2.45 5.67-8.05 8.96-15.17 8.96-11.01 0-18.88-7.58-18.88-18.25v.01ZM290.93 34.38v35.44h-9.38v-4.08c-2.45 3.1-6.04 4.61-10.57 4.61-9.57 0-16.93-7.11-16.93-18.25s7.36-18.25 16.93-18.25c4.15 0 7.68 1.38 10.13 4.28v-3.75h9.82ZM281.3 52.1c0-6.13-3.78-9.81-8.62-9.81S264 45.98 264 52.1c0 6.12 3.78 9.81 8.68 9.81s8.62-3.69 8.62-9.81ZM334.54 52.1c0 11.13-7.36 18.25-16.86 18.25-4.22 0-7.68-1.38-10.19-4.28V82.6h-9.82V34.38h9.38v4.08c2.45-3.1 6.1-4.61 10.63-4.61 9.5 0 16.86 7.11 16.86 18.25Zm-9.94 0c0-6.13-3.71-9.81-8.62-9.81-4.91 0-8.62 3.69-8.62 9.81 0 6.12 3.71 9.81 8.62 9.81 4.91 0 8.62-3.69 8.62-9.81Z"
fill="#000"
/>
</svg>
</span>
</section>
</DocumentFragment>
`;

View File

@@ -0,0 +1,8 @@
import NetlifyAuthenticationPage from './NetlifyAuthenticationPage';
import PKCEAuthenticationPage from './PKCEAuthenticationPage';
export const DecapCmsUiAuth = {
NetlifyAuthenticationPage,
PKCEAuthenticationPage,
};
export { NetlifyAuthenticationPage, PKCEAuthenticationPage };

View File

@@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');
module.exports = getConfig();