From 92afd296509de82e7550f67064b032db916b1f63 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 26 Aug 2016 19:12:19 +0200 Subject: [PATCH] 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 --- Dockerfile | 4 +- .../javascripts/components/actions/meta.jsx | 8 +++ .../components/actions/statuses.jsx | 56 +++++++++++++++++-- .../components/components/composer_drawer.jsx | 1 + .../components/containers/root.jsx | 11 ++++ .../javascripts/components/reducers/index.jsx | 4 +- .../javascripts/components/reducers/meta.jsx | 13 +++++ app/controllers/api_controller.rb | 8 +++ app/controllers/home_controller.rb | 7 +++ app/views/home/index.html.haml | 2 +- config/environments/development.rb | 2 + config/initializers/doorkeeper.rb | 8 +-- ...5805_add_superapp_to_oauth_applications.rb | 5 ++ db/schema.rb | 13 +++-- db/seeds.rb | 9 +-- package.json | 1 + 16 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 app/assets/javascripts/components/actions/meta.jsx create mode 100644 app/assets/javascripts/components/reducers/meta.jsx create mode 100644 db/migrate/20160826155805_add_superapp_to_oauth_applications.rb diff --git a/Dockerfile b/Dockerfile index 2b15d437b..3acef6e13 100644 --- 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 new file mode 100644 index 000000000..1b64db1c2 --- /dev/null +++ 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 index fece257d5..45d62fab2 100644 --- 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 index d33e28219..7b742f64e 100644 --- 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 index 7da984d89..661ffb22c 100644 --- 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 index c7e858f38..96c026c8c 100644 --- 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 new file mode 100644 index 000000000..401be435d --- /dev/null +++ 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 index 8a2712476..bacdd997b 100644 --- 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 index 57973ba49..f159c3df8 100644 --- 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 index 941397384..9279ae9ae 100644 --- 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 index c51d98543..288256dcf 100644 --- 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 index cf320c557..0d6574d9f 100644 --- 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 new file mode 100644 index 000000000..a1b92fada --- /dev/null +++ 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 index 45ab70855..b63df0fea 100644 --- 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 index 4edb1e857..c2bf6a16e 100644 --- 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 index 19f0ac016..4b5a615fa 100644 --- 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",