Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - streaming/index.jsmain
commit
652147a3f4
|
@ -163,21 +163,28 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||||
|
|
||||||
return columnIndex !== -1 ? [
|
const content = columnIndex !== -1 ? (
|
||||||
<TabsBar key='tabs' />,
|
|
||||||
|
|
||||||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||||
{links.map(this.renderView)}
|
{links.map(this.renderView)}
|
||||||
</ReactSwipeableViews>,
|
</ReactSwipeableViews>
|
||||||
|
) : (
|
||||||
|
<div key='content' className='columns-area columns-area--mobile'>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
floatingActionButton,
|
return (
|
||||||
] : [
|
<div className='columns-area__panels'>
|
||||||
<TabsBar key='tabs' />,
|
<div className='columns-area__panels__pane' />
|
||||||
|
|
||||||
<div key='content' className='columns-area columns-area--mobile'>{children}</div>,
|
<div className='columns-area__panels__main'>
|
||||||
|
<TabsBar key='tabs' />
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
|
||||||
floatingActionButton,
|
<div className='columns-area__panels__pane' />
|
||||||
];
|
|
||||||
|
{floatingActionButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -71,11 +71,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
||||||
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||||
const params = [ `stream=${stream}` ];
|
const params = [ `stream=${stream}` ];
|
||||||
|
|
||||||
if (accessToken !== null) {
|
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
||||||
params.push(`access_token=${accessToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
|
|
||||||
|
|
||||||
ws.onopen = connected;
|
ws.onopen = connected;
|
||||||
ws.onmessage = e => received(JSON.parse(e.data));
|
ws.onmessage = e => received(JSON.parse(e.data));
|
||||||
|
|
|
@ -1786,6 +1786,39 @@ a.account__display-name {
|
||||||
&.unscrollable {
|
&.unscrollable {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__panels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__pane {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media screen and (min-width: 360px) {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-swipeable-view-container {
|
.react-swipeable-view-container {
|
||||||
|
@ -1936,7 +1969,6 @@ a.account__display-name {
|
||||||
.columns-area--mobile {
|
.columns-area--mobile {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
.column,
|
.column,
|
||||||
|
@ -1952,7 +1984,7 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 360px) {
|
@media screen and (min-width: 360px) {
|
||||||
padding: 10px;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 630px) {
|
@media screen and (min-width: 630px) {
|
||||||
|
@ -2013,8 +2045,7 @@ a.account__display-name {
|
||||||
.tabs-bar {
|
.tabs-bar {
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
width: calc(100% - 20px);
|
width: 100%;
|
||||||
max-width: 600px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-swipeable-view-container .columns-area--mobile {
|
.react-swipeable-view-container .columns-area--mobile {
|
||||||
|
@ -5427,6 +5458,10 @@ noscript {
|
||||||
&:active {
|
&:active {
|
||||||
background: lighten($ui-highlight-color, 7%);
|
background: lighten($ui-highlight-color, 7%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 630px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__header__content {
|
.account__header__content {
|
||||||
|
|
|
@ -195,14 +195,14 @@ const startWorker = (workerId) => {
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
const accountFromToken = (token, req, next) => {
|
const accountFromToken = (token, allowedScopes, req, next) => {
|
||||||
pgPool.connect((err, client, done) => {
|
pgPool.connect((err, client, done) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||||
done();
|
done();
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -218,18 +218,29 @@ const startWorker = (workerId) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scopes = result.rows[0].scopes.split(' ');
|
||||||
|
|
||||||
|
if (allowedScopes.size > 0 && !scopes.some(scope => allowedScopes.includes(scope))) {
|
||||||
|
err = new Error('Access token does not cover required scopes');
|
||||||
|
err.statusCode = 401;
|
||||||
|
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
req.accountId = result.rows[0].account_id;
|
req.accountId = result.rows[0].account_id;
|
||||||
req.chosenLanguages = result.rows[0].chosen_languages;
|
req.chosenLanguages = result.rows[0].chosen_languages;
|
||||||
|
req.allowNotifications = scopes.some(scope => ['read', 'read:notifications'].includes(scope));
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const accountFromRequest = (req, next, required = true) => {
|
const accountFromRequest = (req, next, required = true, allowedScopes = ['read']) => {
|
||||||
const authorization = req.headers.authorization;
|
const authorization = req.headers.authorization;
|
||||||
const location = url.parse(req.url, true);
|
const location = url.parse(req.url, true);
|
||||||
const accessToken = location.query.access_token;
|
const accessToken = location.query.access_token || req.headers['sec-websocket-protocol'];
|
||||||
|
|
||||||
if (!authorization && !accessToken) {
|
if (!authorization && !accessToken) {
|
||||||
if (required) {
|
if (required) {
|
||||||
|
@ -246,7 +257,7 @@ const startWorker = (workerId) => {
|
||||||
|
|
||||||
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
|
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
|
||||||
|
|
||||||
accountFromToken(token, req, next);
|
accountFromToken(token, allowedScopes, req, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUBLIC_STREAMS = [
|
const PUBLIC_STREAMS = [
|
||||||
|
@ -261,6 +272,16 @@ const startWorker = (workerId) => {
|
||||||
const wsVerifyClient = (info, cb) => {
|
const wsVerifyClient = (info, cb) => {
|
||||||
const location = url.parse(info.req.url, true);
|
const location = url.parse(info.req.url, true);
|
||||||
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
|
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
|
||||||
|
const allowedScopes = [];
|
||||||
|
|
||||||
|
if (authRequired) {
|
||||||
|
allowedScopes.push('read');
|
||||||
|
if (location.query.stream === 'user:notification') {
|
||||||
|
allowedScopes.push('read:notifications');
|
||||||
|
} else {
|
||||||
|
allowedScopes.push('read:statuses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
accountFromRequest(info.req, err => {
|
accountFromRequest(info.req, err => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
@ -269,7 +290,7 @@ const startWorker = (workerId) => {
|
||||||
log.error(info.req.requestId, err.toString());
|
log.error(info.req.requestId, err.toString());
|
||||||
cb(false, 401, 'Unauthorized');
|
cb(false, 401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
}, authRequired);
|
}, authRequired, allowedScopes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
|
@ -286,7 +307,18 @@ const startWorker = (workerId) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
|
const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
|
||||||
accountFromRequest(req, next, authRequired);
|
const allowedScopes = [];
|
||||||
|
|
||||||
|
if (authRequired) {
|
||||||
|
allowedScopes.push('read');
|
||||||
|
if (req.path === '/api/v1/streaming/user/notification') {
|
||||||
|
allowedScopes.push('read:notifications');
|
||||||
|
} else {
|
||||||
|
allowedScopes.push('read:statuses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountFromRequest(req, next, authRequired, allowedScopes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMiddleware = (err, req, res, {}) => {
|
const errorMiddleware = (err, req, res, {}) => {
|
||||||
|
@ -339,6 +371,10 @@ const startWorker = (workerId) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event === 'notification' && !req.allowNotifications) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only send local-only statuses to logged-in users
|
// Only send local-only statuses to logged-in users
|
||||||
if (payload.local_only && !req.accountId) {
|
if (payload.local_only && !req.accountId) {
|
||||||
log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`);
|
log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`);
|
||||||
|
|
Loading…
Reference in New Issue