diff --git a/db/migrate/20210810081142_create_verification_codes.rb b/db/migrate/20210810081142_create_verification_codes.rb new file mode 100644 index 0000000000000000000000000000000000000000..910d052151c5f4558ed4bd53944f082b6e7f4e4a --- /dev/null +++ b/db/migrate/20210810081142_create_verification_codes.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateVerificationCodes < Gitlab::Database::Migration[1.0] + def up + create_table_with_constraints :verification_codes do |t| + t.text :visitor_id + t.text :code + t.text :phone + + t.text_limit :visitor_id, 64 + t.text_limit :code, 8 + t.text_limit :phone, 32 + + t.timestamps_with_timezone + end + end + + def down + drop_table :verification_codes + end +end diff --git a/db/migrate/20210902084808_add_phone_to_user_details.rb b/db/migrate/20210902084808_add_phone_to_user_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..685e0fa3c0185fe18ec0b19adcd145ba3ba2fa2e --- /dev/null +++ b/db/migrate/20210902084808_add_phone_to_user_details.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddPhoneToUserDetails < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20210902085536_add_text_limit_to_user_details_phone + def change + add_column :user_details, :phone, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20210902085536_add_text_limit_to_user_details_phone.rb b/db/migrate/20210902085536_add_text_limit_to_user_details_phone.rb new file mode 100644 index 0000000000000000000000000000000000000000..f250aad32532d8de98ad2809e72cb0891d69d18b --- /dev/null +++ b/db/migrate/20210902085536_add_text_limit_to_user_details_phone.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitToUserDetailsPhone < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + add_text_limit :user_details, :phone, 32 + end + + def down + remove_text_limit :user_details, :phone + end +end diff --git a/db/migrate/20210902091002_add_index_for_phone_to_user_details.rb b/db/migrate/20210902091002_add_index_for_phone_to_user_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..9e4c1c9f40eb74d2d9f0690984386f48e1dbc360 --- /dev/null +++ b/db/migrate/20210902091002_add_index_for_phone_to_user_details.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexForPhoneToUserDetails < Gitlab::Database::Migration[1.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'index_user_details_on_phone' + + DOWNTIME = false + + def up + add_concurrent_index :user_details, [:phone], where: 'phone IS NOT NULL', name: INDEX_NAME + end + + def down + remove_concurrent_index :user_details, :state, name: INDEX_NAME + end +end diff --git a/db/migrate/20210902092835_add_index_for_verification_codes.rb b/db/migrate/20210902092835_add_index_for_verification_codes.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b10374bd98c8994be8f5fd8a8ccc96d22cd2e84 --- /dev/null +++ b/db/migrate/20210902092835_add_index_for_verification_codes.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexForVerificationCodes < Gitlab::Database::Migration[1.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'index_verification_codes_on_visitor_id_and_phone' + + DOWNTIME = false + + def up + add_concurrent_index :verification_codes, [:visitor_id, :phone], where: 'visitor_id IS NOT NULL', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :verification_codes, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210810081142 b/db/schema_migrations/20210810081142 new file mode 100644 index 0000000000000000000000000000000000000000..e3dbff34b79688521ef1d1884b9901227f20582a --- /dev/null +++ b/db/schema_migrations/20210810081142 @@ -0,0 +1 @@ +1ae39d4e04b6fdf2c853374dec1a14c517ec75d9b794ff66544b2a6eae8fbdd8 \ No newline at end of file diff --git a/db/schema_migrations/20210902084808 b/db/schema_migrations/20210902084808 new file mode 100644 index 0000000000000000000000000000000000000000..0cc067b69ba6c34abb57b6f0c2cb20b1a54ad60e --- /dev/null +++ b/db/schema_migrations/20210902084808 @@ -0,0 +1 @@ +c5ca4200f652010ba21351efdc0ebd9509410ea3c5ab86bf355cbe432bf41308 \ No newline at end of file diff --git a/db/schema_migrations/20210902085536 b/db/schema_migrations/20210902085536 new file mode 100644 index 0000000000000000000000000000000000000000..c9eb9c999f98a8d432f78e9ed8b4f0d7d8929eb1 --- /dev/null +++ b/db/schema_migrations/20210902085536 @@ -0,0 +1 @@ +c32d10233b9c0610f25b671a3022b29df00bcbc665597bdd384ad5db62262736 \ No newline at end of file diff --git a/db/schema_migrations/20210902091002 b/db/schema_migrations/20210902091002 new file mode 100644 index 0000000000000000000000000000000000000000..e8c9b2c701af8b5f011da5fc2f709cc3173ef291 --- /dev/null +++ b/db/schema_migrations/20210902091002 @@ -0,0 +1 @@ +fd7b1d3edd6a65da408cfc62fe5a5b6be018784f206f5b611d142ca8f27cc860 \ No newline at end of file diff --git a/db/schema_migrations/20210902092835 b/db/schema_migrations/20210902092835 new file mode 100644 index 0000000000000000000000000000000000000000..8c92978e0ed4d65ff9456a88f0c5e841826c0326 --- /dev/null +++ b/db/schema_migrations/20210902092835 @@ -0,0 +1 @@ +b21757d64b27e73737d3506405f62408cb0b39b20b231caf3f4ed052f2706f94 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0d8014e1e0bededbe89117941d585ff43ae7858a..6320561f30af3030bfc9c196514f20d771a56ca1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10333,10 +10333,10 @@ CREATE TABLE application_settings ( throttle_unauthenticated_api_enabled boolean DEFAULT false NOT NULL, throttle_unauthenticated_api_requests_per_period integer DEFAULT 3600 NOT NULL, throttle_unauthenticated_api_period_in_seconds integer DEFAULT 3600 NOT NULL, - jobs_per_stage_page_size integer DEFAULT 200 NOT NULL, sidekiq_job_limiter_mode smallint DEFAULT 1 NOT NULL, sidekiq_job_limiter_compression_threshold_bytes integer DEFAULT 100000 NOT NULL, sidekiq_job_limiter_limit_bytes integer DEFAULT 0 NOT NULL, + jobs_per_stage_page_size integer DEFAULT 200 NOT NULL, suggest_pipeline_enabled boolean DEFAULT true NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), @@ -13038,7 +13038,6 @@ CREATE TABLE dep_ci_build_trace_section_names ( ); CREATE SEQUENCE dep_ci_build_trace_section_names_id_seq - AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -15253,6 +15252,24 @@ CREATE SEQUENCE iterations_cadences_id_seq ALTER SEQUENCE iterations_cadences_id_seq OWNED BY iterations_cadences.id; +CREATE TABLE jh_verification_codes ( + id bigint NOT NULL, + visitor_id_code text, + code text, + phone text, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE jh_verification_codes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE jh_verification_codes_id_seq OWNED BY jh_verification_codes.id; + CREATE TABLE jira_connect_installations ( id bigint NOT NULL, client_key character varying, @@ -15592,8 +15609,8 @@ CREATE TABLE members ( expires_at date, ldap boolean DEFAULT false NOT NULL, override boolean DEFAULT false NOT NULL, - state smallint DEFAULT 0, - invite_email_success boolean DEFAULT true NOT NULL + invite_email_success boolean DEFAULT true NOT NULL, + state smallint DEFAULT 0 ); CREATE SEQUENCE members_id_seq @@ -19758,7 +19775,11 @@ CREATE TABLE user_details ( provisioned_by_group_id bigint, pronouns text, pronunciation text, + phone text, + jh_phone text, CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)), + CONSTRAINT check_5dc92dd616 CHECK ((char_length(jh_phone) <= 32)), + CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 32)), CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)), CONSTRAINT check_eeeaf8d4f0 CHECK ((char_length(pronouns) <= 50)), CONSTRAINT check_f932ed37db CHECK ((char_length(pronunciation) <= 255)) @@ -20057,6 +20078,27 @@ CREATE SEQUENCE users_statistics_id_seq ALTER SEQUENCE users_statistics_id_seq OWNED BY users_statistics.id; +CREATE TABLE verification_codes ( + id bigint NOT NULL, + visitor_id text, + code text, + phone text, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_8ed01b9743 CHECK ((char_length(visitor_id) <= 64)), + CONSTRAINT check_9b84e6aaff CHECK ((char_length(code) <= 8)), + CONSTRAINT check_f5684c195b CHECK ((char_length(phone) <= 32)) +); + +CREATE SEQUENCE verification_codes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE verification_codes_id_seq OWNED BY verification_codes.id; + CREATE TABLE vulnerabilities ( id bigint NOT NULL, milestone_id bigint, @@ -21343,6 +21385,8 @@ ALTER TABLE ONLY issues ALTER COLUMN id SET DEFAULT nextval('issues_id_seq'::reg ALTER TABLE ONLY iterations_cadences ALTER COLUMN id SET DEFAULT nextval('iterations_cadences_id_seq'::regclass); +ALTER TABLE ONLY jh_verification_codes ALTER COLUMN id SET DEFAULT nextval('jh_verification_codes_id_seq'::regclass); + ALTER TABLE ONLY jira_connect_installations ALTER COLUMN id SET DEFAULT nextval('jira_connect_installations_id_seq'::regclass); ALTER TABLE ONLY jira_connect_subscriptions ALTER COLUMN id SET DEFAULT nextval('jira_connect_subscriptions_id_seq'::regclass); @@ -21725,6 +21769,8 @@ ALTER TABLE ONLY users_star_projects ALTER COLUMN id SET DEFAULT nextval('users_ ALTER TABLE ONLY users_statistics ALTER COLUMN id SET DEFAULT nextval('users_statistics_id_seq'::regclass); +ALTER TABLE ONLY verification_codes ALTER COLUMN id SET DEFAULT nextval('verification_codes_id_seq'::regclass); + ALTER TABLE ONLY vulnerabilities ALTER COLUMN id SET DEFAULT nextval('vulnerabilities_id_seq'::regclass); ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vulnerability_exports_id_seq'::regclass); @@ -23015,6 +23061,9 @@ ALTER TABLE ONLY sprints ALTER TABLE ONLY iterations_cadences ADD CONSTRAINT iterations_cadences_pkey PRIMARY KEY (id); +ALTER TABLE ONLY jh_verification_codes + ADD CONSTRAINT jh_verification_codes_pkey PRIMARY KEY (id); + ALTER TABLE ONLY jira_connect_installations ADD CONSTRAINT jira_connect_installations_pkey PRIMARY KEY (id); @@ -23696,6 +23745,9 @@ ALTER TABLE ONLY users_star_projects ALTER TABLE ONLY users_statistics ADD CONSTRAINT users_statistics_pkey PRIMARY KEY (id); +ALTER TABLE ONLY verification_codes + ADD CONSTRAINT verification_codes_pkey PRIMARY KEY (id); + ALTER TABLE ONLY vulnerabilities ADD CONSTRAINT vulnerabilities_pkey PRIMARY KEY (id); @@ -26620,6 +26672,8 @@ CREATE INDEX index_user_custom_attributes_on_key_and_value ON user_custom_attrib CREATE UNIQUE INDEX index_user_custom_attributes_on_user_id_and_key ON user_custom_attributes USING btree (user_id, key); +CREATE INDEX index_user_details_on_phone ON user_details USING btree (phone) WHERE (phone IS NOT NULL); + CREATE INDEX index_user_details_on_provisioned_by_group_id ON user_details USING btree (provisioned_by_group_id); CREATE UNIQUE INDEX index_user_details_on_user_id ON user_details USING btree (user_id); @@ -26702,6 +26756,8 @@ CREATE INDEX index_users_star_projects_on_project_id ON users_star_projects USIN CREATE UNIQUE INDEX index_users_star_projects_on_user_id_and_project_id ON users_star_projects USING btree (user_id, project_id); +CREATE INDEX index_verification_codes_on_visitor_id_and_phone ON verification_codes USING btree (visitor_id, phone) WHERE (visitor_id IS NOT NULL); + CREATE UNIQUE INDEX index_vuln_historical_statistics_on_project_id_and_date ON vulnerability_historical_statistics USING btree (project_id, date); CREATE INDEX index_vulnerabilities_on_author_id ON vulnerabilities USING btree (author_id); @@ -26866,6 +26922,8 @@ CREATE UNIQUE INDEX issue_user_mentions_on_issue_id_and_note_id_index ON issue_u CREATE UNIQUE INDEX issue_user_mentions_on_issue_id_index ON issue_user_mentions USING btree (issue_id) WHERE (note_id IS NULL); +CREATE INDEX jh_index_user_details_on_jh_phone ON user_details USING btree (jh_phone) WHERE (jh_phone IS NOT NULL); + CREATE UNIQUE INDEX kubernetes_namespaces_cluster_and_namespace ON clusters_kubernetes_namespaces USING btree (cluster_id, namespace); CREATE INDEX merge_request_mentions_temp_index ON merge_requests USING btree (id) WHERE ((description ~~ '%@%'::text) OR ((title)::text ~~ '%@%'::text)); @@ -27870,9 +27928,6 @@ ALTER TABLE ONLY identities ALTER TABLE ONLY boards ADD CONSTRAINT fk_ab0a250ff6 FOREIGN KEY (iteration_cadence_id) REFERENCES iterations_cadences(id) ON DELETE CASCADE; -ALTER TABLE ONLY dep_ci_build_trace_sections - ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; - ALTER TABLE ONLY ci_sources_pipelines ADD CONSTRAINT fk_acd9737679 FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -28161,9 +28216,6 @@ ALTER TABLE ONLY cluster_agents ALTER TABLE ONLY protected_tag_create_access_levels ADD CONSTRAINT fk_f7dfda8c51 FOREIGN KEY (protected_tag_id) REFERENCES protected_tags(id) ON DELETE CASCADE; -ALTER TABLE ONLY dep_ci_build_trace_section_names - ADD CONSTRAINT fk_f8cd72cd26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; - ALTER TABLE ONLY ci_stages ADD CONSTRAINT fk_fb57e6cc56 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; @@ -29295,6 +29347,9 @@ ALTER TABLE ONLY merge_request_user_mentions ALTER TABLE ONLY x509_commit_signatures ADD CONSTRAINT fk_rails_ab07452314 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY dep_ci_build_trace_sections + ADD CONSTRAINT fk_rails_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY resource_iteration_events ADD CONSTRAINT fk_rails_abf5d4affa FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; @@ -29757,6 +29812,9 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events ALTER TABLE ONLY merge_requests_closing_issues ADD CONSTRAINT fk_rails_f8540692be FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; +ALTER TABLE ONLY dep_ci_build_trace_section_names + ADD CONSTRAINT fk_rails_f8cd72cd26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY merge_trains ADD CONSTRAINT fk_rails_f90820cb08 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE SET NULL; diff --git a/jh/Gemfile b/jh/Gemfile index a87f9aa81d74b50269f9e261ad8e7664b2a0a1ca..314664a9dc7281732c69d2825dbfa6031f327a54 100644 --- a/jh/Gemfile +++ b/jh/Gemfile @@ -3,3 +3,8 @@ gemfile = File.read(File.expand_path('../Gemfile', __dir__)) instance_eval gemfile.gsub('vendor/gems', '../vendor/gems') + +# rubocop: disable Cop/GemFetcher +gem 'tencentcloud-sdk-common', git: 'https://github.com/TencentCloud/tencentcloud-sdk-ruby.git', glob: 'tencentcloud-sdk-common/tencentcloud-sdk-common.gemspec' +gem 'tencentcloud-sdk-sms', git: 'https://github.com/TencentCloud/tencentcloud-sdk-ruby.git', glob: 'tencentcloud-sdk-sms/tencentcloud-sdk-sms.gemspec' +# rubocop: enable Cop/GemFetcher diff --git a/jh/Gemfile.lock b/jh/Gemfile.lock index 60ad322b194e3aaaabbdaa090c6f8d0d9288a39c..9c5bedc2152c615ee233bc1b89addaa8789ad9f2 100644 --- a/jh/Gemfile.lock +++ b/jh/Gemfile.lock @@ -1,3 +1,18 @@ +GIT + remote: https://github.com/TencentCloud/tencentcloud-sdk-ruby.git + revision: 228e857fa8ff2d880558ce69ad65d2ef618861dd + glob: tencentcloud-sdk-sms/tencentcloud-sdk-sms.gemspec + specs: + tencentcloud-sdk-sms (1.0.153) + tencentcloud-sdk-common (~> 1.0) + +GIT + remote: https://github.com/TencentCloud/tencentcloud-sdk-ruby.git + revision: 228e857fa8ff2d880558ce69ad65d2ef618861dd + glob: tencentcloud-sdk-common/tencentcloud-sdk-common.gemspec + specs: + tencentcloud-sdk-common (1.0.153) + PATH remote: ../vendor/gems/mail-smtp_pool specs: @@ -1630,6 +1645,8 @@ DEPENDENCIES stackprof (~> 0.2.15) state_machines-activerecord (~> 0.8.0) sys-filesystem (~> 1.1.6) + tencentcloud-sdk-common! + tencentcloud-sdk-sms! terser (= 1.0.2) test-prof (~> 0.12.0) test_file_finder (~> 0.1.3) diff --git a/jh/app/assets/javascripts/pages/profiles/show/index.js b/jh/app/assets/javascripts/pages/profiles/show/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d10fbf647fd37ae8113de58968a6efdbb163bb84 --- /dev/null +++ b/jh/app/assets/javascripts/pages/profiles/show/index.js @@ -0,0 +1,21 @@ +import '~/pages/profiles/show'; +import $ from 'jquery'; +import PhoneValidator from 'jh/pages/sessions/new/phone_validator'; +import VerificationCodeButton from 'jh/verification_code_button'; + +const phoneInput = $(`.js-validate-phone`); +const originPhone = phoneInput.val(); +const verificationField = $('.verification-code.form-group'); +const phoneField = $('.phone.form-group'); + +new PhoneValidator(); // eslint-disable-line no-new +new VerificationCodeButton(); // eslint-disable-line no-new + +phoneInput.on('input', () => { + if (phoneInput.val() !== originPhone) { + verificationField.insertAfter(phoneField); + } else { + verificationField.remove(); + } +}); +phoneInput.trigger('input'); diff --git a/jh/app/assets/javascripts/pages/registrations/new/index.js b/jh/app/assets/javascripts/pages/registrations/new/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9235083f5405fbee03eb2efaed8bd111a833ca2e --- /dev/null +++ b/jh/app/assets/javascripts/pages/registrations/new/index.js @@ -0,0 +1,6 @@ +import '~/pages/registrations/new'; +import PhoneValidator from 'jh/pages/sessions/new/phone_validator'; +import VerificationCodeButton from 'jh/verification_code_button'; + +new PhoneValidator(); // eslint-disable-line no-new +new VerificationCodeButton(); // eslint-disable-line no-new diff --git a/jh/app/assets/javascripts/pages/sessions/new/phone_validator.js b/jh/app/assets/javascripts/pages/sessions/new/phone_validator.js new file mode 100644 index 0000000000000000000000000000000000000000..0228cb7271a2ebc6207e7c9829a06ea61656bbc1 --- /dev/null +++ b/jh/app/assets/javascripts/pages/sessions/new/phone_validator.js @@ -0,0 +1,101 @@ +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import InputValidator from '~/validators/input_validator'; + +const rootUrl = gon.relative_url_root; +const invalidInputClass = 'gl-field-error-outline'; +const successInputClass = 'gl-field-success-outline'; +const successMessageSelector = '.validation-success'; +const pendingMessageSelector = '.validation-pending'; +const unavailableMessageSelector = '.validation-error'; +const { CancelToken } = axios; +let cancel; + +export default class PhoneValidator extends InputValidator { + constructor(opts = {}) { + super(); + + const container = opts.container || ''; + const validateLengthElements = document.querySelectorAll(`${container} .js-validate-phone`); + + validateLengthElements.forEach((element) => { + element.isValid = Promise.resolve(false); + element.addEventListener('input', (e) => { + PhoneValidator.resetInputState(e.target); + PhoneValidator.validatePhoneInputExist(e.target); + }); + }); + } + static async validatePhoneInput(inputDomElement) { + const isPhoneValid = + PhoneValidator.validatePhoneInputPattern(inputDomElement) && (await inputDomElement.isValid); + return isPhoneValid; + } + + static validatePhoneInputPattern(inputDomElement) { + return /^1[3456789]\d{9}$/.test(inputDomElement.value); + } + + static validatePhoneInputExist(inputDomElement) { + const element = inputDomElement; + const phone = element.value; + if (cancel !== undefined) { + cancel(); + } + if (element.checkValidity() && this.validatePhoneInputPattern(element)) { + this.setMessageVisibility(element, pendingMessageSelector); + element.isValid = PhoneValidator.fetchPhoneAvailability(phone) + .then((PhoneTaken) => { + PhoneValidator.setInputState(element, !PhoneTaken); + PhoneValidator.setMessageVisibility(element, pendingMessageSelector, false); + PhoneValidator.setMessageVisibility( + element, + PhoneTaken ? unavailableMessageSelector : successMessageSelector, + ); + return !PhoneTaken; + }) + .catch((error) => { + if (axios.isCancel(error)) { + PhoneValidator.setMessageVisibility(element, pendingMessageSelector, false); + return false; + } + createFlash({ + message: __('An error occurred while validating phone'), + }); + return false; + }); + } + } + + static fetchPhoneAvailability(phone) { + return axios + .get(`${rootUrl}/users/${phone}/phone_exists`, { + cancelToken: new CancelToken(function executor(c) { + cancel = c; + }), + }) + .then(({ data }) => data.exists); + } + + static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) { + const messageElement = inputDomElement.parentElement.parentElement.querySelector( + messageSelector, + ); + messageElement.classList.toggle('hide', !isVisible); + } + + static setInputState(inputDomElement, success = true) { + inputDomElement.classList.toggle(successInputClass, success); + inputDomElement.classList.toggle(invalidInputClass, !success); + } + + static resetInputState(inputDomElement) { + PhoneValidator.setMessageVisibility(inputDomElement, successMessageSelector, false); + PhoneValidator.setMessageVisibility(inputDomElement, unavailableMessageSelector, false); + + if (inputDomElement.checkValidity()) { + inputDomElement.classList.remove(successInputClass, invalidInputClass); + } + } +} diff --git a/jh/app/assets/javascripts/pages/users/terms/index/index.js b/jh/app/assets/javascripts/pages/users/terms/index/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d14aad8ba8b1c6b6129569005f452d5ee71ec970 --- /dev/null +++ b/jh/app/assets/javascripts/pages/users/terms/index/index.js @@ -0,0 +1,19 @@ +import $ from 'jquery'; +import PhoneValidator from 'jh/pages/sessions/new/phone_validator'; +import VerificationCodeButton from 'jh/verification_code_button'; + +const phoneElement = $('.js-validate-phone')[0]; + +if (phoneElement) { + $('.button_to') + .eq(0) + .submit(() => { + if (!PhoneValidator.validatePhoneInputPattern(phoneElement)) { + return false; + } + $('.phone-form').submit(); + return false; + }); + new PhoneValidator(); // eslint-disable-line no-new + new VerificationCodeButton(); // eslint-disable-line no-new +} diff --git a/jh/app/assets/javascripts/verification_code_button.js b/jh/app/assets/javascripts/verification_code_button.js new file mode 100644 index 0000000000000000000000000000000000000000..3c048018c328475a157cdba9abe4e6c0724d1655 --- /dev/null +++ b/jh/app/assets/javascripts/verification_code_button.js @@ -0,0 +1,72 @@ +import Cookies from 'js-cookie'; +import PhoneValidator from 'jh/pages/sessions/new/phone_validator'; +import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { __, s__, sprintf } from '~/locale'; + +const rootUrl = gon.relative_url_root; +export default class VerificationCodeButton { + wait = 10; + timer = null; + enabledButtonText = __('Get code'); + get disabledButtonText() { + return sprintf(s__('Resend in %{wait}s'), { + wait: this.wait, + }); + } + constructor() { + const phoneInputElement = document.querySelector('.js-phone-input'); + const verificationBtnElement = document.querySelector('.js-verification-btn'); + phoneInputElement.addEventListener('input', async () => { + const isPhoneValid = + !this.timer && (await PhoneValidator.validatePhoneInput(phoneInputElement)); + verificationBtnElement.classList.toggle('disabled', !isPhoneValid); + }); + + verificationBtnElement.addEventListener('click', async () => { + const isPhoneValid = + !this.timer && (await PhoneValidator.validatePhoneInput(phoneInputElement)); + if (!isPhoneValid) { + return; + } + + const response = await waitForCaptchaToBeSolved(window.gon.recaptcha_sitekey); + const params = { + visitor_id: Cookies.get('visitor_id'), + phone: phoneInputElement.value, + 'g-recaptcha-response': response, + }; + verificationBtnElement.setAttribute('disabled', true); + this.timer = setInterval(() => { + if (this.wait <= 0) { + this.resetVerificationBtnState(verificationBtnElement); + return; + } + verificationBtnElement.innerHTML = this.disabledButtonText; + this.wait -= 1; + }, 1000); + axios.post(`${rootUrl}/-/sms/verification_code`, params) + .then(({ data }) => { + if (data.status !== 'OK') { + createFlash({ + message: __('An error occurred while sending verification code'), + }); + } + }) + .catch(() => { + createFlash({ + message: __('An error occurred while sending verification code'), + }); + return false; + }); + }); + } + resetVerificationBtnState(verificationBtnElement) { + clearInterval(this.timer); + this.timer = null; + this.wait = 10; + verificationBtnElement.removeAttribute('disabled'); + verificationBtnElement.innerHTML = this.enabledButtonText; // eslint-disable-line no-param-reassign + } +} diff --git a/jh/app/assets/stylesheets/page_bundles/signup.scss b/jh/app/assets/stylesheets/page_bundles/signup.scss new file mode 100644 index 0000000000000000000000000000000000000000..377a2e9ceb9f47301a4bf31e322963edf0387f0a --- /dev/null +++ b/jh/app/assets/stylesheets/page_bundles/signup.scss @@ -0,0 +1,2 @@ +@import '../../../../../app/assets/stylesheets/page_bundles/signup'; + diff --git a/jh/app/assets/stylesheets/pages/clusters.scss b/jh/app/assets/stylesheets/pages/clusters.scss index 6383dd610da23296d8a07a34b46faa7875f52ce2..97d63b0364abf4f415c76de057668b2da086de41 100644 --- a/jh/app/assets/stylesheets/pages/clusters.scss +++ b/jh/app/assets/stylesheets/pages/clusters.scss @@ -1,3 +1,5 @@ +@import '../../../../../app/assets/stylesheets/pages/clusters'; + .gcp-signup-offer { display: none !important; } diff --git a/jh/app/assets/stylesheets/phone_verification.scss b/jh/app/assets/stylesheets/phone_verification.scss new file mode 100644 index 0000000000000000000000000000000000000000..7afba242124e7e7a7cd101692648346dabae714f --- /dev/null +++ b/jh/app/assets/stylesheets/phone_verification.scss @@ -0,0 +1,56 @@ +@import 'page_bundles/mixins_and_variables_and_functions'; +.gl-flex-fill-1 { + flex: 1; +} + +.gl-mt-3 { + margin-top: 0.5rem; +} + +.gl-ml-3 { + margin-left: 0.5rem; +} + +.gl-flex-grow-1 { + flex-grow: 1; +} + +.gl-align-items-flex-start { + align-items: flex-start; +} + +.phone-input, +.verification-code-input { + .form-group { + margin-bottom: 0; + } + p { + margin-bottom: 0; + } +} + +.js-verification-btn { + min-width: 120px; +} + +.verification-code, +.phone { + .input-group { + max-width: 344px; + } +} + +.phone-form { + padding: $gl-padding; +} + +.phone-form.inline { + @include media-breakpoint-up(md) { + .phone, + .verification-code { + display: inline-block; + margin-right: $gl-spacing-scale-4; + width: 360px; + } + } +} diff --git a/jh/app/assets/stylesheets/startup/startup-general.scss b/jh/app/assets/stylesheets/startup/startup-general.scss new file mode 100644 index 0000000000000000000000000000000000000000..960a9224119b974a7173fe1db3ac8f4180883101 --- /dev/null +++ b/jh/app/assets/stylesheets/startup/startup-general.scss @@ -0,0 +1,2 @@ +@import '../../../../../ee/app/assets/stylesheets/startup/startup-general'; +@import '../phone_verification'; diff --git a/jh/app/assets/stylesheets/startup/startup-signin.scss b/jh/app/assets/stylesheets/startup/startup-signin.scss new file mode 100644 index 0000000000000000000000000000000000000000..24f9d0a84fd19590970da5faaa160ea6c087933c --- /dev/null +++ b/jh/app/assets/stylesheets/startup/startup-signin.scss @@ -0,0 +1,3 @@ +@import '../../../../../ee/app/assets/stylesheets/startup/startup-signin'; +@import '../phone_verification'; + diff --git a/jh/app/controllers/jh/application_controller.rb b/jh/app/controllers/jh/application_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..2ddc90b13d0bf1db7c3ae7187bf553c6d60b95c6 --- /dev/null +++ b/jh/app/controllers/jh/application_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module JH + module ApplicationController + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + # JH add one more step in enforce_terms! for phone verification + override :enforce_terms! + def enforce_terms! + super + + enforce_phone_verification! if current_user + end + + override :can? + def can?(object, action, subject = :global) + # Monkey patch the method of `can?` only in the page of terms index + if controller_name == "terms" && action_name == "index" + !object.phone_verified? || super(object, action, subject) if action == :accept_terms || action == :decline_terms + else + super(object, action, subject) + end + end + + private + + def enforce_phone_verification! + return if current_user.phone_verified? + + redirect_path = if request.get? + request.fullpath + else + URI(request.referer).path if request.referer + end + + flash[:notice] = _("Please verify your phone before continuing.") + redirect_to terms_path(redirect: redirect_path), status: :found + end + end +end diff --git a/jh/app/controllers/jh/profiles_controller.rb b/jh/app/controllers/jh/profiles_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..77ce3bad609469a0b8bbbc37d5650e1875917bab --- /dev/null +++ b/jh/app/controllers/jh/profiles_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module JH + module ProfilesController + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + before_action :verify_code_received_by_phone, only: [:update], if: -> { ::Gitlab.dev_env_or_com? && ::Gitlab.jh? } + end + + private + + override :user_params_attributes + def user_params_attributes + super + [:phone, :verification_code] + end + + def verify_code_received_by_phone + unless params[:user][:phone] == current_user.phone + verification = VerificationCode&.current(visitor_id, params[:user][:phone]).last + + unless verification&.code == params[:user][:verification_code] + redirect_back_or_default(default: { action: 'show' }, options: { alert: s_('Verification code is incorrect.') }) + end + end + end + + def visitor_id + return cookies[:visitor_id] if cookies[:visitor_id].present? + + uuid = SecureRandom.uuid + cookies[:visitor_id] = { value: uuid, expires: 24.months } + uuid + end + end +end diff --git a/jh/app/controllers/jh/registrations_controller.rb b/jh/app/controllers/jh/registrations_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..e9be6ba1c06d80dd35ec36711709793de376d7ed --- /dev/null +++ b/jh/app/controllers/jh/registrations_controller.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module JH + module RegistrationsController + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + before_action :verify_code_received_by_phone, only: [:create], if: -> { ::Gitlab.dev_env_or_com? && ::Gitlab.jh? } + end + + private + + # add phone verification_code for JH + override :sign_up_params_attributes + def sign_up_params_attributes + super + [:phone, :verification_code] + end + + def verify_code_received_by_phone + ensure_correct_params! + verification = VerificationCode&.current(visitor_id, params[:user][:phone]).last + return unless verification.present? + + unless verification.code == params[:user][:verification_code] + + redirect_to new_user_registration_path, + status: :not_found, + alert: s_('Verification code is incorrect.') + end + end + + override :check_captcha + def check_captcha + # Skip the form check_captcha if JH SaaS or dev + return if ::Gitlab.jh? && ::Gitlab.dev_env_or_com? + + super + end + + def visitor_id + return cookies[:visitor_id] if cookies[:visitor_id].present? + + uuid = SecureRandom.uuid + cookies[:visitor_id] = { value: uuid, expires: 24.months } + uuid + end + end +end diff --git a/jh/app/controllers/jh/users/terms_controller.rb b/jh/app/controllers/jh/users/terms_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b68547b87660cf0f4d35b1549f5020d0eb1072e --- /dev/null +++ b/jh/app/controllers/jh/users/terms_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module JH + module Users + module TermsController + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + before_action :verify_code_received_by_phone, only: [:accept], if: -> { ::Gitlab.dev_env_or_com? && ::Gitlab.jh? } + end + + override :index + def index + super + + # rubocop: disable Gitlab/ModuleWithInstanceVariables + if current_user && @term.accepted_by_user?(current_user) + flash.now[:notice] = if current_user.phone_verified? + _("You have already accepted the terms and verified your phone.") + else + _("You have already accepted the terms but need to verify your phone.") + end + end + # rubocop: enable Gitlab/ModuleWithInstanceVariables + end + + private + + # rubocop: disable Style/AndOr + def verify_code_received_by_phone + return if current_user.phone_verified? + + if !params[:phone] && !params[:verification_code] + flash[:alert] = s_('Verification code or phone is blank.') + redirect_to terms_path, redirect: redirect_path and return + end + + verification = VerificationCode&.current(visitor_id, params[:phone])&.last + + unless verification.present? + flash[:alert] = s_('Verification code is nonexist.') + redirect_to terms_path, redirect: redirect_path and return + end + + unless verification.code == params[:verification_code] + flash[:alert] = s_('Verification code is incorrect.') + redirect_to terms_path, redirect: redirect_path and return + end + + current_user.phone = params[:phone] + + unless current_user.save + flash[:alert] = s_('Phone saved failed.') + redirect_to terms_path, redirect: redirect_path and return + end + end + # rubocop: enable Style/AndOr + + def visitor_id + return cookies[:visitor_id] if cookies[:visitor_id].present? + + uuid = SecureRandom.uuid + cookies[:visitor_id] = { value: uuid, expires: 24.months } + uuid + end + end + end +end diff --git a/jh/app/controllers/jh/users_controller.rb b/jh/app/controllers/jh/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..cfb127fa6415a386d2c46d1a9f7a5f8fcd2b92eb --- /dev/null +++ b/jh/app/controllers/jh/users_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module JH + module UsersController + extend ::Gitlab::Utils::Override + + def phone_exists + render json: { exists: ::UserDetail.find_by_phone(params[:phone]).present? } + end + + private + + override :user + def user + return if action_name == "phone_exists" + + super + end + end +end diff --git a/jh/app/controllers/sms_controller.rb b/jh/app/controllers/sms_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a266774daac37f1a1265aae1793ad42c417162a --- /dev/null +++ b/jh/app/controllers/sms_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class SmsController < ActionController::Metal # rubocop:disable Gitlab/NamespacedClass + include AbstractController::Rendering + include ActionController::ApiRendering + include ActionController::Renderers + include Recaptcha::Verify + + use_renderers :json + + def verification_code + if !(params[:visitor_id] && params[:phone]).present? + render json: { status: 'PARAMS_BLANK_ERROR' } + elsif verify_recaptcha + code = TencentSms.send_code(params[:phone]) + + if code + VerificationCode.create!({ + visitor_id: params[:visitor_id], + phone: params[:phone], + code: code + }) + end + + render json: { status: 'OK' }, status: (code ? :ok : :not_found) + else + render json: { status: 'RECAPTCHA_ERROR' }, status: :ok + end + rescue StandardError => error + render json: { status: 'ERROR', message: error } + end +end diff --git a/jh/app/helpers/jh/recaptcha_helper.rb b/jh/app/helpers/jh/recaptcha_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..44ef160fc23fece1519ce254cf3408d141e5eff4 --- /dev/null +++ b/jh/app/helpers/jh/recaptcha_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module JH + module RecaptchaHelper + extend ::Gitlab::Utils::Override + + override :show_recaptcha_sign_up? + # JH use the phone verification code which already covers + def show_recaptcha_sign_up? + return if ::Gitlab.jh? + + super + end + end +end diff --git a/jh/app/models/jh/user.rb b/jh/app/models/jh/user.rb new file mode 100644 index 0000000000000000000000000000000000000000..5f272772c35be202700a7b791af590aa3fda6016 --- /dev/null +++ b/jh/app/models/jh/user.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module JH + # User JH mixin + # + # This module is intended to encapsulate JH-specific model logic + # and be prepended in the `User` model + + module User + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + # Add relation scope enum etc.. + # Vertual attribute for receive verification code in register form + attr_accessor :verification_code + + delegate :phone, :phone=, to: :user_detail, allow_nil: true + end + + class_methods do + # Add override class_methods + end + + def phone_verified? + return true if project_bot? + + phone.present? + end + end +end diff --git a/jh/app/models/verification_code.rb b/jh/app/models/verification_code.rb new file mode 100644 index 0000000000000000000000000000000000000000..be8ae9a8f25b8d97711f89dda7bb65e524354361 --- /dev/null +++ b/jh/app/models/verification_code.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# rubocop: disable Gitlab/NamespacedClass +class VerificationCode < ApplicationRecord + scope :current, ->(visitor_id, phone) { + where("visitor_id = ? and phone = ?", visitor_id, phone) + } +end +# rubocop: enable Gitlab/NamespacedClass diff --git a/jh/app/services/jh/users/build_service.rb b/jh/app/services/jh/users/build_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e85106217b934ec72014571f83226070cdba4ec --- /dev/null +++ b/jh/app/services/jh/users/build_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module JH + module Users + module BuildService + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + private + + override :signup_params + def signup_params + super + [:phone] + end + end + end +end diff --git a/jh/app/views/devise/shared/_form_phone_verification.html.haml b/jh/app/views/devise/shared/_form_phone_verification.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..f0cd6ca43a8fa9498bad9737e5f7fa24185fba05 --- /dev/null +++ b/jh/app/views/devise/shared/_form_phone_verification.html.haml @@ -0,0 +1,5 @@ +- url = local_assigns.fetch(:url) +- inline = local_assigns.fetch(:inline) +- if ::Gitlab.dev_env_or_com? && ::Gitlab.jh? && !current_user.phone_verified? + = form_with url: url, class: "phone-form gl-show-field-errors #{ 'inline' if inline }" do |f| + = render 'devise/shared/phone_verification.html.haml', form: f diff --git a/jh/app/views/devise/shared/_phone_verification.html.haml b/jh/app/views/devise/shared/_phone_verification.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..e424aa9e6c4115f45ab001847ff0dbe22c95b608 --- /dev/null +++ b/jh/app/views/devise/shared/_phone_verification.html.haml @@ -0,0 +1,30 @@ +- if ::Gitlab.dev_env_or_com? && ::Gitlab.jh? + - f = local_assigns.fetch(:form) + + .phone.form-group + = f.label :phone, _('Phone'), class: 'label-bold' + .input-group.gl-align-items-flex-start + .input-group-prepend + .input-group-text + +86 + .phone-input.gl-flex-grow-1 + = f.text_field :phone, hide_label: true, + class: 'form-control gl-form-input gl-flex-grow-1 bottom js-phone-input js-validate-phone', + style: 'border-top-left-radius: 0;border-bottom-left-radius: 0;', + data: { qa_selector: 'new_user_phone_field' }, + required: true, + pattern: '^1[3456789]\d{9}$', + title: _('Please provide a valid phone number.') + %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Phone is already taken.') + %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Phone is available.') + %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking phone availability...') + .verification-code.form-group + = f.label :verification_code, _('Verification code'), class: 'label-bold' + .input-group.gl-align-items-flex-start.gl-flex-nowrap + .verification-code-input.gl-flex-grow-1 + = f.text_field :verification_code, hide_label: true, + class: 'form-control gl-form-input .gl-flex-grow-1 bottom', + required: true, + id: 'verification_input' + .js-verification-btn.btn.form-control.gl-button.gl-ml-3.btn-confirm.gl-display-block.disabled{ :class => "gl-w-auto!" } + = _('Get code') diff --git a/jh/app/views/profiles/_extra_settings.html.haml b/jh/app/views/profiles/_extra_settings.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..d1a4d84c3cc4db2b993de78ff6339b4676cc394f --- /dev/null +++ b/jh/app/views/profiles/_extra_settings.html.haml @@ -0,0 +1,3 @@ +- f = local_assigns.fetch(:form) + += render 'devise/shared/phone_verification', form: f \ No newline at end of file diff --git a/jh/config/initializers/tencent_sms.rb b/jh/config/initializers/tencent_sms.rb new file mode 100644 index 0000000000000000000000000000000000000000..b09c918b8fe829d8f8af3c0cab24c441970f4977 --- /dev/null +++ b/jh/config/initializers/tencent_sms.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'tencentcloud-sdk-common' +require 'tencentcloud-sdk-sms' + +module TencentSms + include TencentCloud::Common + include TencentCloud::Sms::V20210111 + def self.send_code(phone) + cre = Credential.new(ENV['TC_ID'], ENV['TC_KEY']) + cli = Client.new(cre, 'ap-beijing') + + random_code = (SecureRandom.random_number(9e5) + 1e5).to_i.to_s + + phonenumberset = ["+86#{phone}"] + smssdkappid = "1400551828" + templateid = "1052297" + signname = "极狐GitLab" + templateparamset = [random_code, "10"] + extendcode = nil + sessioncontext = nil + senderid = nil + + req = SendSmsRequest.new( + phonenumberset, + smssdkappid, + templateid, + signname, + templateparamset, + extendcode, + sessioncontext, + senderid + ) + cli.SendSms(req) + random_code + rescue TencentCloudSDKException => e + Gitlab::AppLogger.error(e) + nil + end +end diff --git a/jh/locale/en/gitlab.po b/jh/locale/en/gitlab.po index a0721f0a59c6be5073ae18c48185ded7b0db3911..5f95b62f5a2a0d4b295c07b24858bf1420c69010 100644 --- a/jh/locale/en/gitlab.po +++ b/jh/locale/en/gitlab.po @@ -28,3 +28,12 @@ msgstr "This is a self-managed instance of JiHu GitLab." msgid "Promotions|Improve repositories with GitLab Enterprise Edition." msgstr "Promotions|Improve repositories with JiHu GitLab Enterprise Edition." + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "By clicking %{button_text}, I agree that I have read and accepted the %{link_start}JiHu GitLab Terms of Use and Non-active Account Policy%{link_end}" + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Non-active Account Policy%{link_end}" + +msgid "Accept terms" +msgstr "Verify phone and accept terms" diff --git a/jh/locale/zh_CN/gitlab.po b/jh/locale/zh_CN/gitlab.po index ce309a042200807dbdbd3463a72c83c9479d4219..d826742f01b39ea8c0367a6b0207596b95732482 100644 --- a/jh/locale/zh_CN/gitlab.po +++ b/jh/locale/zh_CN/gitlab.po @@ -34,3 +34,46 @@ msgstr "这是一个在本地托管的极狐GitLab实例。" msgid "Promotions|Improve repositories with GitLab Enterprise Edition." msgstr "升级到极狐GitLab企业版以获得更多仓库功能。" + +msgid "Get code" +msgstr "发送验证码" + +msgid "Resend in %{wait}s" +msgstr "%{wait}秒后重发" + +msgid "Phone" +msgstr "手机号码" + +msgid "Verification code" +msgstr "验证码" + +msgid "Please provide a valid phone number." +msgstr "手机号码格式有误" + +msgid "Phone is already taken." +msgstr "手机号已存在" + +msgid "Phone is available." +msgstr "手机号可用" + +msgid "Checking phone availability..." +msgstr "检测手机号有效性中..." + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "注册 %{button_text}即代表您同意%{link_start}《极狐GitLab 用户服务协议》和《极狐GitLab 非活跃帐号处理规范》%{link_end}" + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "注册 %{button_text}即代表您同意%{link_start}《极狐GitLab 用户服务协议》和《极狐GitLab 非活跃帐号处理规范》%{link_end}" + +msgid "Please verify your phone before continuing." +msgstr "请您在继续使用前验证手机号" + +msgid "Accept terms" +msgstr "验证手机并同意" + +msgid "You have already accepted the terms and verified your phone." +msgstr "您已验证手机号并且同意用户协议" + +msgid "An error occurred while sending verification code" +msgstr "发送验证码时发生错误" + diff --git a/jh/locale/zh_HK/gitlab.po b/jh/locale/zh_HK/gitlab.po index fb2ec047c9943605bd4f4cb20e2843fdb82f5c09..accd2192e79a9edb922eb029d6cdfe0b0747153c 100644 --- a/jh/locale/zh_HK/gitlab.po +++ b/jh/locale/zh_HK/gitlab.po @@ -34,3 +34,45 @@ msgstr "這是一個在本地託管的極狐GitLab實例。" msgid "Promotions|Improve repositories with GitLab Enterprise Edition." msgstr "升級到極狐GitLab企業版以獲得更多倉庫功能。" + +msgid "Get code" +msgstr "發送驗證碼" + +msgid "Resend in %{wait}s" +msgstr "%{wait}秒後重發" + +msgid "Phone" +msgstr "手機號碼" + +msgid "Verification code" +msgstr "驗證碼" + +msgid "Please provide a valid phone number." +msgstr "手機號碼格式有誤" + +msgid "Phone is already taken." +msgstr "手機號已存在" + +msgid "Phone is available." +msgstr "手機號可用" + +msgid "Checking phone availability..." +msgstr "檢測手機號有效性中..." + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "註冊 %{button_text}即代表您同意《極狐GitLab 用戶服務協議》%{link_start}和《極狐GitLab 非活躍帳號處理規範》%{link_end}" + +msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}" +msgstr "註冊 %{button_text}即代表您同意《極狐GitLab 用戶服務協議》%{link_start}和《極狐GitLab 非活躍帳號處理規範》%{link_end}" + +msgid "Please verify your phone before continuing." +msgstr "請您在繼續使用前驗證手機號" + +msgid "Accept terms" +msgstr "驗證手機並同意" + +msgid "You have already accepted the terms and verified your phone." +msgstr "您已驗證手機號並且同意用戶協議" + +msgid "An error occurred while sending verification code" +msgstr "發送驗證碼時發生錯誤" diff --git a/jh/spec/controllers/application_controller_spec.rb b/jh/spec/controllers/application_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..88d96bea40ca256a823102b1cdbd8082d37cbfd4 --- /dev/null +++ b/jh/spec/controllers/application_controller_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ApplicationController do + include TermsHelper + + let(:user) { create(:user) } + + context 'terms' do + controller(described_class) do + def index + render html: 'authenticated' + end + end + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in user + end + + context 'when enforce_terms and enforce_phone_verification also' do + before do + enforce_terms + end + + it 'shows a message when the user accepted terms and did not verify phone' do + accept_terms(user) + + get :index + + expect(controller).to set_flash[:notice].to(/Please verify your phone before continuing./) + end + + it 'does not redirect when the user accepted terms and verified phone' do + accept_terms(user) + user.phone = '15688886666' + user.save + + get :index + + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end diff --git a/jh/spec/controllers/jh/registrations_controller_spec.rb b/jh/spec/controllers/jh/registrations_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d19fc081ed538baef99a6d67ecd8344025cb2e2f --- /dev/null +++ b/jh/spec/controllers/jh/registrations_controller_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RegistrationsController do + # TODO +end diff --git a/jh/spec/controllers/jh/users_controller_spec.rb b/jh/spec/controllers/jh/users_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebd4c26e124cfd44e85b91f38dc2a63462fe6c52 --- /dev/null +++ b/jh/spec/controllers/jh/users_controller_spec.rb @@ -0,0 +1 @@ +# frozen_string_literal: true \ No newline at end of file diff --git a/jh/spec/controllers/profiles_controller_spec.rb b/jh/spec/controllers/profiles_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9238afef1d29344a1908f21867f48be3e1356bf3 --- /dev/null +++ b/jh/spec/controllers/profiles_controller_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require('spec_helper') + +RSpec.describe ProfilesController, :request_store do + let(:user) { create(:user) } + + let(:verification_code) do + VerificationCode.create( + visitor_id: 'c3ef19f4-ea91-4898-8289-5639e6abxxxx', + code: '888888', + phone: '15688886666' + ) + end + + describe 'PUT update' do + it 'allows an phone update from a user with verification code' do + sign_in(user) + + cookies[:visitor_id] = verification_code.visitor_id + + put :update, + params: { user: { phone: verification_code.phone, verification_code: verification_code.code } } + + user.reload + + expect(response).to have_gitlab_http_status(:found) + expect(user.phone).to eq(verification_code.phone) + end + end +end diff --git a/jh/spec/controllers/users/terms_controller_spec.rb b/jh/spec/controllers/users/terms_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5712344791e3012637c711f13ee7dadb284b1beb --- /dev/null +++ b/jh/spec/controllers/users/terms_controller_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::TermsController do + include TermsHelper + + let_it_be(:user) { create(:user) } + + let(:term) { create(:term) } + + let(:verification_code) do + VerificationCode.create( + visitor_id: 'c3ef19f4-ea91-4898-8289-5639e6abxxxx', + code: '888888', + phone: '15688886666' + ) + end + + before do + sign_in user + end + + describe 'GET #index' do + context 'when a user is signed in' do + context 'when terms exist' do + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + term + end + + it 'shows a message when the user already accepted the terms and verified the phone' do + accept_terms(user) + + get :index + expect(controller).to set_flash.now[:notice].to(/You have already accepted the terms but need to verify your phone/) + end + + it 'shows a message when the user already accepted the terms and did not verify the phone' do + accept_terms(user) + + user.phone = verification_code.phone + user.save + get :index + expect(controller).to set_flash.now[:notice].to(/You have already accepted the terms and verified your phone./) + end + end + end + end + + describe 'POST #accept' do + context 'when a user is signed in' do + it 'saves that the user accepted the terms and verified phone' do + post :accept, params: { id: term.id, phone: verification_code.phone, verification_code: verification_code.phone } + cookies[:visitor_id] = verification_code.visitor_id + # rubocop: disable CodeReuse/ActiveRecord + agreement = user.term_agreements.find_by(term: term) + # rubocop: enable CodeReuse/ActiveRecord + expect(agreement.accepted).to eq(true) + expect(user.phone_verified?).to eq(true) + end + end + end +end