commit: 92afd296509de82e7550f67064b032db916b1f63
parent: 44e57f64dd8b00900c31d7fd56fda94f4e69e986
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 26 Aug 2016 19:12:19 +0200
The frontend will now be an OAuth app, auto-authorized. The frontend will use an access token for API requests
Adding better errors for the API controllers, posting a simple status works from the frontend now
Diffstat:
16 files changed, 127 insertions(+), 25 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -9,9 +9,9 @@ WORKDIR /mastodon
ADD Gemfile /mastodon/Gemfile
ADD Gemfile.lock /mastodon/Gemfile.lock
-ADD package.json /mastodon/package.json
-
RUN bundle install --deployment --without test development
+
+ADD package.json /mastodon/package.json
RUN npm install
ADD . /mastodon
diff --git a/app/assets/javascripts/components/actions/meta.jsx b/app/assets/javascripts/components/actions/meta.jsx
@@ -0,0 +1,8 @@
+export const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN';
+
+export function setAccessToken(token) {
+ return {
+ type: SET_ACCESS_TOKEN,
+ token: token
+ };
+}
diff --git a/app/assets/javascripts/components/actions/statuses.jsx b/app/assets/javascripts/components/actions/statuses.jsx
@@ -2,7 +2,11 @@ import fetch from 'isomorphic-fetch'
export const SET_TIMELINE = 'SET_TIMELINE';
export const ADD_STATUS = 'ADD_STATUS';
-export const PUBLISH = 'PUBLISH';
+
+export const PUBLISH = 'PUBLISH';
+export const PUBLISH_START = 'PUBLISH_START';
+export const PUBLISH_SUCC = 'PUBLISH_SUCC';
+export const PUBLISH_ERROR = 'PUBLISH_ERROR';
export function setTimeline(timeline, statuses) {
return {
@@ -20,14 +24,58 @@ export function addStatus(timeline, status) {
};
}
+export function publishStart() {
+ return {
+ type: PUBLISH_START
+ };
+}
+
+export function publishError(error) {
+ return {
+ type: PUBLISH_ERROR,
+ error: error
+ };
+}
+
+export function publishSucc(status) {
+ return {
+ type: PUBLISH_SUCC,
+ status: status
+ };
+}
+
export function publish(text, in_reply_to_id) {
- return function (dispatch) {
+ return function (dispatch, getState) {
+ const access_token = getState().getIn(['meta', 'access_token']);
+
+ var data = new FormData();
+
+ data.append('status', text);
+
+ if (in_reply_to_id !== null) {
+ data.append('in_reply_to_id', in_reply_to_id);
+ }
+
+ dispatch(publishStart());
+
return fetch('/api/statuses', {
- method: 'POST'
+ method: 'POST',
+
+ headers: {
+ 'Authorization': `Bearer ${access_token}`
+ },
+
+ body: data
}).then(function (response) {
return response.json();
}).then(function (json) {
- console.log(json);
+ if (json.error) {
+ dispatch(publishError(json.error));
+ } else {
+ dispatch(publishSucc(json));
+ }
+ }).catch(function (error) {
+ dispatch(publishError(error));
});
};
}
diff --git a/app/assets/javascripts/components/components/composer_drawer.jsx b/app/assets/javascripts/components/components/composer_drawer.jsx
@@ -26,6 +26,7 @@ const ComposerDrawer = React.createClass({
handleSubmit () {
this.props.onSubmit(this.state.text, null);
+ this.setState({ text: '' });
},
render () {
diff --git a/app/assets/javascripts/components/containers/root.jsx b/app/assets/javascripts/components/containers/root.jsx
@@ -2,12 +2,23 @@ import { Provider } from 'react-redux';
import configureStore from '../store/configureStore';
import Frontend from '../components/frontend';
import { setTimeline, addStatus } from '../actions/statuses';
+import { setAccessToken } from '../actions/meta';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
const store = configureStore();
const Root = React.createClass({
+ propTypes: {
+ token: React.PropTypes.string.isRequired,
+ timelines: React.PropTypes.array
+ },
+
+ mixins: [PureRenderMixin],
+
componentWillMount() {
+ store.dispatch(setAccessToken(this.props.token));
+
for (var timelineType in this.props.timelines) {
if (this.props.timelines.hasOwnProperty(timelineType)) {
store.dispatch(setTimeline(timelineType, JSON.parse(this.props.timelines[timelineType])));
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
@@ -1,6 +1,8 @@
import { combineReducers } from 'redux-immutable';
import statuses from './statuses';
+import meta from './meta';
export default combineReducers({
- statuses
+ statuses,
+ meta
});
diff --git a/app/assets/javascripts/components/reducers/meta.jsx b/app/assets/javascripts/components/reducers/meta.jsx
@@ -0,0 +1,13 @@
+import { SET_ACCESS_TOKEN } from '../actions/meta';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map();
+
+export default function meta(state = initialState, action) {
+ switch(action.type) {
+ case SET_ACCESS_TOKEN:
+ return state.set('access_token', action.token);
+ default:
+ return state;
+ }
+}
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
@@ -2,6 +2,14 @@ class ApiController < ApplicationController
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token
+ rescue_from ActiveRecord::RecordInvalid do
+ render json: { error: 'Record invalid' }, status: 422
+ end
+
+ rescue_from ActiveRecord::RecordNotFound do
+ render json: { error: 'Record not found' }, status: 404
+ end
+
protected
def current_resource_owner
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
@@ -5,5 +5,12 @@ class HomeController < ApplicationController
@body_classes = 'app-body'
@home = Feed.new(:home, current_user.account).get(20)
@mentions = Feed.new(:mentions, current_user.account).get(20)
+ @token = find_or_create_access_token.token
+ end
+
+ private
+
+ def find_or_create_access_token
+ Doorkeeper::AccessToken.find_or_create_for(Doorkeeper::Application.where(superapp: true).first, current_user.id, nil, Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled?)
end
end
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
@@ -1 +1 @@
-= react_component 'Root', { timelines: { home: render(file: 'api/statuses/home', locals: { statuses: @home }, formats: :json), mentions: render(file: 'api/statuses/mentions', locals: { statuses: @mentions }, formats: :json) }}, class: 'app-holder', prerender: false
+= react_component 'Root', { token: @token, timelines: { home: render(file: 'api/statuses/home', locals: { statuses: @home }, formats: :json), mentions: render(file: 'api/statuses/mentions', locals: { statuses: @mentions }, formats: :json) }}, class: 'app-holder', prerender: false
diff --git a/config/environments/development.rb b/config/environments/development.rb
@@ -62,6 +62,8 @@ Rails.application.configure do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
+
+ Bullet.add_whitelist type: :n_plus_one_query, class_name: 'User', association: :account
end
config.react.variant = :development
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
@@ -4,7 +4,7 @@ Doorkeeper.configure do
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
- current_user || warden.authenticate!(scope: :user)
+ current_user || redirect_to(new_user_session_url)
end
resource_owner_from_credentials do |routes|
@@ -100,9 +100,9 @@ Doorkeeper.configure do
# Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step.
# For example if dealing with a trusted application.
- # skip_authorization do |resource_owner, client|
- # client.superapp? or resource_owner.admin?
- # end
+ skip_authorization do |resource_owner, client|
+ client.superapp?
+ end
# WWW-Authenticate Realm (default "Doorkeeper").
# realm "Doorkeeper"
diff --git a/db/migrate/20160826155805_add_superapp_to_oauth_applications.rb b/db/migrate/20160826155805_add_superapp_to_oauth_applications.rb
@@ -0,0 +1,5 @@
+class AddSuperappToOauthApplications < ActiveRecord::Migration[5.0]
+ def change
+ add_column :oauth_applications, :superapp, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160325130944) do
+ActiveRecord::Schema.define(version: 20160826155805) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -94,15 +94,16 @@ ActiveRecord::Schema.define(version: 20160325130944) do
end
create_table "oauth_applications", force: :cascade do |t|
- t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
- t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
+ t.string "name", null: false
+ t.string "uid", null: false
+ t.string "secret", null: false
+ t.text "redirect_uri", null: false
+ t.string "scopes", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "owner_id"
t.string "owner_type"
+ t.boolean "superapp", default: false, null: false
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
end
diff --git a/db/seeds.rb b/db/seeds.rb
@@ -1,7 +1,2 @@
-# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
-#
-# Examples:
-#
-# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
-# Mayor.create(name: 'Emanuel', city: cities.first)
+web_app = Doorkeeper::Application.new(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri)
+web_app.save(validate: false)
diff --git a/package.json b/package.json
@@ -15,6 +15,7 @@
"immutable": "^3.8.1",
"isomorphic-fetch": "^2.2.1",
"moment": "^2.14.1",
+ "react-addons-pure-render-mixin": "^15.3.1",
"react-immutable-proptypes": "^2.1.0",
"react-redux": "^4.4.5",
"redux": "^3.5.2",