mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added basic member authentication
refs https://github.com/TryGhost/Team/issues/1664 - Added basic API handling - Added member authentication - Basic avatar with form - Setup sentry
This commit is contained in:
parent
0eeff657de
commit
710c265601
6 changed files with 269 additions and 7 deletions
|
@ -1,10 +1,12 @@
|
|||
import Form from './components/Form';
|
||||
import CustomIFrame from './components/CustomIFrame';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import React from 'react';
|
||||
import ActionHandler from './actions';
|
||||
import {createPopupNotification} from './utils/helpers';
|
||||
import {createPopupNotification,isSentryEventAllowed} from './utils/helpers';
|
||||
import AppContext from './AppContext';
|
||||
import {hasMode} from './utils/check-mode';
|
||||
import setupGhostApi from './utils/api';
|
||||
import CommentsBox from './components/CommentsBox';
|
||||
|
||||
function SentryErrorBoundary({dsn, children}) {
|
||||
if (dsn) {
|
||||
|
@ -28,11 +30,41 @@ export default class App extends React.Component {
|
|||
// Todo: this state is work in progress
|
||||
this.state = {
|
||||
action: 'init:running',
|
||||
initStatus: 'running',
|
||||
member: null,
|
||||
popupNotification: null,
|
||||
customSiteUrl: props.customSiteUrl
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initSetup();
|
||||
}
|
||||
|
||||
/** Initialize comments setup on load, fetch data and setup state*/
|
||||
async initSetup() {
|
||||
try {
|
||||
// Fetch data from API, links, preview, dev sources
|
||||
const {site, member} = await this.fetchApiData();
|
||||
const state = {
|
||||
site,
|
||||
member,
|
||||
action: 'init:success',
|
||||
initStatus: 'success'
|
||||
};
|
||||
|
||||
this.setState(state);
|
||||
} catch (e) {
|
||||
/* eslint-disable no-console */
|
||||
console.error(`[Comments] Failed to initialize:`, e);
|
||||
/* eslint-enable no-console */
|
||||
this.setState({
|
||||
action: 'init:failed',
|
||||
initStatus: 'failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle actions from across App and update App state */
|
||||
async dispatchAction(action, data) {
|
||||
clearTimeout(this.timeoutId);
|
||||
|
@ -66,13 +98,59 @@ export default class App extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
/** Fetch site and member session data with Ghost Apis */
|
||||
async fetchApiData() {
|
||||
const {siteUrl, customSiteUrl, apiUrl, apiKey} = this.props;
|
||||
|
||||
try {
|
||||
this.GhostApi = this.props.api || setupGhostApi({siteUrl, apiUrl, apiKey});
|
||||
const {site, member} = await this.GhostApi.init();
|
||||
|
||||
this.setupSentry({site});
|
||||
return {site, member};
|
||||
} catch (e) {
|
||||
if (hasMode(['dev', 'test'], {customSiteUrl})) {
|
||||
return {};
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Setup Sentry */
|
||||
setupSentry({site}) {
|
||||
if (hasMode(['test'])) {
|
||||
return null;
|
||||
}
|
||||
const {portal_sentry: portalSentry, portal_version: portalVersion, version: ghostVersion} = site;
|
||||
const appVersion = process.env.REACT_APP_VERSION || portalVersion;
|
||||
const releaseTag = `comments@${appVersion}|ghost@${ghostVersion}`;
|
||||
if (portalSentry && portalSentry.dsn) {
|
||||
Sentry.init({
|
||||
dsn: portalSentry.dsn,
|
||||
environment: portalSentry.env || 'development',
|
||||
release: releaseTag,
|
||||
beforeSend: (event) => {
|
||||
if (isSentryEventAllowed({event})) {
|
||||
return event;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
allowUrls: [
|
||||
/https?:\/\/((www)\.)?unpkg\.com\/@tryghost\/comments/
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**Get final App level context from App state*/
|
||||
getContextFromState() {
|
||||
const {action, popupNotification, customSiteUrl} = this.state;
|
||||
const {action, popupNotification, customSiteUrl, member} = this.state;
|
||||
return {
|
||||
action,
|
||||
popupNotification,
|
||||
customSiteUrl,
|
||||
member,
|
||||
onAction: (_action, data) => this.dispatchAction(_action, data)
|
||||
};
|
||||
}
|
||||
|
@ -83,6 +161,10 @@ export default class App extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (this.state.initStatus !== 'success') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iFrameStyles = {
|
||||
border: 'none',
|
||||
width: '100%'
|
||||
|
@ -92,7 +174,7 @@ export default class App extends React.Component {
|
|||
<SentryErrorBoundary dsn={this.props.sentryDsn}>
|
||||
<AppContext.Provider value={this.getContextFromState()}>
|
||||
<CustomIFrame style={iFrameStyles}>
|
||||
<Form />
|
||||
<CommentsBox />
|
||||
</CustomIFrame>
|
||||
</AppContext.Provider>
|
||||
</SentryErrorBoundary>
|
||||
|
|
23
apps/comments-ui/src/components/CommentsBox.js
Normal file
23
apps/comments-ui/src/components/CommentsBox.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import AppContext from '../AppContext';
|
||||
import NotSignedInBox from './NotSignedInBox';
|
||||
import Form from './Form';
|
||||
|
||||
class CommentsBox extends React.Component {
|
||||
static contextType = AppContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section>
|
||||
{ this.context.member ? <Form /> : <NotSignedInBox /> }
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentsBox;
|
|
@ -38,12 +38,44 @@ class Form extends React.Component {
|
|||
this.setState({message: event.target.value});
|
||||
}
|
||||
|
||||
getInitials() {
|
||||
if (!this.context.member) {
|
||||
return '';
|
||||
}
|
||||
const parts = this.context.member.name.split(' ');
|
||||
|
||||
if (parts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
return parts[0].substring(0, 1);
|
||||
}
|
||||
|
||||
return parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.submitForm} className="comment-form">
|
||||
<figure className="avatar">
|
||||
<span />
|
||||
</figure>
|
||||
<div style={{display: 'flex', flexDirection: 'row'}}>
|
||||
<figure style={{position: 'relative', marginRight: '15px'}}>
|
||||
<div style={{backgroundColor: 'black', width: '60px', height: '60px', borderRadius: '30px', color: 'white', textAlign: 'center', fontSize: '20px', fontWeight: 'bold', lineHeight: '60px'}}>
|
||||
{ this.getInitials() }
|
||||
</div>
|
||||
{ this.context.member ? <img src={this.context.member.avatar_image} width="60" height="60" style={{position: 'absolute', left: '0', top: '0'}} alt="Avatar"/> : '' }
|
||||
</figure>
|
||||
<div>
|
||||
<div style={{fontWeight: 'bold'}}>
|
||||
{this.context.member ? this.context.member.name : ''}
|
||||
</div>
|
||||
<span>
|
||||
Add a bio
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<textarea className="w-full rounded-md border p-2" value={this.state.message} onChange={this.handleChange} placeholder="What are your thoughts?" />
|
||||
<button type="submit" className="bg-black p-2 text-white rounded w-full mt-2">Comment</button>
|
||||
</form>
|
||||
|
|
10
apps/comments-ui/src/components/NotSignedInBox.js
Normal file
10
apps/comments-ui/src/components/NotSignedInBox.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
function NotSignedInBox() {
|
||||
return (
|
||||
<section>
|
||||
<h1>You are not signed in</h1>
|
||||
<p>Log in to place a comment.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotSignedInBox;
|
100
apps/comments-ui/src/utils/api.js
Normal file
100
apps/comments-ui/src/utils/api.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import {transformApiSiteData} from './helpers';
|
||||
|
||||
function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
||||
const apiPath = 'members/api';
|
||||
|
||||
function endpointFor({type, resource}) {
|
||||
if (type === 'members') {
|
||||
return `${siteUrl.replace(/\/$/, '')}/${apiPath}/${resource}/`;
|
||||
}
|
||||
}
|
||||
|
||||
function contentEndpointFor({resource, params = ''}) {
|
||||
if (apiUrl && apiKey) {
|
||||
return `${apiUrl.replace(/\/$/, '')}/${resource}/?key=${apiKey}&limit=all${params}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function makeRequest({url, method = 'GET', headers = {}, credentials = undefined, body = undefined}) {
|
||||
const options = {
|
||||
method,
|
||||
headers,
|
||||
credentials,
|
||||
body
|
||||
};
|
||||
return fetch(url, options);
|
||||
}
|
||||
const api = {};
|
||||
|
||||
api.site = {
|
||||
settings() {
|
||||
const url = contentEndpointFor({resource: 'settings'});
|
||||
return makeRequest({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(function (res) {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
throw new Error('Failed to fetch site data');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
api.member = {
|
||||
identity() {
|
||||
const url = endpointFor({type: 'members', resource: 'session'});
|
||||
return makeRequest({
|
||||
url,
|
||||
credentials: 'same-origin'
|
||||
}).then(function (res) {
|
||||
if (!res.ok || res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
return res.text();
|
||||
});
|
||||
},
|
||||
|
||||
sessionData() {
|
||||
const url = endpointFor({type: 'members', resource: 'member'});
|
||||
return makeRequest({
|
||||
url,
|
||||
credentials: 'same-origin'
|
||||
}).then(function (res) {
|
||||
if (!res.ok || res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
api.init = async () => {
|
||||
let [member] = await Promise.all([
|
||||
api.member.sessionData()
|
||||
]);
|
||||
let site = {};
|
||||
let settings = {};
|
||||
|
||||
try {
|
||||
settings = await api.site.settings();
|
||||
site = {
|
||||
...settings
|
||||
};
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
site = transformApiSiteData({site});
|
||||
return {site, member};
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
export default setupGhostApi;
|
|
@ -14,3 +14,18 @@ export const createPopupNotification = ({type, status, autoHide, duration = 2600
|
|||
count
|
||||
};
|
||||
};
|
||||
|
||||
export function transformApiSiteData({site}) {
|
||||
if (!site) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return site;
|
||||
}
|
||||
|
||||
export function isSentryEventAllowed({event: sentryEvent}) {
|
||||
const frames = sentryEvent?.exception?.values?.[0]?.stacktrace?.frames || [];
|
||||
const fileNames = frames.map(frame => frame.filename).filter(filename => !!filename);
|
||||
const lastFileName = fileNames[fileNames.length - 1] || '';
|
||||
return lastFileName.includes('@tryghost/comments');
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue