diff --git a/.env.production.sample b/.env.production.sample index 83ab698a096583..5362aea59964c3 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -92,3 +92,6 @@ SMTP_FROM_ADDRESS=notifications@example.com STREAMING_CLUSTER_NUM=1 SENTRY_DSN= + +QIITA_CLIENT_ID= +QIITA_CLIENT_SECRET= diff --git a/.env.sample b/.env.sample index 3c0a0c5bfe453f..8eb24aae99536a 100644 --- a/.env.sample +++ b/.env.sample @@ -19,3 +19,6 @@ OTP_SECRET=RANDOM_STRING # Cluster number setting for streaming API server. # If you comment out following line, cluster number will be `numOfCpuCores - 1`. STREAMING_CLUSTER_NUM=1 + +QIITA_CLIENT_ID= +QIITA_CLIENT_SECRET= diff --git a/Gemfile b/Gemfile index e341c0fe48d87e..9001a282a293ef 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,8 @@ gem 'link_header' gem 'local_time' gem 'nokogiri' gem 'oj' +gem 'omniauth' +gem 'omniauth_qiita' gem 'ostatus2', '~> 2.0' gem 'ox' gem 'rabl' diff --git a/Gemfile.lock b/Gemfile.lock index 236344562d47b7..a019b97303b388 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -159,7 +159,7 @@ GEM fabrication (2.16.1) faker (1.7.3) i18n (~> 0.5) - faraday (0.12.1) + faraday (0.11.0) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) font-awesome-rails (4.7.0.1) @@ -183,6 +183,7 @@ GEM hamlit (>= 1.2.0) railties (>= 4.0.1) hashdiff (0.3.2) + hashie (3.5.5) highline (1.7.8) hiredis (0.6.1) htmlentities (4.3.4) @@ -216,6 +217,7 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.1.0) + jwt (1.5.6) kaminari (1.0.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.0.1) @@ -258,6 +260,8 @@ GEM mimemagic (0.3.2) mini_portile2 (2.1.0) minitest (5.10.1) + multi_json (1.12.1) + multi_xml (0.6.0) multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) @@ -267,7 +271,22 @@ GEM mini_portile2 (~> 2.1.0) nokogumbo (1.4.10) nokogiri + oauth2 (1.3.1) + faraday (>= 0.8, < 0.12) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) oj (3.0.2) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) + omniauth_qiita (0.1.0) + multi_json (~> 1.10) + omniauth-oauth2 (~> 1.2) openssl (2.0.3) orm_adapter (0.5.0) ostatus2 (2.0.0) @@ -537,6 +556,8 @@ DEPENDENCIES microformats2 nokogiri oj + omniauth + omniauth_qiita ostatus2 (~> 2.0) ox paperclip (~> 5.1) diff --git a/app/assets/javascripts/components/features/ui/components/alert_bar.jsx b/app/assets/javascripts/components/features/ui/components/alert_bar.jsx new file mode 100644 index 00000000000000..a3c8e19117c271 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/alert_bar.jsx @@ -0,0 +1,27 @@ +import { Link } from 'react-router'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class AlertBar extends React.Component { + + render () { + const { isEmailConfirmed } = this.props; + + return ( +
+ { + (!isEmailConfirmed && +
+ +
+ ) + } +
+ ); + } + +} + +AlertBar.propTypes = { + isEmailConfirmed: PropTypes.bool.isRequired, +}; diff --git a/app/assets/javascripts/components/features/ui/containers/alert_bar_container.jsx b/app/assets/javascripts/components/features/ui/containers/alert_bar_container.jsx new file mode 100644 index 00000000000000..786b97040ed21a --- /dev/null +++ b/app/assets/javascripts/components/features/ui/containers/alert_bar_container.jsx @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import AlertBar from '../components/alert_bar'; + +const mapStateToProps = state => ({ + isEmailConfirmed: state.getIn(['meta', 'is_email_confirmed']), +}); + +export default connect(mapStateToProps)(AlertBar); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index b402639ce6d196..6dc6f8c85a5cfb 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -5,6 +5,7 @@ import LoadingBarContainer from './containers/loading_bar_container'; import HomeTimeline from '../home_timeline'; import Compose from '../compose'; import TabsBar from './components/tabs_bar'; +import AlertBarContainer from './containers/alert_bar_container'; import ModalContainer from './containers/modal_container'; import Notifications from '../notifications'; import { connect } from 'react-redux'; @@ -144,6 +145,7 @@ class UI extends React.PureComponent { return (
+ {mountedColumns} diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index afe714cac16d8b..fcb47cf2de3c8a 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -24,6 +24,7 @@ const en = { "account.unblock": "Unblock @{name}", "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", + "alert_bar.email_confirm_alert": "Your email address is not confirmed. Please confirm the sent email in 24 hours after registration.", "boost_modal.combo": "You can press {combo} to skip this next time", "column.blocks": "Blocked users", "column.community": "Local timeline", diff --git a/app/assets/javascripts/components/locales/ja.jsx b/app/assets/javascripts/components/locales/ja.jsx index 6a753652702477..802b542259c55f 100644 --- a/app/assets/javascripts/components/locales/ja.jsx +++ b/app/assets/javascripts/components/locales/ja.jsx @@ -14,6 +14,7 @@ const ja = { "account.unblock": "ブロック解除", "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", + "alert_bar.email_confirm_alert": "メールアドレスの確認が完了していません。登録後24時間以内に、登録したアドレス宛に送信されたメールを確認してください。", "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", "column.blocks": "ブロックしたユーザー", "column.community": "ローカルタイムライン", diff --git a/app/assets/stylesheets/alert.scss b/app/assets/stylesheets/alert.scss new file mode 100644 index 00000000000000..1de89944623079 --- /dev/null +++ b/app/assets/stylesheets/alert.scss @@ -0,0 +1,11 @@ +.alert-bar { + .alert { + border: 1px solid transparent; + border-radius: 4px; + padding: 12px; + + color: DarkGoldenrod; + background-color: Beige; + border-color: Goldenrod; + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5becf213d61ad2..a7e035ab5a046b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -21,4 +21,6 @@ @import 'rtl'; @import 'themes'; +@import 'alert'; @import 'code'; +@import 'qiita'; diff --git a/app/assets/stylesheets/qiita.scss b/app/assets/stylesheets/qiita.scss new file mode 100644 index 00000000000000..76733d5ee9493d --- /dev/null +++ b/app/assets/stylesheets/qiita.scss @@ -0,0 +1,56 @@ +.qiita { + .button { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + border-radius: 3px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .button-primary { + border-color: #4ea30a; + background-color: #59bb0c; + color: #fff; + } + + .registration { + width: 100%; + text-transform: none; + } +} + +.registrations { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .qiita { + width: 300px; + margin: 5px auto; + text-align: center; + } + + .password_registration_closed_caution { + width: 300px; + margin: 10px auto; + } + + .info a { + color: white; + text-transform: none; + } +} diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index fc9064068c3612..2cc3f416c6fe16 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -2,7 +2,7 @@ module Admin class SettingsController < BaseController - BOOLEAN_SETTINGS = %w(open_registrations).freeze + BOOLEAN_SETTINGS = %w(open_registrations prohibit_registrations_except_qiita_oauth).freeze def index @settings = Setting.all_as_records diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb new file mode 100644 index 00000000000000..8809e72df9634c --- /dev/null +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController + def qiita + auth_hash = request.env['omniauth.auth'] + + if current_user + authorization = QiitaAuthorization.find_or_initialize_by(uid: auth_hash[:uid]) do |qiita_authorization| + authorization.user = current_user + end + + if authorization.save + flash[:notice] = I18n.t('omniauth_callbacks.success') + else + flash[:alert] = I18n.t('omniauth_callbacks.failure') + end + redirect_to settings_qiita_authorizations_path + else + if authorization = QiitaAuthorization.find_by(uid: auth_hash[:uid]) + sign_in(authorization.user) + redirect_to web_path + else + store_omniauth_auth + redirect_to new_user_oauth_registration_path + end + end + end + + private + + def store_omniauth_auth + session[:devise_omniauth_auth] = request.env['omniauth.auth'] + end +end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index dd30be32a40e98..410418f2f1c5a0 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -29,7 +29,15 @@ def after_inactive_sign_up_path_for(_resource) end def check_enabled_registrations - redirect_to root_path if single_user_mode? || !Setting.open_registrations + redirect_to root_path if single_user_mode? || !Setting.open_registrations || Setting.prohibit_registrations_except_qiita_oauth + end + + def update_resource(resource, params) + if resource.try(:has_dummy_password?) + resource.update_without_current_password(params) + else + super + end end private diff --git a/app/controllers/oauth_registrations_controller.rb b/app/controllers/oauth_registrations_controller.rb new file mode 100644 index 00000000000000..7184d9457ac76f --- /dev/null +++ b/app/controllers/oauth_registrations_controller.rb @@ -0,0 +1,40 @@ +class OauthRegistrationsController < DeviseController + layout 'auth' + + before_action :check_enabled_registrations + before_action :require_omniauth_auth + before_action :require_no_authentication + + def new + @oauth_registration = Form::OauthRegistration.from_omniauth_auth(omniauth_auth) + end + + def create + @oauth_registration = Form::OauthRegistration.from_omniauth_auth(omniauth_auth) + @oauth_registration.assign_attributes( + params.require(:form_oauth_registration).permit(:email, :username, :password, :password_confirmation).merge(locale: I18n.locale) + ) + + if @oauth_registration.save + sign_in(@oauth_registration.user) + redirect_to web_path + flash[:notice] = I18n.t('oauth_registration.success') + else + render :new, status: :unprocessable_entity + end + end + + private + + def omniauth_auth + @omniauth_auth ||= session[:devise_omniauth_auth].try(:deep_symbolize_keys) + end + + def check_enabled_registrations + redirect_to root_path if single_user_mode? || !Setting.open_registrations + end + + def require_omniauth_auth + redirect_to root_path unless omniauth_auth + end +end diff --git a/app/controllers/settings/qiita_authorizations_controller.rb b/app/controllers/settings/qiita_authorizations_controller.rb new file mode 100644 index 00000000000000..ef55e2886a087d --- /dev/null +++ b/app/controllers/settings/qiita_authorizations_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Settings::QiitaAuthorizationsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + + def show + @account = current_account + @qiita_authorization = current_account.user.qiita_authorization + end +end diff --git a/app/models/form/oauth_registration.rb b/app/models/form/oauth_registration.rb new file mode 100644 index 00000000000000..0bdea4e7932488 --- /dev/null +++ b/app/models/form/oauth_registration.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class Form::OauthRegistration + include ActiveModel::Model + + attr_accessor :user, :provider, :locale, :avatar, :email, :uid, :username, :token + validate :validate_user + + class UnsupportedProviderError < StandardError; end + + class << self + def from_omniauth_auth(auth) + case auth[:provider] + when 'qiita' + new( + provider: auth[:provider], + avatar: auth[:info][:image], + uid: auth[:uid], + username: normalize_username(auth[:uid]), + token: auth[:credentials][:token], + ) + else + fail UnsupportedProviderError + end + end + + private + + def normalize_username(username) + username.to_s.downcase.tr('-', '_').gsub('@github', '').remove(/[^a-z0-9_]/i) + end + end + + def save + return false if invalid? + + ApplicationRecord.transaction do + self.user = build_user + oauth_authentication = build_authorization(user) + user.save! && oauth_authentication.save! + end + + true + rescue ActiveRecord::RecordInvalid + false + end + + private + + def validate_user + user = build_user + user.valid? + + [user, user.account].each do |record| + record.errors.each do |key, value| + errors.add(key, value) if respond_to?(key) + end + end + end + + def build_user + password = SecureRandom.base64 + + User.new( + email: email, + locale: locale, + password: password, + password_confirmation: password, + dummy_password_flag: true, + account_attributes: { + username: username, + avatar: avatar + }, + ) + end + + def build_authorization(user) + case provider + when 'qiita' + QiitaAuthorization.new( + uid: uid, + user: user, + token: token, + ) + else + fail UnsupportedProviderError + end + end +end diff --git a/app/models/qiita_authorization.rb b/app/models/qiita_authorization.rb new file mode 100644 index 00000000000000..c1a0bc1787b07b --- /dev/null +++ b/app/models/qiita_authorization.rb @@ -0,0 +1,3 @@ +class QiitaAuthorization < ApplicationRecord + belongs_to :user, inverse_of: :qiita_authorization +end diff --git a/app/models/user.rb b/app/models/user.rb index 48e7b80887664e..3d174ae6f1c931 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,10 +6,12 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :two_factor_authenticatable, :two_factor_backupable, + :omniauthable, otp_secret_encryption_key: ENV['OTP_SECRET'], otp_number_of_backup_codes: 10 belongs_to :account, inverse_of: :user, required: true + has_one :qiita_authorization, inverse_of: :user, dependent: :destroy accepts_nested_attributes_for :account validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?' @@ -19,6 +21,8 @@ class User < ApplicationRecord scope :admins, -> { where(admin: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } + before_validation :disable_dummy_password_flag, on: :update, if: :encrypted_password_changed? + def confirmed? confirmed_at.present? end @@ -38,4 +42,25 @@ def setting_boost_modal def setting_auto_play_gif settings.auto_play_gif end + + def has_dummy_password? + dummy_password_flag + end + + def disable_dummy_password_flag + self.dummy_password_flag = false + true + end + + def update_without_current_password(params, *options) + if params[:password].blank? + params.delete(:password) + params.delete(:password_confirmation) if params[:password_confirmation].blank? + end + p params + + result = update_attributes(params, *options) + clean_up_passwords + result + end end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 9a69809d0e5841..8cc08e6c2845f5 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -5,6 +5,7 @@ class InstancePresenter :closed_registrations_message, :site_contact_email, :open_registrations, + :prohibit_registrations_except_qiita_oauth, :site_description, :site_extended_description, to: Setting @@ -29,4 +30,8 @@ def domain_count def version_number Mastodon::Version end + + def open_password_registrations + open_registrations && !prohibit_registrations_except_qiita_oauth + end end diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index c7a9a488b48efb..00e4da051d27d4 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -1,30 +1,44 @@ -= simple_form_for(new_user, url: user_registration_path) do |f| - = f.simple_fields_for :account do |account_fields| - = account_fields.input :username, - autofocus: true, - placeholder: t('simple_form.labels.defaults.username'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } +.registrations + = render 'shared/qiita_authentication' - = f.input :email, - placeholder: t('simple_form.labels.defaults.email'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, - autocomplete: "off", - placeholder: t('simple_form.labels.defaults.password'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } - = f.input :password_confirmation, - autocomplete: "off", - placeholder: t('simple_form.labels.defaults.confirm_password'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + - if @instance_presenter.open_password_registrations - .actions - = f.button :button, t('about.get_started'), type: :submit + = simple_form_for(new_user, url: user_registration_path) do |f| - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.about_this'), about_more_path + = f.simple_fields_for :account do |account_fields| + = account_fields.input :username, + autofocus: true, + placeholder: t('simple_form.labels.defaults.username'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + + = f.input :email, + placeholder: t('simple_form.labels.defaults.email'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password_confirmation, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.confirm_password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + + .actions + = f.button :button, t('about.get_started'), type: :submit + + .info + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + · + = link_to t('about.about_this'), about_more_path + - else + + .password_registration_closed_caution + = t('auth.password_signup_closed_caution') + .info + = link_to t('auth.login'), new_user_session_path + · + = link_to t('about.about_this'), about_more_path diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml index b00e75a1666400..f8cfe5da716e75 100644 --- a/app/views/admin/settings/index.html.haml +++ b/app/views/admin/settings/index.html.haml @@ -29,6 +29,10 @@ %strong= t('admin.settings.site_description_extended.title') %p= t('admin.settings.site_description_extended.desc_html') %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) + %tr + %td + %strong= t('admin.settings.prohibit_registrations_except_qiita_oauth.title') + %td= best_in_place @settings['prohibit_registrations_except_qiita_oauth'], :value, as: :checkbox, collection: { false: t('admin.settings.prohibit_registrations_except_qiita_oauth.disabled'), true: t('admin.settings.prohibit_registrations_except_qiita_oauth.enabled')}, url: admin_setting_path(@settings['prohibit_registrations_except_qiita_oauth']) %tr %td %strong= t('admin.settings.registrations.open.title') diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 39b726f9c21190..e878a5111e8205 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -5,9 +5,13 @@ = render 'shared/error_messages', object: resource = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') } - = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') } - = f.input :current_password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') } + - if current_user.has_dummy_password? + = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + - else + = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') } + = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') } + = f.input :current_password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') } .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 93b9629f1d4bdb..d5bcecb5619def 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -1,6 +1,10 @@ - content_for :page_title do = t('auth.login') += render 'shared/qiita_authentication' + +%hr + = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index b599b5cf003f06..cf490c9910ce6f 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -10,6 +10,7 @@ node(:meta) do admin: @admin.try(:id), boost_modal: current_account.user.setting_boost_modal, auto_play_gif: current_account.user.setting_auto_play_gif, + is_email_confirmed: current_user.confirmed?, } end diff --git a/app/views/oauth_registrations/new.html.haml b/app/views/oauth_registrations/new.html.haml new file mode 100644 index 00000000000000..b44ca07164541f --- /dev/null +++ b/app/views/oauth_registrations/new.html.haml @@ -0,0 +1,15 @@ +- content_for :page_title do + = t('auth.oauth_registration') + += simple_form_for(@oauth_registration, url: user_oauth_registration_path, method: 'POST') do |f| + = render 'shared/error_messages', object: f.object + + .omniauth + = f.label :username + = f.input :username, autofocus: true, placeholdoer: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + + = f.label :email + = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + + .actions + = f.button :button, t('auth.register'), type: :submit diff --git a/app/views/settings/qiita_authorizations/show.html.haml b/app/views/settings/qiita_authorizations/show.html.haml new file mode 100644 index 00000000000000..0f910576e35873 --- /dev/null +++ b/app/views/settings/qiita_authorizations/show.html.haml @@ -0,0 +1,11 @@ +- content_for :page_title do + = t('settings.qiita_authorizations') + +%table.table + %tbody + %tr + %th User ID + %th Status + %tr + %td= @qiita_authorization.try(:uid) + %td= @qiita_authorization ? '連携中' : link_to('連携する', '/auth/auth/qiita') diff --git a/app/views/settings/shared/_links.html.haml b/app/views/settings/shared/_links.html.haml index 0abb5a7abb17ae..25f1e5c1ea09b9 100644 --- a/app/views/settings/shared/_links.html.haml +++ b/app/views/settings/shared/_links.html.haml @@ -7,4 +7,6 @@ %li= link_to t('auth.change_password'), edit_user_registration_path - if controller_name != 'two_factor_authentications' %li= link_to t('settings.two_factor_authentication'), settings_two_factor_authentication_path + - if controller_name != 'qiita_authorizations' + %li= link_to t('settings.qiita_authorizations'), settings_qiita_authorizations_path %li= link_to t('settings.back'), root_path diff --git a/app/views/shared/_qiita_authentication.html.haml b/app/views/shared/_qiita_authentication.html.haml new file mode 100644 index 00000000000000..c293368cfba259 --- /dev/null +++ b/app/views/shared/_qiita_authentication.html.haml @@ -0,0 +1,2 @@ +.qiita + = link_to 'Qiitaアカウントで参加', '/auth/auth/qiita', class: 'button button-primary registration' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 4754c2c8ca22bd..fa224e4f778236 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -114,7 +114,7 @@ # able to access the website for two days without confirming their account, # access will be blocked just in the third day. Default is 0.days, meaning # the user cannot access the website without confirming their account. - # config.allow_unconfirmed_access_for = 2.days + config.allow_unconfirmed_access_for = 1.days # A period that the user is allowed to confirm their account before their # token becomes invalid. For example, if set to 3.days, the user can confirm @@ -243,6 +243,7 @@ # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :qiita, ENV['QIITA_CLIENT_ID'], ENV['QIITA_CLIENT_SECRET'], scope: 'read_qiita' # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/locales/en.yml b/config/locales/en.yml index 6f54e343ea5b65..4ef3c7d29524d3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -170,6 +170,10 @@ en: disabled: Disabled enabled: Enabled title: Open registration + prohibit_registrations_except_qiita_oauth: + title: Prohibit registration except Qiita OAuth + disabled: Don't Prohibit + enabled: Prohibit setting: Setting site_description: desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.
You can use HTML tags, in particular <a> and <em>. @@ -196,6 +200,8 @@ en: resend_confirmation: Resend confirmation instructions reset_password: Reset password set_new_password: Set new password + oauth_registration: User Registration + password_signup_closed_caution: For now, we allow registrations only with Qiita account. authorize_follow: error: Unfortunately, there was an error looking up the remote account follow: Follow @@ -304,6 +310,7 @@ en: preferences: Preferences settings: Settings two_factor_authentication: Two-factor Authentication + qiita_authorizations: Qiita Authorizations statuses: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded @@ -339,3 +346,8 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code + omniauth_callbacks: + success: Succeed to integrate + failure: Failed to integrate + oauth_registration: + success: Succeed to register diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 7a53541f123e8b..10cdf1e9bccd04 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -177,6 +177,10 @@ ja: disabled: 無効 enabled: 有効 title: 新規登録を受け付ける + prohibit_registrations_except_qiita_oauth: + title: Qiita連携以外の新規登録を禁止する + disabled: 禁止しない + enabled: 禁止する setting: 設定 site_description: desc_html: トップページへの表示と meta タグに使用されます。
HTMLタグ、特に<a><em>が利用可能です。 @@ -203,6 +207,8 @@ ja: resend_confirmation: 確認メールを再送する reset_password: パスワードを再発行 set_new_password: 新しいパスワード + oauth_registration: ユーザー登録 + password_signup_closed_caution: 現在は負荷対策のため、Qiitaアカウント連携以外での新規登録を停止しています。 authorize_follow: error: 残念ながら、リモートアカウントにエラーが発生しました。 follow: フォロー @@ -311,6 +317,7 @@ ja: preferences: ユーザー設定 settings: 設定 two_factor_authentication: 二段階認証 + qiita_authorizations: Qiita連携 statuses: open_in_web: Webで開く over_character_limit: 上限は %{max}文字までです @@ -346,3 +353,8 @@ ja: users: invalid_email: メールアドレスが無効です invalid_otp_token: 二段階認証コードが間違っています + omniauth_callbacks: + success: 連携に成功しました + failure: 連携に失敗しました + oauth_registration: + success: ユーザー登録しました diff --git a/config/navigation.rb b/config/navigation.rb index 16bc86696dff39..daeff41c0ec1c2 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -13,6 +13,7 @@ settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url + settings.item :qiita_authorizations, safe_join([fa_icon('users fw'), t('settings.qiita_authorizations')]), settings_qiita_authorizations_url end primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin| diff --git a/config/routes.rb b/config/routes.rb index 9adaffcafd92f2..e5102ef98a10ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,8 +23,15 @@ registrations: 'auth/registrations', passwords: 'auth/passwords', confirmations: 'auth/confirmations', + omniauth_callbacks: 'auth/omniauth_callbacks', } + devise_scope :user do + with_devise_exclusive_scope('/auth', :user, {}) do + resource :oauth_registration, only: [:new, :create], path: 'oauth/oauth_registrations' + end + end + get '/users/:username', to: redirect('/@%{username}'), constraints: { format: :html } resources :accounts, path: 'users', only: [:show], param: :username do @@ -65,6 +72,7 @@ end resource :follower_domains, only: [:show, :update] + resource :qiita_authorizations, only: [:show] end resources :media, only: [:show] diff --git a/config/settings.yml b/config/settings.yml index 9813963b28ec15..cf5a996d1e9855 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -14,6 +14,7 @@ defaults: &defaults site_contact_email: '' open_registrations: true closed_registrations_message: '' + prohibit_registrations_except_qiita_oauth: false boost_modal: false auto_play_gif: true notification_emails: diff --git a/db/migrate/20170504103736_create_qiita_authorizations.rb b/db/migrate/20170504103736_create_qiita_authorizations.rb new file mode 100644 index 00000000000000..50bea7045540b6 --- /dev/null +++ b/db/migrate/20170504103736_create_qiita_authorizations.rb @@ -0,0 +1,12 @@ +class CreateQiitaAuthorizations < ActiveRecord::Migration[5.0] + def change + create_table :qiita_authorizations do |t| + t.belongs_to :user, foreign_key: true + t.string :uid + t.string :token + + t.index :uid, unique: true + t.timestamps + end + end +end diff --git a/db/migrate/20170517123337_add_dummy_password_flag_to_user.rb b/db/migrate/20170517123337_add_dummy_password_flag_to_user.rb new file mode 100644 index 00000000000000..db88026169b57f --- /dev/null +++ b/db/migrate/20170517123337_add_dummy_password_flag_to_user.rb @@ -0,0 +1,5 @@ +class AddDummyPasswordFlagToUser < ActiveRecord::Migration[5.0] + def change + add_column :users, :dummy_password_flag, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 66326f2e208938..8d700d8d6c12ae 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: 20170425202925) do +ActiveRecord::Schema.define(version: 20170517123337) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -214,6 +214,16 @@ t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree end + create_table "qiita_authorizations", force: :cascade do |t| + t.integer "user_id" + t.string "uid" + t.string "token" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["uid"], name: "index_qiita_authorizations_on_uid", unique: true, using: :btree + t.index ["user_id"], name: "index_qiita_authorizations_on_user_id", using: :btree + end + create_table "reports", force: :cascade do |t| t.integer "account_id", null: false t.integer "target_account_id", null: false @@ -326,6 +336,7 @@ t.boolean "otp_required_for_login" t.datetime "last_emailed_at" t.string "otp_backup_codes", array: true + t.boolean "dummy_password_flag", default: false, null: false t.index ["account_id"], name: "index_users_on_account_id", using: :btree t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree t.index ["email"], name: "index_users_on_email", unique: true, using: :btree @@ -340,5 +351,6 @@ t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true, using: :btree end + add_foreign_key "qiita_authorizations", "users" add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade end diff --git a/spec/fabricators/qiita_authorization_fabricator.rb b/spec/fabricators/qiita_authorization_fabricator.rb new file mode 100644 index 00000000000000..b2b79070ea911e --- /dev/null +++ b/spec/fabricators/qiita_authorization_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:qiita_authorization) do + user nil + uid "qiitan" + provider "qiita" +end diff --git a/spec/models/oauth_authorization_spec.rb b/spec/models/oauth_authorization_spec.rb new file mode 100644 index 00000000000000..1ccc5dcc208303 --- /dev/null +++ b/spec/models/oauth_authorization_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe QiitaAuthorization, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end