Merge branch 'standalone_develop' into dev_local_v1

# Conflicts:
#	app/controllers/accounts_controller.rb
#	app/controllers/api/v1/users_controller.rb
#	app/controllers/organizations/organizations_controller.rb
#	app/services/accounts/reset_password_service.rb
#	app/services/api/v1/users/update_email_service.rb
#	app/services/api/v1/users/update_phone_service.rb
#	app/services/users/register_service.rb
This commit is contained in:
xxq250 2025-05-07 11:48:22 +08:00
commit 284ef0da65
23 changed files with 214 additions and 71 deletions

View File

@ -32,11 +32,12 @@ class AccountsController < ApplicationController
username = params[:username]&.gsub(/\s+/, "")
tip_exception("无法使用以下关键词:#{username},请重新命名") if ReversedKeyword.check_exists?(username)
email = params[:email]&.gsub(/\s+/, "")
phone = params[:phone]&.gsub(/\s+/, "")
password = params[:password]
platform = (params[:platform] || 'forge')&.gsub(/\s+/, "")
ActiveRecord::Base.transaction do
result = autologin_register(username, email, password, platform)
result = autologin_register(username, email, password, platform, phone)
if result[:message].blank?
render_ok({user: result[:user]})
else
@ -144,7 +145,7 @@ class AccountsController < ApplicationController
user = Users::RegisterService.call(register_params)
user.mail = "#{user.login}@example.org" if user.mail.blank?
password = decrypt(register_params[:password]) rescue ""
password = decrypt(register_params[:password]) rescue register_params[:password].to_s
password = password.strip
# gitea用户注册, email, username, password
@ -195,7 +196,7 @@ class AccountsController < ApplicationController
# 用户登录
def login
password = decrypt(login_params[:password]) rescue login_params[:password]
password = decrypt(login_params[:password]) rescue login_params[:password].to_s
Users::LoginForm.new(login_params.merge!({password: password})).validate!
@user = User.try_to_login(params[:login], password)
@ -225,9 +226,9 @@ class AccountsController < ApplicationController
end
def change_password
password = decrypt(params[:password]) rescue params[:password]
new_password_repeat = decrypt(params[:new_password_repeat]) rescue params[:new_password_repeat]
old_password = decrypt(params[:old_password]) rescue ""
password = decrypt(params[:password]) rescue params[:password].to_s
new_password_repeat = decrypt(params[:new_password_repeat]) rescue params[:new_password_repeat].to_s
old_password = decrypt(params[:old_password]) rescue params[:old_password]
return render_error("两次输入的密码不一致") if password.to_s != new_password_repeat.to_s
@user = User.find_by(login: params[:login])
return render_forbidden unless User.current.login == @user&.login

View File

@ -19,7 +19,7 @@ class Api::Pm::DashboardsController < Api::Pm::BaseController
pm_project_ids = params[:pm_project_ids].split(",") rescue []
pm_issue_types = params[:pm_issue_types].split(",") rescue []
@all_issues = Issue.where(pm_project_id: pm_project_ids, pm_issue_type: pm_issue_types)
@issues = @all_issues.joins(:issue_participants).where(issue_participants: {participant_id: current_user.id})
@issues = @all_issues.joins(:issue_participants).where(issue_participants: {participant_id: current_user.id, participant_type: 'assigned'})
@issues = @issues.where.not(status_id: 5) if params[:category] == "opened"
@issues = kaminari_paginate(@issues.distinct.pm_includes)

View File

@ -0,0 +1,77 @@
class Api::Pm::PipelinesController < Api::Pm::BaseController
include RepositoriesHelper
def index
@owner = Owner.find_by(login: params[:owner_id].to_s) || Owner.find_by(id: params[:owner_id].to_s)
tip_exception('组织未找到') if @owner.blank?
unless @owner.is_a?(Organization) && @owner.is_member?(current_user.id)
tip_exception('没有查看组织的权限')
end
@project_ids = @owner.projects.ids
project_gpids = @owner.projects.pluck(:gpid)
action_runs = Gitea::ActionRun.where(owner_id: @owner.gitea_uid)
group_data = action_runs.where(status: [1,2]).group(:workflow_id, :status).count
pipelines = Action::Pipeline.where(project_id: @project_ids).order(updated_at: :desc)
run_files = Gitea::ActionRun.select(:workflow_id, :repo_id).where(owner_id: @owner.gitea_uid).group(:workflow_id, :repo_id)
db_files = pipelines.pluck(:file_name)
@run_result = []
run_files.each do |file_info|
file = file_info.workflow_id
project = Project.find_by(gpid: file_info.repo_id)
next if project.blank?
unless db_files.include?(".gitea/workflows/#{file}")
pipeline = Action::Pipeline.find_or_initialize_by(pipeline_name: file.to_s.gsub(".yml", "").gsub(".yaml", ""),
file_name: ".gitea/workflows/#{file}",
branch: project.default_branch,
is_graphic_design: false,
disable: false,
project_id: project.id)
interactor = Repositories::EntriesInteractor.call(@owner, project.identifier, ".gitea/workflows/#{file}", ref: project.default_branch)
if interactor.success?
pipeline.yaml = decode64_content(interactor.result, @owner, project.repository, project.default_branch, nil)
end
pipeline.user_id = current_user.id
pipeline.save
# 导入的流水线统一先禁用
$gitea_hat_client.post_repos_actions_disable(project&.owner&.login, project&.identifier, {query: {workflow: file}}) rescue nil
end
last_action_run = action_runs.where(repo_id: project.gpid).where(workflow_id: file).order(updated: :desc).first
last_action_run_json = last_action_run.present? ? {
id: last_action_run.id,
schedule: last_action_run.schedule_id > 0,
title: last_action_run.title,
index: last_action_run.index,
status: last_action_run.status,
started: last_action_run.started,
stopped: last_action_run.stopped,
length: last_action_run.stopped-last_action_run.started,
created: last_action_run.created,
updated: last_action_run.updated,
} : {}
total = 0
success = 0
failure = 0
group_data.each do |k,v|
total += v if k[0] == file
success += v if k[0] == file && k[1] == 1
failure += v if k[0] == file && k[1] == 2
end
@run_result << {
repo_id: last_action_run.repo_id,
filename: ".gitea/workflows/#{file}",
total: total,
success: success,
failure: failure
}.merge(last_action_run_json)
end
# Rails.logger.info("@run_result======#{@run_result}")
@disabled_workflows = Gitea::RepoUnit.where(repo_id: project_gpids, type: 10).where("config is not null")
@pipelines = Action::Pipeline.select("distinct project_id,max(updated_at) as updated_at")
.where(project_id: @project_ids).group(:project_id).order("updated_at desc")
@pipelines = @pipelines.where("pipeline_name like ?", "%#{params[:pipeline_name]}%") if params[:pipeline_name].present?
@pipelines = @pipelines.where(pipeline_type: params[:pipeline_type]) if params[:pipeline_type].present?
@pipelines = kaminari_paginate(@pipelines)
end
end

View File

@ -1,9 +1,12 @@
class Api::V1::Projects::Actions::RunsController < Api::V1::Projects::Actions::BaseController
def index
@result_object = Api::V1::Projects::Actions::Runs::ListService.call(@project, {workflow: params[:workflow], page: page, limit: limit}, current_user&.gitea_token)
@begin_num = (page.to_i - 1) * limit.to_i
# puts @result_object
@files = $gitea_client.get_repos_contents_by_owner_repo_filepath(@project&.owner&.login, @project&.identifier, ".gitea/workflows") rescue []
@has_file = @files.select { |i| i['name'] == params[:workflow] }.present?
if @has_file
@result_object = Api::V1::Projects::Actions::Runs::ListService.call(@project, {workflow: params[:workflow], page: page, limit: limit}, current_user&.gitea_token)
@begin_num = (page.to_i - 1) * limit.to_i
end
end
def create

View File

@ -11,13 +11,11 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
@run_result = []
@files.map { |i| i['name'] }.each do |file|
unless db_files.include?(".gitea/workflows/#{file}")
disabled_config = Gitea::RepoUnit.where(repo_id: @project.gpid, type: 10)&.first&.config
disabled_workflows = disabled_config.present? ? JSON.parse(disabled_config)["DisabledWorkflows"] : []
pipeline = Action::Pipeline.new(pipeline_name: file.to_s.gsub(".yml", "").gsub(".yaml", ""),
pipeline = Action::Pipeline.find_or_initialize_by(pipeline_name: file.to_s.gsub(".yml", "").gsub(".yaml", ""),
file_name: ".gitea/workflows/#{file}",
branch: @project.default_branch,
is_graphic_design: false,
disable: disabled_workflows.include?(file.to_s),
disable: false,
project_id: @project.id)
interactor = Repositories::EntriesInteractor.call(@owner, @project.identifier, ".gitea/workflows/#{file}", ref: @project.default_branch)
if interactor.success?
@ -25,6 +23,8 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
end
pipeline.user_id = current_user.id
pipeline.save
# 导入的流水线统一先禁用
$gitea_hat_client.post_repos_actions_disable(@project&.owner&.login, @project&.identifier, {query: {workflow: file}}) rescue nil
end
last_action_run = @action_runs.where(workflow_id: file).order(updated: :desc).first
last_action_run_json = last_action_run.present? ? {
@ -55,9 +55,11 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
failure: failure
}.merge(last_action_run_json)
end
Rails.logger.info("@run_result======#{@run_result}")
# Rails.logger.info("@run_result======#{@run_result}")
disabled_config = Gitea::RepoUnit.where(repo_id: @project.gpid, type: 10)&.first&.config
@disabled_workflows = disabled_config.present? ? JSON.parse(disabled_config)["DisabledWorkflows"] : []
@pipelines = Action::Pipeline.where(project_id: @project.id).order(updated_at: :desc)
@pipelines = paginate @pipelines
@pipelines = kaminari_paginate(@pipelines)
end
def test_yaml
@ -94,19 +96,21 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
def save_yaml
@pipeline = params[:id].present? ? Action::Pipeline.find(params[:id]) : Action::Pipeline.find_or_initialize_by(pipeline_name: params[:pipeline_name], project_id: @project.id)
@pipeline.file_name = ".gitea/workflows/#{@pipeline.pipeline_name}.yml"
@pipeline.branch = params[:branch] || @project.default_branch
@pipeline.branch = params[:branch].to_json if params[:branch].present?
@pipeline.json = params[:pipeline_json].to_json if params[:pipeline_json].present?
pipeline_yaml = params[:pipeline_yaml].present? ? params[:pipeline_yaml] : build_pipeline_yaml_new(params[:pipeline_name], params[:pipeline_json])
@pipeline.pipeline_name = params[:pipeline_name] if params[:pipeline_name].present?
pipeline_yaml = params[:pipeline_yaml].present? ? params[:pipeline_yaml] : build_pipeline_yaml_new(@pipeline.pipeline_name, params[:pipeline_json])
tip_exception("流水线yaml内空不能为空") if pipeline_yaml.blank?
@pipeline.yaml = pipeline_yaml
Rails.logger.info "pipeline_yaml base64=========================#{Base64.encode64(@pipeline.yaml).gsub(/\n/, '')}"
#Rails.logger.info "pipeline_yaml base64=========================#{Base64.encode64(@pipeline.yaml).gsub(/\n/, '')}"
sha = get_pipeline_file_sha(@pipeline.file_name, @pipeline.branch)
#Rails.logger.info "content_params=========#{content_params("create")}"
interactor = sha.present? ? Gitea::UpdateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("update").merge(sha: sha)) : Gitea::CreateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("create"))
tip_exception(interactor.error) unless interactor.success?
file = interactor.result
@pipeline.pipeline_type = @pipeline.json.present? ? 2 : 1
@pipeline.save
render_ok({ pipeline_yaml: pipeline_yaml, pipeline_name: params[:pipeline_name], file_name: @pipeline.file_name, sha: sha.present? ? sha : file['content']['sha'] })
render_ok({ pipeline_yaml: pipeline_yaml, pipeline_name: @pipeline.pipeline_name, file_name: @pipeline.file_name, sha: sha.present? ? sha : file['content']['sha'] })
end
def build_yaml
@ -144,6 +148,7 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
interactor = Gitea::DeleteFileInteractor.call(current_user.gitea_token, @owner.login, del_content_params(sha).merge(identifier: @project.identifier))
tip_exception(interactor.error) unless interactor.success?
end
Gitea::ActionRun.where(repo_id: @pipeline.project.gpid).destroy_all
@pipeline.destroy!
end
render_ok
@ -175,6 +180,8 @@ class Api::V1::Projects::PipelinesController < Api::V1::BaseController
def show
@pipeline = Action::Pipeline.find_by(id: params[:id])
disabled_config = Gitea::RepoUnit.where(repo_id: @project.gpid, type: 10)&.first&.config
@disabled_workflows = disabled_config.present? ? JSON.parse(disabled_config)["DisabledWorkflows"] : []
# @pipeline = Action::Pipeline.new(id: 0, pipeline_name: "test-ss", yaml: build_test_yaml) if @pipeline.blank?
end

View File

@ -54,7 +54,7 @@ class Api::V1::UsersController < Api::V1::BaseController
end
def check_password
password = decrypt(params[:password]) rescue params[:password]
password = decrypt(params[:password]) rescue params[:password].to_s
return tip_exception(-5, "8~16位密码支持字母数字和符号") unless password =~ CustomRegexp::PASSWORD
return tip_exception(-5, "密码错误") unless @observe_user.check_password?(password)
render_ok
@ -127,7 +127,7 @@ class Api::V1::UsersController < Api::V1::BaseController
def destroy
password = decrypt(params[:password]) rescue params[:password]
password = decrypt(params[:password]) rescue params[:password].to_s
return tip_exception(-1, "密码不正确.") unless @observe_user.check_password?(password)
org_ids = TeamUser.where(user_id: @observe_user.id).pluck(:organization_id) | OrganizationUser.where(user_id: @observe_user.id).pluck(:organization_id)
org_count = TeamUser.where(organization_id: org_ids).where(user_id: @observe_user.id).joins(:team).where(teams: {authorize: %w(owner)}).count

View File

@ -0,0 +1,17 @@
class HomeController < ApplicationController
def index
@user_count = Rails.cache.fetch("homecontroller:user_count", expires_in: 1.hours) do
User.count
end
@project_count = Rails.cache.fetch("homecontroller:project_count", expires_in: 1.hours) do
Project.count
end
@project_dataset_count = Rails.cache.fetch("homecontroller:project_dataset_count", expires_in: 1.hours) do
ProjectDataset.count
end
@commit_count = Rails.cache.fetch("homecontroller:commit_count", expires_in: 1.hours) do
CommitLog.count
end
end
end

View File

@ -140,7 +140,7 @@ class Organizations::OrganizationsController < Organizations::BaseController
end
def password
decrypt(params[:password]) rescue params[:password]
decrypt(params[:password]) rescue params[:password].to_s
end
def load_organization

View File

@ -37,6 +37,10 @@ class RepositoriesController < ApplicationController
@fork_project = Project.find_by(id: @project_fork_id)
@fork_project_user = @fork_project.owner
end
# 修正默认分支
if @result[:repo].present? && @result[:repo]['default_branch'].present? && @result[:repo]['default_branch'] != @project.default_branch
@project.update_column('default_branch', @result[:repo]['default_branch'])
end
rescue Exception => e
uid_logger_error(e.message)
tip_exception(e.message)

View File

@ -10,6 +10,8 @@ class Users::MessagesController < Users::BaseController
result = Notice::Read::ListService.call(observed_user.id, message_type, message_status, page, limit)
return render_error if result.nil?
@data = result[2]
rescue
render_ok
end
def create

View File

@ -54,14 +54,14 @@ class BaseForm
end
def check_password(password)
password = decrypt(password) rescue ""
password = decrypt(password) rescue password
password = strip(password)
raise PasswordFormatError, "密码8~16位密码支持字母数字和符号" unless password =~ CustomRegexp::PASSWORD
end
def check_password_confirmation(password, password_confirmation)
password = decrypt(password) rescue ""
password_confirmation = decrypt(password_confirmation) rescue ""
password = decrypt(password) rescue password
password_confirmation = decrypt(password_confirmation) rescue password_confirmation
password = strip(password)
password_confirmation = strip(password_confirmation)

View File

@ -4,8 +4,8 @@ module Accounts
# login、code、password、password_confirmation
def initialize(user, params)
@user = user
@password = decrypt(params[:password]) rescue params[:password]
@password_confirmation = decrypt(params[:password_confirmation]) rescue ""
@password = decrypt(params[:password]) rescue params[:password].to_s
@password_confirmation = decrypt(params[:password_confirmation]) rescue params[:password_confirmation].to_s
end
def call

View File

@ -11,7 +11,7 @@ class Api::V1::Users::UpdateEmailService < ApplicationService
def initialize(user, params, token =nil)
@user = user
@token = token
@password = decrypt(params[:password]) rescue params[:password]
@password = decrypt(params[:password]) rescue params[:password].to_s
@mail = params[:email]
@old_mail = user.mail
@code = params[:code]

View File

@ -9,7 +9,7 @@ class Api::V1::Users::UpdatePhoneService < ApplicationService
def initialize(user, params)
@user = user
@password = decrypt(params[:password]) rescue params[:password]
@password = decrypt(params[:password]) rescue params[:password].to_s
@phone = params[:phone]
@code = params[:code]
@verify_code = VerificationCode.where(phone: @phone, code_type: 4).last

View File

@ -4,7 +4,7 @@ class Users::RegisterService < ApplicationService
def initialize(params)
@login = params[:login]
@namespace = params[:namespace]
@password = decrypt(params[:password]) rescue params[:password]
@password = decrypt(params[:password]) rescue params[:password].to_s
@code = params[:code]
end

View File

@ -51,7 +51,7 @@
<li>
<%= sidebar_item_group('#setting-index', '首页配置', icon: 'file', has_permission: current_user.admin? || current_user.business?) do %>
<li><%= sidebar_item(admins_topic_banners_path, 'banner管理', icon: 'image', controller: 'admins-topic-banners', has_permission: current_user.admin? || current_user.business?) %></li>
<!--<li><%#= sidebar_item(admins_topic_cards_path, '卡片管理', icon: 'archive', controller: 'admins-topic-cards', has_permission: current_user.admin? || current_user.business?) %></li>-->
<li><%= sidebar_item(admins_topic_cards_path, '卡片管理', icon: 'archive', controller: 'admins-topic-cards', has_permission: current_user.admin? || current_user.business?) %></li>
<li><%= sidebar_item(admins_topic_activity_forums_path, '平台动态管理', icon: 'bell', controller: 'admins-topic-activity_forums', has_permission: current_user.admin? || current_user.business?) %></li>
<li><%= sidebar_item(admins_topic_excellent_projects_path, '优秀仓库管理', icon: 'git', controller: 'admins-topic-excellent_projects', has_permission: current_user.admin? || current_user.business?) %></li>
<li><%= sidebar_item(admins_topic_pinned_forums_path, '精选文章管理', icon: 'edit', controller: 'admins-topic-pinned_forums', has_permission: current_user.admin? || current_user.business?) %></li>

View File

@ -0,0 +1,30 @@
json.status 0
json.message "success"
json.count @pipelines.total_count
json.projects @pipelines.map(&:project_id).uniq.each do |project_id|
json.id project_id
project = Project.find_by(id: project_id)
if project.present?
json.owner project.owner.name
json.identifier project.identifier
json.name project.name
json.url "#{Rails.application.config_for(:configuration)['platform_url']}/#{project.owner.name}/#{project.identifier}"
pipelines = Action::Pipeline.where(project_id: project.id).order(updated_at: :desc)
json.pipelines pipelines.each do |pipeline|
json.extract! pipeline, :id, :project_id, :pipeline_name, :pipeline_status, :description, :file_name, :is_graphic_design,
:repo_name, :repo_identifier, :branch, :event, :sha, :disable, :json, :yaml, :created_at, :updated_at
repo_config = @disabled_workflows.select { |config| config.repo_id == pipeline.project.gpid }
if repo_config.present?
json.disable JSON.parse(repo_config.first.config)["DisabledWorkflows"].to_s.include?(pipeline.file_name.to_s.gsub(".gitea/workflows/", ""))
else
json.disable pipeline.disable
end
json.pipeline_type pipeline.pipeline_type
if pipeline.json.blank? && pipeline.yaml.blank?
json.run_data nil
else
json.run_data @run_result.select { |result| result[:repo_id] == project.gpid && result[:filename] == pipeline.file_name}.first
end
end
end
end

View File

@ -1,24 +1,29 @@
json.total_data @result_object[:total_data].to_i
if @result_object[:data]["Runs"].present?
json.runs @result_object[:data]["Runs"].each_with_index.to_a do |run, index|
json.num @result_object[:total_data].to_i - @begin_num - index
json.workflow run["WorkflowID"]
json.index run["Index"]
json.title run["Title"]
json.trigger_user do
json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(run['TriggerUser']), name: run['TriggerUser']['Name'] }
end
if @has_file
json.total_data @result_object[:total_data].to_i
if @result_object[:data]["Runs"].present?
json.runs @result_object[:data]["Runs"].each_with_index.to_a do |run, index|
json.num @result_object[:total_data].to_i - @begin_num - index
json.workflow run["WorkflowID"]
json.index run["Index"]
json.title run["Title"]
json.trigger_user do
json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(run['TriggerUser']), name: run['TriggerUser']['Name'] }
end
if run["Ref"].starts_with?("refs/tags")
json.ref run["Ref"].gsub!("/refs/tags/", "")
else
json.ref run["Ref"].gsub!("refs/heads/", "")
end
if run["Ref"].starts_with?("refs/tags")
json.ref run["Ref"].gsub!("/refs/tags/", "")
else
json.ref run["Ref"].gsub!("refs/heads/", "")
end
json.status run["Status"]
json.time_ago time_from_now(run["Created"])
json.holding_time run["Status"] == 6 ? Time.now.to_i - run["Created"] : run["Stopped"] - run["Created"]
json.status run["Status"]
json.time_ago time_from_now(run["Created"])
json.holding_time run["Status"] == 6 ? Time.now.to_i - run["Created"] : run["Stopped"] - run["Created"]
end
else
json.runs []
end
else
json.runs []
json.total_data 0
json.runs []
end

View File

@ -1,9 +1,11 @@
json.status 0
json.message "success"
json.count @pipelines.total_count
json.pipelines @pipelines.each do |pipeline|
json.extract! pipeline, :id, :pipeline_name, :pipeline_status, :description, :file_name, :is_graphic_design,
:repo_name, :repo_identifier, :branch, :event, :sha, :disable, :json, :yaml, :created_at, :updated_at
json.disable @disabled_workflows.include?(pipeline.file_name.to_s.gsub(".gitea/workflows/", ""))
json.pipeline_type pipeline.pipeline_type
json.run_data @run_result.select { |result| result[:filename] == pipeline.file_name }.first
end

View File

@ -1,4 +1,5 @@
json.status 0
json.message "success"
json.extract! @pipeline, :id, :pipeline_name, :pipeline_status, :description, :file_name, :is_graphic_design, :pipeline_type,
:repo_name, :repo_identifier, :branch, :event, :sha, :disable, :json, :yaml, :created_at, :updated_at
:repo_name, :repo_identifier, :branch, :event, :sha, :json, :yaml, :created_at, :updated_at
json.disable @disabled_workflows.include?(@pipeline.file_name.to_s.gsub(".gitea/workflows/", ""))

View File

@ -1,21 +1,9 @@
json.images_url @images_url
json.reps @rep_list
json.shixuns do
json.partial! 'shixuns/shixun', locals: {shixuns: @shixuns}
json.status 0
json.message "success"
json.statistics_data do
json.user_count @user_count
json.project_count @project_count
json.project_dataset_count @project_dataset_count
json.commit_count @commit_count
end
json.subjects do
json.partial! 'subjects/subject', locals: {subjects: @subjects}
end
# if current_laboratory.main_site?
# json.teachers do
# json.partial! 'users/user_small', users: @tea_users
# end
#
# json.students do
# json.partial! 'users/user_small', users: @stu_users
# end
# end

View File

@ -51,6 +51,7 @@ defaults format: :json do
end
end
resources :action_runs, only: [:index]
resources :pipelines, only: [:index]
end
namespace :v1 do

View File

@ -0,0 +1,5 @@
class AddUserIndexPhone < ActiveRecord::Migration[5.2]
def change
add_index :users, :phone
end
end