Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: page password #301

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
{
"label": "Next Dev",
"type": "shell",
"command": "cd website && yarn dev"
"command": ". ~/.nvm/nvm.sh && nvm use 18 && cd website && yarn dev"
},
{
"label": "Next Build",
"type": "shell",
"command": "cd website && yarn build"
"command": ". ~/.nvm/nvm.sh && nvm use 18 && cd website && yarn build"
},
{
"label": "Next Yarn Install",
"type": "shell",
"command": "cd website && yarn install"
"command": ". ~/.nvm/nvm.sh && nvm use 18 && cd website && yarn install"
},
{
"label": "Wordpress ACF Fix",
Expand Down
2 changes: 2 additions & 0 deletions website/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ module.exports = withBundleAnalyzer({
'localhost',
'127.0.0.1',
'bubsnext.wpengine.com',
'bubsnexts.wpengine.com',
'bubs.patronage.org',
'wordpress.bubsnext.orb.local',
],
},

Expand Down
98 changes: 98 additions & 0 deletions website/src/components/PagePassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import cx from 'classnames';
import { getContent } from 'lib/wordpress';
import { useRouter } from 'next/router';
import { useState } from 'react';
// import styles from './PagePassword.module.scss';

const getPasswordPost = async ({ router, password }) => {
if (password) {
const slug = `/${router.query.slug.join('/')}`;
const data = await getContent({
slug,
preview: false,
options: {
password: password,
},
});

if (data) {
return data.contentNode;
}
}

return null;
};

export default function PagePassword({ setPost, badPassword }) {
const router = useRouter();
const [loading, setLoading] = useState(false);

async function handlePasswordSubmit(e) {
e.preventDefault();
setLoading(true);
const password = e.target.post_password.value;
const content = await getPasswordPost({ router, password });
if (content) {
setPost(content);
}
setLoading(false);
}

return (
<div
// className={styles.singlePassword}
>
<section className="section-padded">
<div className="container">
<form method="post" onSubmit={handlePasswordSubmit}>
<div className="row align-items-center g-3">
<div className="col-auto">
<label htmlFor="pwbox" className="visually-hidden">
Password:
</label>
<input
className={cx(
// styles.fwdFormControl,
'form-control',
)}
name="post_password"
id="pwbox"
type="password"
placeholder="Password"
size="20"
maxLength="20"
/>
</div>
<div className="col-auto">
<button
className={cx(
// styles.fwdFormControl,
// styles.btn,
'form-control btn btn-primary',
)}
type="submit"
name="Submit"
disabled={loading}
>
{loading ? 'Loading' : 'Submit'}
</button>
</div>
</div>
</form>
<div className="row">
<div className="col-auto">
{badPassword && !loading && (
<div
// className={styles.badPassword}
>
Incorrect password
</div>
)}
</div>
</div>
</div>
</section>
<section className="section-padded"></section>
</div>
);
}
13 changes: 13 additions & 0 deletions website/src/lib/resettingState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useState, useEffect } from 'react';

// https://nextjs.org/docs/pages/api-reference/functions/use-router#resetting-state-after-navigation

export function useResettingState(prop) {
const [state, setState] = useState(prop);

useEffect(() => {
setState(prop);
}, [prop]);

return [state, setState];
}
1 change: 1 addition & 0 deletions website/src/lib/wordpress/graphql/queryContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function queryContent(draft, options) {
id
databaseId
isPreview
isRestricted
link
slug
uri
Expand Down
9 changes: 9 additions & 0 deletions website/src/lib/wordpress/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ async function fetchAPI({
query = {},
variables = {},
token,
password,
}) {
const SETTINGS = getSettings({ project });
let wordpress_api_url =
Expand All @@ -25,6 +26,13 @@ async function fetchAPI({
headers['x-preview-token'] = '1';
}

if (password) {
headers['x-wp-post-password'] = password;
// for password requests, hit origin and bypass CDN caching
// https://docs.graphcdn.io/docs/bypass-headers
headers['x-preview-token'] = '1';
}

// console.log('API', WORDPRESS_API_URL);
// console.log('-------');
// console.log('variables', variables);
Expand Down Expand Up @@ -186,6 +194,7 @@ export async function getContent({
query,
variables: { slug, preview: !!preview },
token: previewData?.token,
password: options?.password,
});

return data;
Expand Down
32 changes: 30 additions & 2 deletions website/src/pages/[[...slug]].js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Flex from 'components/flex/Flex';
import LayoutDefault from 'components/layouts/LayoutDefault';
import PagePassword from 'components/PagePassword';
import PostBody from 'components/PostBody';
import { GlobalsProvider } from 'contexts/GlobalsContext';
import checkRedirects from 'lib/checkRedirects';
import { getSettings } from 'lib/getSettings';
import { useResettingState } from 'lib/resettingState';
import { isStaticFile } from 'lib/utils';
import {
getContent,
Expand All @@ -12,10 +14,37 @@ import {
getAllContentWithSlug,
} from 'lib/wordpress';

export default function Page({ post, preview, globals }) {
export default function Page(props) {
const initialPost = props.post;
const [post, setPost] = useResettingState(initialPost);

const preview = props.preview;
const globals = {
...props.globals,
pageOptions: post?.acfPageOptions || null,
};
const flexSections = post?.template?.acfFlex?.flexContent || null;
const template = post?.template?.templateName || null;

if (post?.isRestricted) {
return (
<GlobalsProvider globals={globals}>
<LayoutDefault
postId={post?.databaseId}
isRevision={post?.isPreview}
seo={post?.seo}
preview={preview}
title={post?.title}
>
<PagePassword
setPost={setPost}
badPassword={post?.badPassword}
/>
</LayoutDefault>
</GlobalsProvider>
);
}

if (template === 'Flex') {
return (
<GlobalsProvider globals={globals}>
Expand Down Expand Up @@ -126,7 +155,6 @@ export async function getStaticProps({
props: {
globals: {
...globals,
pageOptions: data.contentNode?.acfPageOptions || null,
THEME: SETTINGS.THEME,
CONFIG: SETTINGS.CONFIG,
},
Expand Down
4 changes: 3 additions & 1 deletion wordpress/wp-content/themes/headless/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

// Customize these variables per site
$staging_wp_host = 'bubsnexts.wpengine.com';
$local_domain = 'http://localhost:3000';
$dashboard_cleanup = false; // Optionally will hide all but our custom widget
$docs_link = ''; // set to a path if you have a site/document for editor instructions

Expand All @@ -23,7 +24,7 @@
// Determine the hosting environment we're in
if (defined('WP_ENV') && WP_ENV == 'development') {
define('WP_HOST', 'localhost');
$headless_domain = $local_domain || 'http://localhost:3000';
$headless_domain = $local_domain;
} else {
$headless_domain = rtrim(get_theme_mod('headless_preview_url'), '/');

Expand Down Expand Up @@ -69,6 +70,7 @@ function bubs_theme_options($wp_customize) {
include_once 'setup/helpers/role-super-editor.php';
include_once 'setup/helpers/webhooks.php';
include_once 'setup/helpers/wpgraphql.php';
include_once 'setup/helpers/wpgraphql-page-password.php';
include_once 'setup/helpers/wysiwyg.php';

// Security Settings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

$restricted_fields = [];
$bad_password = false;

/****
* We need to allow these headers in the request to graphql so we can
* check the password that we set in header "x-wp-post-password" and
* bypass graphcdn with the "x-preview-token" header"
****/
add_filter('graphql_access_control_allow_headers', function ($headers) {
return array_merge($headers, ['x-wp-post-password', 'x-preview-token']);
});

/****
* We check if the password is set in the request headers as well as the post's visibility
* being 'restricted' (password protected) and that the data type is a PostObject.
* We check the submitted password against the post's password and if it matches, we
* set the visibility to public. If the password does not match we set the global variable
* $bad_password to true, which is used to send the bad_password field back in
* graphql_request_results.
****/
add_filter(
'graphql_object_visibility',
function ($visibility, $model_name, $data) {
if (
$visibility === 'restricted' &&
$model_name === 'PostObject' &&
isset($_SERVER['HTTP_X_WP_POST_PASSWORD'])
) {
$client_password = $_SERVER['HTTP_X_WP_POST_PASSWORD'];

if ($client_password === $data->post_password) {
$visibility = 'public';
} else {
global $bad_password;
$bad_password = true;
}
}

return $visibility;
},
10,
3,
);

/****
* If the post visibility is set to 'restricted' we get the restricted fields that Wordpress
* allows to be displayed and set those to a global variable $restricted_fields for use in
* graphql_request_results.
****/
add_filter(
'graphql_allowed_fields_on_restricted_type',
function ($fields, $model_name, $data, $visibility, $owner, $current_user) {
if ($model_name === 'PostObject') {
if ($visibility === 'restricted') {
global $restricted_fields;
$restricted_fields = $fields;
} else {
global $restricted_fields;
$restricted_fields = [];
}
}

return $fields;
},
10,
6,
);

/****
* If restricted fields is an empty array (which we set as default
* at the top of this file) then we immediately return the $reponse
* and don't touch the data. If restricted fields is not empty, we
* check if the $response contains data['contentNode'] and if so, we
* loop through the fields and remove any that are not in the $restricted_fields.
* We also set seo fields so the page will have noindex and nofollow and we set the
* $bad_password field so if a bad password was entered we can display an error.
****/
add_filter(
'graphql_request_results',
function ($response) {
global $restricted_fields;
if ($restricted_fields === []) {
return $response;
}
if (is_object($response) && isset($response->data['contentNode'])) {
foreach ($response->data['contentNode'] as $key => $node) {
if (!in_array($key, $restricted_fields) && $key !== '__typename') {
unset($response->data['contentNode'][$key]);
}
}

$response->data['contentNode']['seo']['metaRobotsNoindex'] = 'noindex';
$response->data['contentNode']['seo']['metaRobotsNofollow'] = 'nofollow';
global $bad_password;
$response->data['contentNode']['badPassword'] = $bad_password;
}
return $response;
},
10,
1,
);