mirror of
https://github.com/flarum/core.git
synced 2025-08-02 22:47:33 +02:00
Merge branch 'master' into compact-posts
This commit is contained in:
44
CHANGELOG.md
Normal file
44
CHANGELOG.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Change Log
|
||||||
|
All notable changes to Flarum and its bundled extensions will be documented in this file.
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased][unreleased]
|
||||||
|
*nothing yet*
|
||||||
|
|
||||||
|
## [0.1.0-beta.2] - 2015-09-15
|
||||||
|
### Added
|
||||||
|
- Check prerequisites (PHP version, extensions, etc.) before installation (#364)
|
||||||
|
- Enforce maximum title and post length through validation (#53, #338)
|
||||||
|
- Ctrl+Enter submits posts (#276)
|
||||||
|
- Syntax highlighting for code blocks (#248)
|
||||||
|
- All links open in new window, receive rel=nofollow attribute (#247)
|
||||||
|
- Default build script for extensions (#438)
|
||||||
|
- Input validation in installer
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Ask for admin password confirmation in installer (#405)
|
||||||
|
- Increased some text contrasts for accessibility (#390)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Discussion list did not work with non-empty database prefix (#269, #380)
|
||||||
|
- Non-admins could not reset their password (#229)
|
||||||
|
- Requests ending with a slash resulted in a 404 (#334)
|
||||||
|
- In rare cases, posts did not load correctly (#295)
|
||||||
|
- Avatars did not show up when installed in a subfolder (#291)
|
||||||
|
- Installer crashed when views directory was not writable (#376)
|
||||||
|
- Table prefix could not be set in web installer (#269)
|
||||||
|
- Enabling an extension disabled all other extensions (#402)
|
||||||
|
- Invalid custom CSS could crash the application (#400)
|
||||||
|
- First posts could not be restored or deleted
|
||||||
|
- Several design bugs
|
||||||
|
- Set cookies to be HTTP-only
|
||||||
|
- Tags: Sometimes, tags could not be dragged for reordering in the admin panel (#341)
|
||||||
|
- Suspend: Use correct column name in when migrating database
|
||||||
|
- Lock: Check for correct permission when displaying lock control
|
||||||
|
- Likes: Allow liking permissions to be configured
|
||||||
|
|
||||||
|
## 0.1.0-beta - 2015-08-27
|
||||||
|
First Version
|
||||||
|
|
||||||
|
[unreleased]: https://github.com/flarum/core/compare/v0.1.0-beta.2...HEAD
|
||||||
|
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2
|
@@ -26,13 +26,14 @@
|
|||||||
"tobscure/json-api": "^0.1.1",
|
"tobscure/json-api": "^0.1.1",
|
||||||
"oyejorge/less.php": "~1.5",
|
"oyejorge/less.php": "~1.5",
|
||||||
"intervention/image": "^2.3.0",
|
"intervention/image": "^2.3.0",
|
||||||
"s9e/text-formatter": "^0.1.0",
|
"s9e/text-formatter": "^0.3.2",
|
||||||
"psr/http-message": "^1.0",
|
"psr/http-message": "^1.0",
|
||||||
"zendframework/zend-diactoros": "^1.1",
|
"zendframework/zend-diactoros": "^1.1",
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
"dflydev/fig-cookies": "^1.0",
|
"dflydev/fig-cookies": "^1.0",
|
||||||
"symfony/console": "^2.7",
|
"symfony/console": "^2.7",
|
||||||
"symfony/yaml": "^2.7"
|
"symfony/yaml": "^2.7",
|
||||||
|
"doctrine/dbal": "^2.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "2.*",
|
"squizlabs/php_codesniffer": "2.*",
|
||||||
|
852
composer.lock
generated
852
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@ export default class AppearancePage extends Component {
|
|||||||
|
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
children: 'Save Changes',
|
children: 'Save Changes',
|
||||||
loading: this.loading
|
loading: this.loading
|
||||||
})}
|
})}
|
||||||
|
@@ -30,6 +30,7 @@ export default class EditCustomCssModal extends Modal {
|
|||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
{Button.component({
|
{Button.component({
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
children: 'Save Changes',
|
children: 'Save Changes',
|
||||||
loading: this.loading
|
loading: this.loading
|
||||||
})}
|
})}
|
||||||
|
@@ -4,6 +4,7 @@ import Search from 'flarum/components/Search';
|
|||||||
import Composer from 'flarum/components/Composer';
|
import Composer from 'flarum/components/Composer';
|
||||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
import ReplyComposer from 'flarum/components/ReplyComposer';
|
||||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||||
|
import SignUpModal from 'flarum/components/SignUpModal';
|
||||||
|
|
||||||
export default class ForumApp extends App {
|
export default class ForumApp extends App {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
@@ -76,4 +77,27 @@ export default class ForumApp extends App {
|
|||||||
return this.current instanceof DiscussionPage &&
|
return this.current instanceof DiscussionPage &&
|
||||||
this.current.discussion === discussion;
|
this.current.discussion === discussion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when an external authenticator (social login) action has
|
||||||
|
* completed.
|
||||||
|
*
|
||||||
|
* If the payload indicates that the user has been logged in, then the page
|
||||||
|
* will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
|
||||||
|
* with the provided details.
|
||||||
|
*
|
||||||
|
* @param {Object} payload A dictionary of props to pass into the sign up
|
||||||
|
* modal. A truthy `authenticated` prop indicates that the user has logged
|
||||||
|
* in, and thus the page is reloaded.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
authenticationComplete(payload) {
|
||||||
|
if (payload.authenticated) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const modal = new SignUpModal(payload);
|
||||||
|
this.modal.show(modal);
|
||||||
|
modal.$('[name=password]').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,66 +0,0 @@
|
|||||||
import Component from 'flarum/Component';
|
|
||||||
import humanTime from 'flarum/helpers/humanTime';
|
|
||||||
import avatar from 'flarum/helpers/avatar';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Activity` component represents a piece of activity of a user's activity
|
|
||||||
* feed. Subclasses should implement the `description` and `content` methods.
|
|
||||||
*
|
|
||||||
* ### Props
|
|
||||||
*
|
|
||||||
* - `activity`
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Activity extends Component {
|
|
||||||
view() {
|
|
||||||
const activity = this.props.activity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Activity">
|
|
||||||
{avatar(this.user(), {className: 'Activity-avatar'})}
|
|
||||||
|
|
||||||
<div className="Activity-header">
|
|
||||||
<strong className="Activity-description">{this.description()}</strong>
|
|
||||||
{humanTime(this.time())}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.content()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user whose avatar should be displayed.
|
|
||||||
*
|
|
||||||
* @return {User}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
user() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the time of the activity.
|
|
||||||
*
|
|
||||||
* @return {Date}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
time() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the description of the activity.
|
|
||||||
*
|
|
||||||
* @return {VirtualElement}
|
|
||||||
*/
|
|
||||||
description() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content to show below the activity description.
|
|
||||||
*
|
|
||||||
* @return {VirtualElement}
|
|
||||||
*/
|
|
||||||
content() {
|
|
||||||
}
|
|
||||||
}
|
|
30
js/forum/src/components/LogInButton.js
Normal file
30
js/forum/src/components/LogInButton.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LogInButton` component displays a social login button which will open
|
||||||
|
* a popup window containing the specified path.
|
||||||
|
*
|
||||||
|
* ### Props
|
||||||
|
*
|
||||||
|
* - `path`
|
||||||
|
*/
|
||||||
|
export default class LogInButton extends Button {
|
||||||
|
static initProps(props) {
|
||||||
|
props.className = (props.className || '') + ' LogInButton';
|
||||||
|
|
||||||
|
props.onclick = function() {
|
||||||
|
const width = 620;
|
||||||
|
const height = 400;
|
||||||
|
const $window = $(window);
|
||||||
|
|
||||||
|
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
|
||||||
|
`width=${width},` +
|
||||||
|
`height=${height},` +
|
||||||
|
`top=${$window.height() / 2 - height / 2},` +
|
||||||
|
`left=${$window.width() / 2 - width / 2},` +
|
||||||
|
'status=no,scrollbars=no,resizable=no');
|
||||||
|
};
|
||||||
|
|
||||||
|
super.initProps(props);
|
||||||
|
}
|
||||||
|
}
|
25
js/forum/src/components/LogInButtons.js
Normal file
25
js/forum/src/components/LogInButtons.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Component from 'flarum/Component';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LogInButtons` component displays a collection of social login buttons.
|
||||||
|
*/
|
||||||
|
export default class LogInButtons extends Component {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="LogInButtons">
|
||||||
|
{this.items().toArray()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a list of LogInButton components.
|
||||||
|
*
|
||||||
|
* @return {ItemList}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
items() {
|
||||||
|
return new ItemList();
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
|
|||||||
import SignUpModal from 'flarum/components/SignUpModal';
|
import SignUpModal from 'flarum/components/SignUpModal';
|
||||||
import Alert from 'flarum/components/Alert';
|
import Alert from 'flarum/components/Alert';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
|
import LogInButtons from 'flarum/components/LogInButtons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `LogInModal` component displays a modal dialog with a login form.
|
* The `LogInModal` component displays a modal dialog with a login form.
|
||||||
@@ -42,6 +43,8 @@ export default class LogInModal extends Modal {
|
|||||||
content() {
|
content() {
|
||||||
return [
|
return [
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
|
<LogInButtons/>
|
||||||
|
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
|
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
|
||||||
@@ -71,6 +74,7 @@ export default class LogInModal extends Modal {
|
|||||||
<p className="LogInModal-forgotPassword">
|
<p className="LogInModal-forgotPassword">
|
||||||
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
|
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{app.forum.attribute('allowSignUp') ? (
|
{app.forum.attribute('allowSignUp') ? (
|
||||||
<p className="LogInModal-signUp">
|
<p className="LogInModal-signUp">
|
||||||
{app.trans('core.before_sign_up_link')}{' '}
|
{app.trans('core.before_sign_up_link')}{' '}
|
||||||
@@ -84,6 +88,8 @@ export default class LogInModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* Open the forgot password modal, prefilling it with an email if the user has
|
* Open the forgot password modal, prefilling it with an email if the user has
|
||||||
* entered one.
|
* entered one.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
forgotPassword() {
|
forgotPassword() {
|
||||||
const email = this.email();
|
const email = this.email();
|
||||||
@@ -95,6 +101,8 @@ export default class LogInModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* Open the sign up modal, prefilling it with an email/username/password if
|
* Open the sign up modal, prefilling it with an email/username/password if
|
||||||
* the user has entered one.
|
* the user has entered one.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
signUp() {
|
signUp() {
|
||||||
const props = {password: this.password()};
|
const props = {password: this.password()};
|
||||||
|
@@ -2,6 +2,7 @@ import Modal from 'flarum/components/Modal';
|
|||||||
import LogInModal from 'flarum/components/LogInModal';
|
import LogInModal from 'flarum/components/LogInModal';
|
||||||
import avatar from 'flarum/helpers/avatar';
|
import avatar from 'flarum/helpers/avatar';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
|
import LogInButtons from 'flarum/components/LogInButtons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
* The `SignUpModal` component displays a modal dialog with a singup form.
|
||||||
@@ -11,6 +12,7 @@ import Button from 'flarum/components/Button';
|
|||||||
* - `username`
|
* - `username`
|
||||||
* - `email`
|
* - `email`
|
||||||
* - `password`
|
* - `password`
|
||||||
|
* - `token` An email token to sign up with.
|
||||||
*/
|
*/
|
||||||
export default class SignUpModal extends Modal {
|
export default class SignUpModal extends Modal {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
@@ -65,7 +67,9 @@ export default class SignUpModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body() {
|
body() {
|
||||||
const body = [(
|
const body = [
|
||||||
|
this.props.token ? '' : <LogInButtons/>,
|
||||||
|
|
||||||
<div className="Form Form--centered">
|
<div className="Form Form--centered">
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
|
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
|
||||||
@@ -78,26 +82,28 @@ export default class SignUpModal extends Modal {
|
|||||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
||||||
value={this.email()}
|
value={this.email()}
|
||||||
onchange={m.withAttr('value', this.email)}
|
onchange={m.withAttr('value', this.email)}
|
||||||
disabled={this.loading} />
|
disabled={this.loading || this.props.token} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.props.token ? '' : (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
||||||
value={this.password()}
|
value={this.password()}
|
||||||
onchange={m.withAttr('value', this.password)}
|
onchange={m.withAttr('value', this.password)}
|
||||||
disabled={this.loading} />
|
disabled={this.loading} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
{Button.component({
|
<Button
|
||||||
className: 'Button Button--primary Button--block',
|
className="Button Button--primary Button--block"
|
||||||
type: 'submit',
|
type="submit"
|
||||||
loading: this.loading,
|
loading={this.loading}>
|
||||||
children: app.trans('core.sign_up')
|
{app.trans('core.sign_up')}
|
||||||
})}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)];
|
];
|
||||||
|
|
||||||
if (this.welcomeUser) {
|
if (this.welcomeUser) {
|
||||||
const user = this.welcomeUser;
|
const user = this.welcomeUser;
|
||||||
@@ -115,20 +121,12 @@ export default class SignUpModal extends Modal {
|
|||||||
{avatar(user)}
|
{avatar(user)}
|
||||||
<h3>{app.trans('core.welcome_user', {user})}</h3>
|
<h3>{app.trans('core.welcome_user', {user})}</h3>
|
||||||
|
|
||||||
{!user.isActivated() ? [
|
|
||||||
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
||||||
<p>
|
<p>
|
||||||
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
||||||
{app.trans('core.go_to', {location: emailProviderName})}
|
{app.trans('core.go_to', {location: emailProviderName})}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
] : (
|
|
||||||
<p>
|
|
||||||
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
|
|
||||||
{app.trans('core.dismiss')}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,6 +148,8 @@ export default class SignUpModal extends Modal {
|
|||||||
/**
|
/**
|
||||||
* Open the log in modal, prefilling it with an email/username/password if
|
* Open the log in modal, prefilling it with an email/username/password if
|
||||||
* the user has entered one.
|
* the user has entered one.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
logIn() {
|
logIn() {
|
||||||
const props = {
|
const props = {
|
||||||
@@ -161,7 +161,7 @@ export default class SignUpModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onready() {
|
onready() {
|
||||||
if (this.props.username) {
|
if (this.props.username && !this.props.token) {
|
||||||
this.$('[name=email]').select();
|
this.$('[name=email]').select();
|
||||||
} else {
|
} else {
|
||||||
super.onready();
|
super.onready();
|
||||||
@@ -175,24 +175,50 @@ export default class SignUpModal extends Modal {
|
|||||||
|
|
||||||
const data = this.submitData();
|
const data = this.submitData();
|
||||||
|
|
||||||
app.store.createRecord('users').save(data).then(
|
app.request({
|
||||||
user => {
|
url: app.forum.attribute('baseUrl') + '/register',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
}).then(
|
||||||
|
payload => {
|
||||||
|
const user = app.store.pushPayload(payload);
|
||||||
|
|
||||||
|
// If the user's new account has been activated, then we can assume
|
||||||
|
// that they have been logged in too. Thus, we will reload the page.
|
||||||
|
// Otherwise, we will show a message asking them to check their email.
|
||||||
|
if (user.isActivated()) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
this.welcomeUser = user;
|
this.welcomeUser = user;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
response => {
|
response => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.handleErrors(response.errors);
|
this.handleErrors(response);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data that should be submitted in the sign-up request.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
submitData() {
|
submitData() {
|
||||||
return {
|
const data = {
|
||||||
username: this.username(),
|
username: this.username(),
|
||||||
email: this.email(),
|
email: this.email()
|
||||||
password: this.password()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.props.token) {
|
||||||
|
data.token = this.props.token;
|
||||||
|
} else {
|
||||||
|
data.password = this.password();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ export default class Session {
|
|||||||
* The token that was used for authentication.
|
* The token that was used for authentication.
|
||||||
*
|
*
|
||||||
* @type {String|null}
|
* @type {String|null}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ export default class Session {
|
|||||||
* @param {String} identification The username/email.
|
* @param {String} identification The username/email.
|
||||||
* @param {String} password
|
* @param {String} password
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
login(identification, password) {
|
login(identification, password) {
|
||||||
return app.request({
|
return app.request({
|
||||||
@@ -38,6 +40,8 @@ export default class Session {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the user out.
|
* Log the user out.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
||||||
@@ -48,8 +52,11 @@ export default class Session {
|
|||||||
* XMLHttpRequest object.
|
* XMLHttpRequest object.
|
||||||
*
|
*
|
||||||
* @param {XMLHttpRequest} xhr
|
* @param {XMLHttpRequest} xhr
|
||||||
|
* @public
|
||||||
*/
|
*/
|
||||||
authorize(xhr) {
|
authorize(xhr) {
|
||||||
|
if (this.token) {
|
||||||
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@@ -25,7 +25,8 @@ export default class Button extends Component {
|
|||||||
|
|
||||||
delete attrs.children;
|
delete attrs.children;
|
||||||
|
|
||||||
attrs.className = (attrs.className || '');
|
attrs.className = attrs.className || '';
|
||||||
|
attrs.type = attrs.type || 'button';
|
||||||
|
|
||||||
const iconName = extract(attrs, 'icon');
|
const iconName = extract(attrs, 'icon');
|
||||||
if (iconName) attrs.className += ' hasIcon';
|
if (iconName) attrs.className += ' hasIcon';
|
||||||
|
@@ -129,7 +129,7 @@ export default class Modal extends Component {
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
if (errors) {
|
if (errors) {
|
||||||
this.$('form [name=' + errors[0].path + ']').select();
|
this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
|
||||||
} else {
|
} else {
|
||||||
this.$('form :input:first').select();
|
this.$('form :input:first').select();
|
||||||
}
|
}
|
||||||
|
@@ -45,7 +45,7 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
.read & {
|
.read & {
|
||||||
color: mix(@heading-color, @body-bg, 60%);
|
color: mix(@heading-color, @body-bg, 55%);
|
||||||
}
|
}
|
||||||
.unread & {
|
.unread & {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -59,6 +59,12 @@
|
|||||||
|
|
||||||
> li {
|
> li {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
opacity: 0.7;
|
||||||
|
.transition(opacity 0.2s);
|
||||||
|
|
||||||
|
.DiscussionListItem:hover &, .DiscussionListItem.active & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.username {
|
.username {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
11
less/forum/LogInButton.less
Normal file
11
less/forum/LogInButton.less
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.LogInButton {
|
||||||
|
&:extend(.Button--block);
|
||||||
|
}
|
||||||
|
.LogInButtons {
|
||||||
|
width: 200px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
|
||||||
|
.LogInButton {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
@@ -139,7 +139,7 @@
|
|||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
img {
|
img, iframe {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -126,6 +126,9 @@
|
|||||||
& .icon {
|
& .icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
&.online .fa-circle {
|
||||||
|
color: @online-user-circle-color;
|
||||||
|
}
|
||||||
&.online .icon {
|
&.online .icon {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
@import "EditUserModal.less";
|
@import "EditUserModal.less";
|
||||||
@import "Hero.less";
|
@import "Hero.less";
|
||||||
@import "IndexPage.less";
|
@import "IndexPage.less";
|
||||||
|
@import "LogInButton.less";
|
||||||
@import "LogInModal.less";
|
@import "LogInModal.less";
|
||||||
@import "NotificationGrid.less";
|
@import "NotificationGrid.less";
|
||||||
@import "NotificationList.less";
|
@import "NotificationList.less";
|
||||||
|
@@ -27,8 +27,7 @@
|
|||||||
&[disabled],
|
&[disabled],
|
||||||
&[readonly],
|
&[readonly],
|
||||||
fieldset[disabled] & {
|
fieldset[disabled] & {
|
||||||
// background-color: @input-bg-disabled;
|
opacity: 0.5;
|
||||||
opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
@text-color: #111;
|
@text-color: #111;
|
||||||
@link-color: saturate(@primary-color, 10%);
|
@link-color: saturate(@primary-color, 10%);
|
||||||
@heading-color: @text-color;
|
@heading-color: @text-color;
|
||||||
@muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 60%);
|
@muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 55%);
|
||||||
@muted-more-color: #aaa;
|
@muted-more-color: #aaa;
|
||||||
@shadow-color: rgba(0, 0, 0, 0.35);
|
@shadow-color: rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
@@ -130,3 +130,5 @@
|
|||||||
|
|
||||||
@tooltip-bg: rgba(0, 0, 0, 0.9);
|
@tooltip-bg: rgba(0, 0, 0, 0.9);
|
||||||
@tooltip-color: #fff;
|
@tooltip-color: #fff;
|
||||||
|
|
||||||
|
@online-user-circle-color: #7FBA00;
|
||||||
|
@@ -28,7 +28,6 @@ core:
|
|||||||
discussion_started: "Started {ago} by {username}"
|
discussion_started: "Started {ago} by {username}"
|
||||||
discussion_title: Discussion Title
|
discussion_title: Discussion Title
|
||||||
discussions: Discussions
|
discussions: Discussions
|
||||||
dismiss: Dismiss
|
|
||||||
edit: Edit
|
edit: Edit
|
||||||
editing_post: "Post #{number} in {discussion}"
|
editing_post: "Post #{number} in {discussion}"
|
||||||
email: Email
|
email: Email
|
||||||
|
@@ -23,8 +23,8 @@ class CreateEmailTokensTable extends Migration
|
|||||||
{
|
{
|
||||||
$this->schema->create('email_tokens', function (Blueprint $table) {
|
$this->schema->create('email_tokens', function (Blueprint $table) {
|
||||||
$table->string('id', 100)->primary();
|
$table->string('id', 100)->primary();
|
||||||
$table->integer('user_id')->unsigned();
|
|
||||||
$table->string('email', 150);
|
$table->string('email', 150);
|
||||||
|
$table->integer('user_id')->unsigned();
|
||||||
$table->timestamp('created_at');
|
$table->timestamp('created_at');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Flarum\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
class MakeEmailTokensUserIdColumnNullable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->schema->table('email_tokens', function (Blueprint $table) {
|
||||||
|
$table->integer('user_id')->unsigned()->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->schema->table('email_tokens', function (Blueprint $table) {
|
||||||
|
$table->integer('user_id')->unsigned()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -15,36 +15,32 @@ use Illuminate\Contracts\Container\Container;
|
|||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Stratigility\MiddlewareInterface;
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
use Flarum\Forum\Middleware\LoginWithCookie;
|
||||||
|
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||||
|
|
||||||
class LoginWithCookieAndCheckAdmin implements MiddlewareInterface
|
class LoginWithCookieAndCheckAdmin extends LoginWithCookie
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var Container
|
|
||||||
*/
|
|
||||||
protected $app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Container $app
|
|
||||||
*/
|
|
||||||
public function __construct(Container $app)
|
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
if (($token = array_get($request->getCookieParams(), 'flarum_remember')) &&
|
if (! $this->logIn($request)) {
|
||||||
($accessToken = AccessToken::valid($token)) &&
|
throw new PermissionDeniedException;
|
||||||
$accessToken->user->isAdmin()
|
|
||||||
) {
|
|
||||||
$this->app->instance('flarum.actor', $accessToken->user);
|
|
||||||
} else {
|
|
||||||
die('Access Denied');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out ? $out($request, $response) : $response;
|
return $out ? $out($request, $response) : $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getToken(Request $request)
|
||||||
|
{
|
||||||
|
$token = parent::getToken($request);
|
||||||
|
|
||||||
|
if ($token && $token->user && $token->user->isAdmin()) {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Api;
|
|||||||
|
|
||||||
use Flarum\Core\Model;
|
use Flarum\Core\Model;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo document database columns with @property
|
* @todo document database columns with @property
|
||||||
@@ -55,14 +56,13 @@ class AccessToken extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the given token only if it is valid.
|
* Check that the token has not expired.
|
||||||
*
|
*
|
||||||
* @param string $token
|
* @return bool
|
||||||
* @return static|null
|
|
||||||
*/
|
*/
|
||||||
public static function valid($token)
|
public function isValid()
|
||||||
{
|
{
|
||||||
return static::where('id', $token)->where('expires_at', '>', new DateTime)->first();
|
return $this->expires_at > new DateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -13,7 +13,7 @@ namespace Flarum\Api\Actions;
|
|||||||
use Flarum\Api\Request;
|
use Flarum\Api\Request;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
abstract class DeleteAction extends JsonApiAction
|
abstract class DeleteAction implements Action
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Delegate deletion of the resource, and return a 204 No Content
|
* Delegate deletion of the resource, and return a 204 No Content
|
||||||
@@ -22,7 +22,7 @@ abstract class DeleteAction extends JsonApiAction
|
|||||||
* @param \Flarum\Api\Request $request
|
* @param \Flarum\Api\Request $request
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
*/
|
*/
|
||||||
public function respond(Request $request)
|
public function handle(Request $request)
|
||||||
{
|
{
|
||||||
$this->delete($request);
|
$this->delete($request);
|
||||||
|
|
||||||
|
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Actions\Extensions;
|
namespace Flarum\Api\Actions\Extensions;
|
||||||
|
|
||||||
use Flarum\Api\Actions\JsonApiAction;
|
use Flarum\Api\Actions\Action;
|
||||||
use Flarum\Api\Request;
|
use Flarum\Api\Request;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||||
use Flarum\Support\ExtensionManager;
|
use Flarum\Support\ExtensionManager;
|
||||||
|
|
||||||
class UpdateAction extends JsonApiAction
|
class UpdateAction implements Action
|
||||||
{
|
{
|
||||||
protected $extensions;
|
protected $extensions;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class UpdateAction extends JsonApiAction
|
|||||||
$this->extensions = $extensions;
|
$this->extensions = $extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function respond(Request $request)
|
public function handle(Request $request)
|
||||||
{
|
{
|
||||||
if (! $request->actor->isAdmin()) {
|
if (! $request->actor->isAdmin()) {
|
||||||
throw new PermissionDeniedException;
|
throw new PermissionDeniedException;
|
||||||
|
@@ -16,7 +16,7 @@ use Flarum\Core\Users\Commands\RequestPasswordReset;
|
|||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
class ForgotAction extends JsonApiAction
|
class ForgotAction implements Action
|
||||||
{
|
{
|
||||||
protected $users;
|
protected $users;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class ForgotAction extends JsonApiAction
|
|||||||
* @param \Flarum\Api\Request $request
|
* @param \Flarum\Api\Request $request
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
*/
|
*/
|
||||||
public function respond(Request $request)
|
public function handle(Request $request)
|
||||||
{
|
{
|
||||||
$email = $request->get('email');
|
$email = $request->get('email');
|
||||||
|
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Api\Actions;
|
|
||||||
|
|
||||||
use Flarum\Api\Request;
|
|
||||||
use Illuminate\Contracts\Validation\ValidationException;
|
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
||||||
use Flarum\Core\Exceptions\ValidationFailureException;
|
|
||||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
|
||||||
|
|
||||||
abstract class JsonApiAction implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an API request and return an API response, handling any relevant
|
|
||||||
* (API-related) exceptions that are thrown.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*/
|
|
||||||
public function handle(Request $request)
|
|
||||||
{
|
|
||||||
// TODO: This is gross. Move this error handling code to middleware?
|
|
||||||
try {
|
|
||||||
return $this->respond($request);
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
$errors = [];
|
|
||||||
foreach ($e->errors()->toArray() as $field => $messages) {
|
|
||||||
$errors[] = [
|
|
||||||
'detail' => implode("\n", $messages),
|
|
||||||
'path' => $field
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return new JsonResponse(['errors' => $errors], 422);
|
|
||||||
} catch (\Flarum\Core\Exceptions\ValidationException $e) {
|
|
||||||
$errors = [];
|
|
||||||
foreach ($e->getMessages() as $path => $detail) {
|
|
||||||
$errors[] = compact('path', 'detail');
|
|
||||||
}
|
|
||||||
return new JsonResponse(['errors' => $errors], 422);
|
|
||||||
} catch (PermissionDeniedException $e) {
|
|
||||||
return new JsonResponse(null, 401);
|
|
||||||
} catch (ModelNotFoundException $e) {
|
|
||||||
return new JsonResponse(null, 404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an API request and return an API response.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*/
|
|
||||||
abstract protected function respond(Request $request);
|
|
||||||
}
|
|
@@ -19,7 +19,7 @@ use Tobscure\JsonApi\Document;
|
|||||||
use Tobscure\JsonApi\SerializerInterface;
|
use Tobscure\JsonApi\SerializerInterface;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
|
||||||
abstract class SerializeAction extends JsonApiAction
|
abstract class SerializeAction implements Action
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The name of the serializer class to output results with.
|
* The name of the serializer class to output results with.
|
||||||
@@ -77,7 +77,7 @@ abstract class SerializeAction extends JsonApiAction
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return JsonResponse
|
* @return JsonResponse
|
||||||
*/
|
*/
|
||||||
public function respond(Request $request)
|
public function handle(Request $request)
|
||||||
{
|
{
|
||||||
$request = $this->buildJsonApiRequest($request);
|
$request = $this->buildJsonApiRequest($request);
|
||||||
$document = new Document();
|
$document = new Document();
|
||||||
|
@@ -18,7 +18,7 @@ use Flarum\Events\UserEmailChangeWasRequested;
|
|||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
|
||||||
class TokenAction extends JsonApiAction
|
class TokenAction implements Action
|
||||||
{
|
{
|
||||||
protected $users;
|
protected $users;
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class TokenAction extends JsonApiAction
|
|||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
* @throws PermissionDeniedException
|
* @throws PermissionDeniedException
|
||||||
*/
|
*/
|
||||||
public function respond(Request $request)
|
public function handle(Request $request)
|
||||||
{
|
{
|
||||||
$identification = $request->get('identification');
|
$identification = $request->get('identification');
|
||||||
$password = $request->get('password');
|
$password = $request->get('password');
|
||||||
|
@@ -73,8 +73,12 @@ class CreateAction extends BaseCreateAction
|
|||||||
*/
|
*/
|
||||||
protected function create(JsonApiRequest $request)
|
protected function create(JsonApiRequest $request)
|
||||||
{
|
{
|
||||||
return $this->bus->dispatch(
|
$user = $this->bus->dispatch(
|
||||||
new RegisterUser($request->actor, $request->get('data'))
|
new RegisterUser($request->actor, $request->get('data'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$request->actor = $user;
|
||||||
|
|
||||||
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,8 @@ namespace Flarum\Api;
|
|||||||
|
|
||||||
use Flarum\Core\Users\User;
|
use Flarum\Core\Users\User;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Api\Middleware\JsonApiErrors;
|
||||||
|
|
||||||
class Client
|
class Client
|
||||||
{
|
{
|
||||||
@@ -38,10 +40,16 @@ class Client
|
|||||||
*/
|
*/
|
||||||
public function send(User $actor, $actionClass, array $input = [])
|
public function send(User $actor, $actionClass, array $input = [])
|
||||||
{
|
{
|
||||||
/** @var \Flarum\Api\Actions\JsonApiAction $action */
|
/** @var \Flarum\Api\Actions\Action $action */
|
||||||
$action = $this->container->make($actionClass);
|
$action = $this->container->make($actionClass);
|
||||||
|
|
||||||
|
try {
|
||||||
$response = $action->handle(new Request($input, $actor));
|
$response = $action->handle(new Request($input, $actor));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$middleware = new JsonApiErrors();
|
||||||
|
|
||||||
|
$response = $middleware->handle($e);
|
||||||
|
}
|
||||||
|
|
||||||
return new Response($response);
|
return new Response($response);
|
||||||
}
|
}
|
||||||
|
@@ -10,35 +10,65 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Middleware;
|
namespace Flarum\Api\Middleware;
|
||||||
|
|
||||||
|
use Flarum\Core\Exceptions\JsonApiSerializable;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use Zend\Stratigility\ErrorMiddlewareInterface;
|
use Zend\Stratigility\ErrorMiddlewareInterface;
|
||||||
|
use Flarum\Core;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
class JsonApiErrors implements ErrorMiddlewareInterface
|
class JsonApiErrors implements ErrorMiddlewareInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function __invoke($error, Request $request, Response $response, callable $out = null)
|
public function __invoke($e, Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
$errorObject = [
|
return $this->handle($e);
|
||||||
'title' => $error->getMessage(),
|
}
|
||||||
];
|
|
||||||
|
|
||||||
|
public function handle(Exception $e)
|
||||||
|
{
|
||||||
|
if ($e instanceof JsonApiSerializable) {
|
||||||
|
$status = $e->getStatusCode();
|
||||||
|
|
||||||
|
$errors = $e->getErrors();
|
||||||
|
} else if ($e instanceof ValidationException) {
|
||||||
|
$status = 422;
|
||||||
|
|
||||||
|
$errors = $e->errors()->toArray();
|
||||||
|
$errors = array_map(function ($field, $messages) {
|
||||||
|
return [
|
||||||
|
'detail' => implode("\n", $messages),
|
||||||
|
'source' => ['pointer' => '/data/attributes/' . $field],
|
||||||
|
];
|
||||||
|
}, array_keys($errors), $errors);
|
||||||
|
} else if ($e instanceof ModelNotFoundException) {
|
||||||
|
$status = 404;
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
} else {
|
||||||
$status = 500;
|
$status = 500;
|
||||||
|
|
||||||
// If it seems to be a valid HTTP status code, we pass on the
|
$error = [
|
||||||
// exception's status.
|
'code' => $status,
|
||||||
$errorCode = $error->getCode();
|
'title' => 'Internal Server Error'
|
||||||
if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) {
|
];
|
||||||
$status = $errorCode;
|
|
||||||
|
if (Core::inDebugMode()) {
|
||||||
|
$error['detail'] = (string) $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [$error];
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON API errors must be collected in an array under the
|
// JSON API errors must be collected in an array under the
|
||||||
// "errors" key in the top level of the document
|
// "errors" key in the top level of the document
|
||||||
$data = [
|
$data = [
|
||||||
'errors' => [$errorObject]
|
'errors' => $errors,
|
||||||
];
|
];
|
||||||
|
|
||||||
return new JsonResponse($data, $status);
|
return new JsonResponse($data, $status);
|
||||||
|
@@ -50,7 +50,7 @@ class LoginWithHeader implements MiddlewareInterface
|
|||||||
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
||||||
$token = substr($parts[0], strlen($this->prefix));
|
$token = substr($parts[0], strlen($this->prefix));
|
||||||
|
|
||||||
if ($accessToken = AccessToken::valid($token)) {
|
if (($accessToken = AccessToken::find($token)) && $accessToken->isValid()) {
|
||||||
$this->app->instance('flarum.actor', $user = $accessToken->user);
|
$this->app->instance('flarum.actor', $user = $accessToken->user);
|
||||||
|
|
||||||
$user->updateLastSeen()->save();
|
$user->updateLastSeen()->save();
|
||||||
|
@@ -27,7 +27,7 @@ class Application extends Container implements ApplicationContract
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
const VERSION = '0.1.0-beta';
|
const VERSION = '0.1.0-beta.2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base path for the Laravel installation.
|
* The base path for the Laravel installation.
|
||||||
|
@@ -12,6 +12,21 @@ namespace Flarum\Core\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class InvalidConfirmationTokenException extends Exception
|
class InvalidConfirmationTokenException extends Exception implements JsonApiSerializable
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getStatusCode()
|
||||||
|
{
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getErrors()
|
||||||
|
{
|
||||||
|
return ['code' => 'invalid_confirmation_token'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
29
src/Core/Exceptions/JsonApiSerializable.php
Normal file
29
src/Core/Exceptions/JsonApiSerializable.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Core\Exceptions;
|
||||||
|
|
||||||
|
interface JsonApiSerializable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return the HTTP status code to be used for this exception.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getStatusCode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of errors, formatted as JSON-API error objects.
|
||||||
|
*
|
||||||
|
* @see http://jsonapi.org/format/#error-objects
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getErrors();
|
||||||
|
}
|
@@ -12,6 +12,26 @@ namespace Flarum\Core\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class PermissionDeniedException extends Exception
|
class PermissionDeniedException extends Exception implements JsonApiSerializable
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Return the HTTP status code to be used for this exception.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getStatusCode()
|
||||||
|
{
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of errors, formatted as JSON-API error objects.
|
||||||
|
*
|
||||||
|
* @see http://jsonapi.org/format/#error-objects
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getErrors()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,17 +12,39 @@ namespace Flarum\Core\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class ValidationException extends Exception
|
class ValidationException extends Exception implements JsonApiSerializable
|
||||||
{
|
{
|
||||||
protected $messages;
|
protected $messages;
|
||||||
|
|
||||||
public function __construct(array $messages)
|
public function __construct(array $messages)
|
||||||
{
|
{
|
||||||
$this->messages = $messages;
|
$this->messages = $messages;
|
||||||
|
|
||||||
|
parent::__construct(implode("\n", $messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMessages()
|
public function getMessages()
|
||||||
{
|
{
|
||||||
return $this->messages;
|
return $this->messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getStatusCode()
|
||||||
|
{
|
||||||
|
return 422;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getErrors()
|
||||||
|
{
|
||||||
|
return array_map(function ($path, $detail) {
|
||||||
|
$source = ['pointer' => '/data/attributes/' . $path];
|
||||||
|
|
||||||
|
return compact('source', 'detail');
|
||||||
|
}, array_keys($this->messages), $this->messages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,14 +10,8 @@
|
|||||||
|
|
||||||
namespace Flarum\Core;
|
namespace Flarum\Core;
|
||||||
|
|
||||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
|
||||||
use Flarum\Core\Users\User;
|
|
||||||
use Flarum\Events\ModelAllow;
|
|
||||||
use Flarum\Events\ModelDates;
|
use Flarum\Events\ModelDates;
|
||||||
use Flarum\Events\ModelRelationship;
|
use Flarum\Events\ModelRelationship;
|
||||||
use Flarum\Events\ScopeModelVisibility;
|
|
||||||
use Illuminate\Contracts\Validation\Factory;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model as Eloquent;
|
use Illuminate\Database\Eloquent\Model as Eloquent;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
|
@@ -13,7 +13,6 @@ namespace Flarum\Core\Users\Commands;
|
|||||||
use Flarum\Core\Users\UserRepository;
|
use Flarum\Core\Users\UserRepository;
|
||||||
use Flarum\Events\UserWillBeSaved;
|
use Flarum\Events\UserWillBeSaved;
|
||||||
use Flarum\Core\Support\DispatchesEvents;
|
use Flarum\Core\Support\DispatchesEvents;
|
||||||
use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
|
|
||||||
use Flarum\Core\Users\EmailToken;
|
use Flarum\Core\Users\EmailToken;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
@@ -36,16 +35,14 @@ class ConfirmEmailHandler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ConfirmEmail $command
|
* @param ConfirmEmail $command
|
||||||
* @return \Flarum\Core\Users\User
|
*
|
||||||
* @throws InvalidConfirmationTokenException
|
* @throws InvalidConfirmationTokenException
|
||||||
|
*
|
||||||
|
* @return \Flarum\Core\Users\User
|
||||||
*/
|
*/
|
||||||
public function handle(ConfirmEmail $command)
|
public function handle(ConfirmEmail $command)
|
||||||
{
|
{
|
||||||
$token = EmailToken::find($command->token);
|
$token = EmailToken::validOrFail($command->token);
|
||||||
|
|
||||||
if (! $token || $token->created_at < new DateTime('-1 day')) {
|
|
||||||
throw new InvalidConfirmationTokenException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $token->user;
|
$user = $token->user;
|
||||||
$user->changeEmail($token->email);
|
$user->changeEmail($token->email);
|
||||||
|
@@ -11,17 +11,25 @@
|
|||||||
namespace Flarum\Core\Users\Commands;
|
namespace Flarum\Core\Users\Commands;
|
||||||
|
|
||||||
use Flarum\Core\Users\User;
|
use Flarum\Core\Users\User;
|
||||||
|
use Flarum\Core\Users\EmailToken;
|
||||||
use Flarum\Events\UserWillBeSaved;
|
use Flarum\Events\UserWillBeSaved;
|
||||||
use Flarum\Core\Support\DispatchesEvents;
|
use Flarum\Core\Support\DispatchesEvents;
|
||||||
use Flarum\Core\Settings\SettingsRepository;
|
use Flarum\Core\Settings\SettingsRepository;
|
||||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
class RegisterUserHandler
|
class RegisterUserHandler
|
||||||
{
|
{
|
||||||
use DispatchesEvents;
|
use DispatchesEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SettingsRepository
|
||||||
|
*/
|
||||||
protected $settings;
|
protected $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SettingsRepository $settings
|
||||||
|
*/
|
||||||
public function __construct(SettingsRepository $settings)
|
public function __construct(SettingsRepository $settings)
|
||||||
{
|
{
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
@@ -29,26 +37,57 @@ class RegisterUserHandler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param RegisterUser $command
|
* @param RegisterUser $command
|
||||||
|
*
|
||||||
|
* @throws PermissionDeniedException if signup is closed and the actor is
|
||||||
|
* not an administrator.
|
||||||
|
* @throws \Flarum\Core\Exceptions\InvalidConfirmationTokenException if an
|
||||||
|
* email confirmation token is provided but is invalid.
|
||||||
|
*
|
||||||
* @return User
|
* @return User
|
||||||
*/
|
*/
|
||||||
public function handle(RegisterUser $command)
|
public function handle(RegisterUser $command)
|
||||||
{
|
{
|
||||||
if (! $this->settings->get('allow_sign_up')) {
|
|
||||||
throw new PermissionDeniedException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$actor = $command->actor;
|
$actor = $command->actor;
|
||||||
$data = $command->data;
|
$data = $command->data;
|
||||||
|
|
||||||
|
if (! $this->settings->get('allow_sign_up') && ! $actor->isAdmin()) {
|
||||||
|
throw new PermissionDeniedException;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a valid email confirmation token was provided as an attribute,
|
||||||
|
// then we can create a random password for this user and consider their
|
||||||
|
// email address confirmed.
|
||||||
|
if (isset($data['attributes']['token'])) {
|
||||||
|
$token = EmailToken::whereNull('user_id')->validOrFail($data['attributes']['token']);
|
||||||
|
|
||||||
|
$email = $token->email;
|
||||||
|
$password = array_get($data, 'attributes.password', str_random(20));
|
||||||
|
} else {
|
||||||
|
$email = array_get($data, 'attributes.email');
|
||||||
|
$password = array_get($data, 'attributes.password');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user's new account. If their email was set via token, then
|
||||||
|
// we can activate their account from the get-go, and they won't need
|
||||||
|
// to confirm their email address.
|
||||||
$user = User::register(
|
$user = User::register(
|
||||||
array_get($data, 'attributes.username'),
|
array_get($data, 'attributes.username'),
|
||||||
array_get($data, 'attributes.email'),
|
$email,
|
||||||
array_get($data, 'attributes.password')
|
$password
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isset($token)) {
|
||||||
|
$user->activate();
|
||||||
|
}
|
||||||
|
|
||||||
event(new UserWillBeSaved($user, $actor, $data));
|
event(new UserWillBeSaved($user, $actor, $data));
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
if (isset($token)) {
|
||||||
|
$token->delete();
|
||||||
|
}
|
||||||
|
|
||||||
$this->dispatchEventsFor($user);
|
$this->dispatchEventsFor($user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
|
@@ -11,6 +11,8 @@
|
|||||||
namespace Flarum\Core\Users;
|
namespace Flarum\Core\Users;
|
||||||
|
|
||||||
use Flarum\Core\Model;
|
use Flarum\Core\Model;
|
||||||
|
use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo document database columns with @property
|
* @todo document database columns with @property
|
||||||
@@ -37,11 +39,12 @@ class EmailToken extends Model
|
|||||||
/**
|
/**
|
||||||
* Generate an email token for the specified user.
|
* Generate an email token for the specified user.
|
||||||
*
|
*
|
||||||
* @param int $userId
|
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
* @param int $userId
|
||||||
|
*
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public static function generate($userId, $email)
|
public static function generate($email, $userId = null)
|
||||||
{
|
{
|
||||||
$token = new static;
|
$token = new static;
|
||||||
|
|
||||||
@@ -62,4 +65,25 @@ class EmailToken extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo('Flarum\Core\Users\User');
|
return $this->belongsTo('Flarum\Core\Users\User');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the token with the given ID, and assert that it has not expired.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $id
|
||||||
|
*
|
||||||
|
* @throws InvalidConfirmationTokenException
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function scopeValidOrFail($query, $id)
|
||||||
|
{
|
||||||
|
$token = $query->find($id);
|
||||||
|
|
||||||
|
if (! $token || $token->created_at < new DateTime('-1 day')) {
|
||||||
|
throw new InvalidConfirmationTokenException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,6 +57,11 @@ class EmailConfirmationMailer
|
|||||||
public function whenUserWasRegistered(UserWasRegistered $event)
|
public function whenUserWasRegistered(UserWasRegistered $event)
|
||||||
{
|
{
|
||||||
$user = $event->user;
|
$user = $event->user;
|
||||||
|
|
||||||
|
if ($user->is_activated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$data = $this->getEmailData($user, $user->email);
|
$data = $this->getEmailData($user, $user->email);
|
||||||
|
|
||||||
$this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function (Message $message) use ($user) {
|
$this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function (Message $message) use ($user) {
|
||||||
@@ -82,11 +87,12 @@ class EmailConfirmationMailer
|
|||||||
/**
|
/**
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
*
|
||||||
* @return EmailToken
|
* @return EmailToken
|
||||||
*/
|
*/
|
||||||
protected function generateToken(User $user, $email)
|
protected function generateToken(User $user, $email)
|
||||||
{
|
{
|
||||||
$token = EmailToken::generate($user->id, $email);
|
$token = EmailToken::generate($email, $user->id);
|
||||||
$token->save();
|
$token->save();
|
||||||
|
|
||||||
return $token;
|
return $token;
|
||||||
@@ -97,6 +103,7 @@ class EmailConfirmationMailer
|
|||||||
*
|
*
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param string $email
|
* @param string $email
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function getEmailData(User $user, $email)
|
protected function getEmailData(User $user, $email)
|
||||||
|
@@ -56,7 +56,6 @@ class ClientAction extends BaseClientAction
|
|||||||
'core.discussion_started',
|
'core.discussion_started',
|
||||||
'core.discussion_title',
|
'core.discussion_title',
|
||||||
'core.discussions',
|
'core.discussions',
|
||||||
'core.dismiss',
|
|
||||||
'core.edit',
|
'core.edit',
|
||||||
'core.editing_post',
|
'core.editing_post',
|
||||||
'core.email',
|
'core.email',
|
||||||
|
70
src/Forum/Actions/ExternalAuthenticatorTrait.php
Normal file
70
src/Forum/Actions/ExternalAuthenticatorTrait.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Forum\Actions;
|
||||||
|
|
||||||
|
use Flarum\Core\Users\User;
|
||||||
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
|
use Flarum\Api\Commands\GenerateAccessToken;
|
||||||
|
use Flarum\Core\Users\EmailToken;
|
||||||
|
|
||||||
|
trait ExternalAuthenticatorTrait
|
||||||
|
{
|
||||||
|
use WritesRememberCookie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Contracts\Bus\Dispatcher
|
||||||
|
*/
|
||||||
|
protected $bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond with JavaScript to tell the Flarum app that the user has been
|
||||||
|
* authenticated, or with information about their sign up status.
|
||||||
|
*
|
||||||
|
* @param string $email The email of the user's account.
|
||||||
|
* @param string $username A suggested username for the user's account.
|
||||||
|
* @return HtmlResponse
|
||||||
|
*/
|
||||||
|
protected function authenticated($email, $username)
|
||||||
|
{
|
||||||
|
$user = User::where('email', $email)->first();
|
||||||
|
|
||||||
|
// If a user with this email address doesn't already exist, then we will
|
||||||
|
// generate a unique confirmation token for this email address and add
|
||||||
|
// it to the response, along with the email address and a suggested
|
||||||
|
// username. Otherwise, we will log in the existing user by generating
|
||||||
|
// an access token.
|
||||||
|
if (! $user) {
|
||||||
|
$token = EmailToken::generate($email);
|
||||||
|
$token->save();
|
||||||
|
|
||||||
|
$payload = compact('email', 'username');
|
||||||
|
|
||||||
|
$payload['token'] = $token->id;
|
||||||
|
} else {
|
||||||
|
$accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id));
|
||||||
|
|
||||||
|
$payload = ['authenticated' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = sprintf('<script>
|
||||||
|
window.opener.app.authenticationComplete(%s);
|
||||||
|
window.close();
|
||||||
|
</script>', json_encode($payload));
|
||||||
|
|
||||||
|
$response = new HtmlResponse($content);
|
||||||
|
|
||||||
|
if (isset($accessToken)) {
|
||||||
|
$response = $this->withRememberCookie($response, $accessToken->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
@@ -47,7 +47,7 @@ class LoginAction extends Action
|
|||||||
/**
|
/**
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param array $routeParams
|
* @param array $routeParams
|
||||||
* @return \Psr\Http\Message\ResponseInterface|EmptyResponse
|
* @return JsonResponse|EmptyResponse
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, array $routeParams = [])
|
public function handle(Request $request, array $routeParams = [])
|
||||||
{
|
{
|
||||||
|
80
src/Forum/Actions/RegisterAction.php
Normal file
80
src/Forum/Actions/RegisterAction.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Forum\Actions;
|
||||||
|
|
||||||
|
use Flarum\Api\Client;
|
||||||
|
use Flarum\Api\AccessToken;
|
||||||
|
use Flarum\Events\UserLoggedIn;
|
||||||
|
use Flarum\Support\Action;
|
||||||
|
use Flarum\Api\Commands\GenerateAccessToken;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
class RegisterAction extends Action
|
||||||
|
{
|
||||||
|
use WritesRememberCookie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Dispatcher
|
||||||
|
*/
|
||||||
|
protected $bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $apiClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Dispatcher $bus
|
||||||
|
* @param Client $apiClient
|
||||||
|
*/
|
||||||
|
public function __construct(Dispatcher $bus, Client $apiClient)
|
||||||
|
{
|
||||||
|
$this->bus = $bus;
|
||||||
|
$this->apiClient = $apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param array $routeParams
|
||||||
|
*
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, array $routeParams = [])
|
||||||
|
{
|
||||||
|
$params = ['data' => ['attributes' => $request->getAttributes()]];
|
||||||
|
|
||||||
|
$apiResponse = $this->apiClient->send(app('flarum.actor'), 'Flarum\Api\Actions\Users\CreateAction', $params);
|
||||||
|
|
||||||
|
$body = $apiResponse->getBody();
|
||||||
|
$statusCode = $apiResponse->getStatusCode();
|
||||||
|
|
||||||
|
$response = new JsonResponse($body, $statusCode);
|
||||||
|
|
||||||
|
if (! empty($body->data->attributes->isActivated)) {
|
||||||
|
$token = $this->bus->dispatch(new GenerateAccessToken($body->data->id));
|
||||||
|
|
||||||
|
// Extend the token's expiry to 2 weeks so that we can set a
|
||||||
|
// remember cookie
|
||||||
|
AccessToken::where('id', $token->id)->update(['expires_at' => new DateTime('+2 weeks')]);
|
||||||
|
|
||||||
|
return $this->withRememberCookie(
|
||||||
|
$response,
|
||||||
|
$token->id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
@@ -24,6 +24,7 @@ trait WritesRememberCookie
|
|||||||
SetCookie::create('flarum_remember', $token)
|
SetCookie::create('flarum_remember', $token)
|
||||||
->withMaxAge(14 * 24 * 60 * 60)
|
->withMaxAge(14 * 24 * 60 * 60)
|
||||||
->withPath('/')
|
->withPath('/')
|
||||||
|
->withHttpOnly(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ trait WritesRememberCookie
|
|||||||
SetCookie::create('flarum_remember')
|
SetCookie::create('flarum_remember')
|
||||||
->withMaxAge(-2628000)
|
->withMaxAge(-2628000)
|
||||||
->withPath('/')
|
->withPath('/')
|
||||||
|
->withHttpOnly(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -102,6 +102,12 @@ class ForumServiceProvider extends ServiceProvider
|
|||||||
$this->action('Flarum\Forum\Actions\LoginAction')
|
$this->action('Flarum\Forum\Actions\LoginAction')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$routes->post(
|
||||||
|
'/register',
|
||||||
|
'flarum.forum.register',
|
||||||
|
$this->action('Flarum\Forum\Actions\RegisterAction')
|
||||||
|
);
|
||||||
|
|
||||||
$routes->get(
|
$routes->get(
|
||||||
'/confirm/{token}',
|
'/confirm/{token}',
|
||||||
'flarum.forum.confirmEmail',
|
'flarum.forum.confirmEmail',
|
||||||
|
@@ -36,14 +36,46 @@ class LoginWithCookie implements MiddlewareInterface
|
|||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
if (($token = array_get($request->getCookieParams(), 'flarum_remember')) &&
|
$this->logIn($request);
|
||||||
($accessToken = AccessToken::valid($token))
|
|
||||||
) {
|
|
||||||
$this->app->instance('flarum.actor', $user = $accessToken->user);
|
|
||||||
|
|
||||||
$user->updateLastSeen()->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out ? $out($request, $response) : $response;
|
return $out ? $out($request, $response) : $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the application's actor instance according to the request token.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function logIn(Request $request)
|
||||||
|
{
|
||||||
|
if ($token = $this->getToken($request)) {
|
||||||
|
if (! $token->isValid()) {
|
||||||
|
// TODO: https://github.com/flarum/core/issues/253
|
||||||
|
} elseif ($token->user) {
|
||||||
|
$this->app->instance('flarum.actor', $user = $token->user);
|
||||||
|
|
||||||
|
$user->updateLastSeen()->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the access token referred to by the request cookie.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return AccessToken|null
|
||||||
|
*/
|
||||||
|
protected function getToken(Request $request)
|
||||||
|
{
|
||||||
|
$token = array_get($request->getCookieParams(), 'flarum_remember');
|
||||||
|
|
||||||
|
if ($token) {
|
||||||
|
return AccessToken::find($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,14 @@
|
|||||||
namespace Flarum\Install\Console;
|
namespace Flarum\Install\Console;
|
||||||
|
|
||||||
use Flarum\Console\Command;
|
use Flarum\Console\Command;
|
||||||
|
use Flarum\Core\Exceptions\ValidationException;
|
||||||
use Flarum\Core\Model;
|
use Flarum\Core\Model;
|
||||||
use Flarum\Core\Users\User;
|
use Flarum\Core\Users\User;
|
||||||
use Flarum\Core\Groups\Group;
|
use Flarum\Core\Groups\Group;
|
||||||
use Flarum\Core\Groups\Permission;
|
use Flarum\Core\Groups\Permission;
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Validation\Factory;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
@@ -100,6 +102,43 @@ class InstallCommand extends Command
|
|||||||
protected function install()
|
protected function install()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$this->dbConfig = $this->dataSource->getDatabaseConfiguration();
|
||||||
|
|
||||||
|
$validation = $this->getValidator()->make(
|
||||||
|
$this->dbConfig,
|
||||||
|
[
|
||||||
|
'driver' => 'required|in:mysql',
|
||||||
|
'host' => 'required',
|
||||||
|
'database' => 'required|alpha_dash',
|
||||||
|
'username' => 'required|alpha_dash',
|
||||||
|
'prefix' => 'alpha_dash|max:10'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validation->fails()) {
|
||||||
|
throw new Exception(implode("\n", call_user_func_array('array_merge', $validation->getMessageBag()->toArray())));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->baseUrl = $this->dataSource->getBaseUrl();
|
||||||
|
$this->settings = $this->dataSource->getSettings();
|
||||||
|
$this->adminUser = $admin = $this->dataSource->getAdminUser();
|
||||||
|
|
||||||
|
if (strlen($admin['password']) < 8) {
|
||||||
|
throw new Exception('Password must be at least 8 characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($admin['password'] !== $admin['password_confirmation']) {
|
||||||
|
throw new Exception('The password did not match its confirmation.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception('You must enter a valid email.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i', $admin['username'])) {
|
||||||
|
throw new Exception('Username can only contain letters, numbers, underscores, and dashes.');
|
||||||
|
}
|
||||||
|
|
||||||
$this->storeConfiguration();
|
$this->storeConfiguration();
|
||||||
|
|
||||||
$this->runMigrations();
|
$this->runMigrations();
|
||||||
@@ -127,7 +166,7 @@ class InstallCommand extends Command
|
|||||||
|
|
||||||
protected function storeConfiguration()
|
protected function storeConfiguration()
|
||||||
{
|
{
|
||||||
$dbConfig = $this->dataSource->getDatabaseConfiguration();
|
$dbConfig = $this->dbConfig;
|
||||||
|
|
||||||
$config = [
|
$config = [
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
@@ -142,7 +181,7 @@ class InstallCommand extends Command
|
|||||||
'prefix' => $dbConfig['prefix'],
|
'prefix' => $dbConfig['prefix'],
|
||||||
'strict' => false
|
'strict' => false
|
||||||
],
|
],
|
||||||
'url' => $this->dataSource->getBaseUrl(),
|
'url' => $this->baseUrl,
|
||||||
'paths' => [
|
'paths' => [
|
||||||
'api' => 'api',
|
'api' => 'api',
|
||||||
'admin' => 'admin',
|
'admin' => 'admin',
|
||||||
@@ -186,12 +225,11 @@ class InstallCommand extends Command
|
|||||||
|
|
||||||
protected function writeSettings()
|
protected function writeSettings()
|
||||||
{
|
{
|
||||||
$data = $this->dataSource->getSettings();
|
|
||||||
$settings = $this->application->make('Flarum\Core\Settings\SettingsRepository');
|
$settings = $this->application->make('Flarum\Core\Settings\SettingsRepository');
|
||||||
|
|
||||||
$this->info('Writing default settings');
|
$this->info('Writing default settings');
|
||||||
|
|
||||||
foreach ($data as $k => $v) {
|
foreach ($this->settings as $k => $v) {
|
||||||
$settings->set($k, $v);
|
$settings->set($k, $v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +284,7 @@ class InstallCommand extends Command
|
|||||||
|
|
||||||
protected function createAdminUser()
|
protected function createAdminUser()
|
||||||
{
|
{
|
||||||
$admin = $this->dataSource->getAdminUser();
|
$admin = $this->adminUser;
|
||||||
|
|
||||||
if ($admin['password'] !== $admin['password_confirmation']) {
|
if ($admin['password'] !== $admin['password_confirmation']) {
|
||||||
throw new Exception('The password did not match its confirmation.');
|
throw new Exception('The password did not match its confirmation.');
|
||||||
@@ -254,11 +292,13 @@ class InstallCommand extends Command
|
|||||||
|
|
||||||
$this->info('Creating admin user '.$admin['username']);
|
$this->info('Creating admin user '.$admin['username']);
|
||||||
|
|
||||||
User::unguard();
|
$user = User::register(
|
||||||
|
$admin['username'],
|
||||||
|
$admin['email'],
|
||||||
|
$admin['password']
|
||||||
|
);
|
||||||
|
|
||||||
$user = new User(Arr::except($admin, 'password_confirmation'));
|
|
||||||
$user->is_activated = 1;
|
$user->is_activated = 1;
|
||||||
$user->join_time = time();
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
$user->groups()->sync([1]);
|
$user->groups()->sync([1]);
|
||||||
@@ -300,6 +340,14 @@ class InstallCommand extends Command
|
|||||||
return $this->application->make('Flarum\Install\Prerequisites\Prerequisite');
|
return $this->application->make('Flarum\Install\Prerequisites\Prerequisite');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Contracts\Validation\Factory
|
||||||
|
*/
|
||||||
|
protected function getValidator()
|
||||||
|
{
|
||||||
|
return new Factory($this->application->make('Symfony\Component\Translation\TranslatorInterface'));
|
||||||
|
}
|
||||||
|
|
||||||
protected function showErrors($errors)
|
protected function showErrors($errors)
|
||||||
{
|
{
|
||||||
foreach ($errors as $error) {
|
foreach ($errors as $error) {
|
||||||
|
@@ -162,7 +162,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>
|
<h1>
|
||||||
<?php include 'logo.svg'; ?>
|
<?php echo file_get_contents(__DIR__ . '/logo.svg'); ?>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="animated fadeIn">
|
<div class="animated fadeIn">
|
||||||
|
Reference in New Issue
Block a user