diff --git a/Gemfile b/Gemfile index 0b02568e..0a522dcc 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,8 @@ gem 'turbo-rails' gem 'stimulus-rails' # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem 'jbuilder' +# Allows returning free formed structs +gem 'ostruct' # Used to differentaite text gem 'diff-lcs' diff --git a/Gemfile.lock b/Gemfile.lock index 9b93d98c..d7f4a4cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,6 +208,7 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) + ostruct (0.6.1) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) @@ -405,6 +406,7 @@ DEPENDENCIES htmldiff importmap-rails jbuilder + ostruct pg propshaft puma (>= 5.0) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index e831d08c..84fe926d 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -178,25 +178,19 @@ def handle_add_lecturers begin ActiveRecord::Base.transaction do - create_lecturer_enrolments(lecturer_emails, @course, unregistered_lecturers, registered_lecturers) + create_lecturer_enrolments(lecturer_emails, @course) end rescue StandardError => e redirect_back_or_to '/', alert: e.message return end - send_invite_emails(unregistered_lecturers) - send_notification_emails(registered_lecturers, @course) - redirect_to course_path(@course) end def handle_add_students authorize @course, :manage_students? - unregistered_students = Set[] - registered_students = [] - if params[:csv_file].blank? || params[:csv_file].content_type != 'text/csv' redirect_back_or_to '/', alert: 'Please provide a CSV file from ebwise' return @@ -233,10 +227,10 @@ def handle_add_students if @course.grouped student_hashmap = @course.parse_csv_grouped(csv_obj) - create_db_entries_grouped(student_hashmap, @course, unregistered_students, registered_students) + create_db_entries_grouped(student_hashmap, @course) else student_set = @course.parse_csv_solo(csv_obj) - create_db_entries_solo(student_set, @course, unregistered_students, registered_students) + create_db_entries_solo(student_set, @course) end end rescue StandardError => e @@ -244,8 +238,6 @@ def handle_add_students return end - send_invite_emails(unregistered_students) - send_notification_emails(registered_students, @course) redirect_to course_path(@course) end @@ -531,37 +523,37 @@ def load_capacity_result @capacity_result = SupervisorCapacityCalculator.new(@course).calculate end - def create_db_entries_grouped(hash_map, parent_course, unregistered_students, registered_students) + def create_db_entries_grouped(hash_map, parent_course) hash_map.keys.each do |group| new_group = ProjectGroup.find_or_create_by!(group_name: group, course: parent_course) hash_map[group].each do |group_member| new_user = User.find_by(email_address: group_member[:email_address]) - if new_user - new_user.update!(instid: group_member[:instid]) - - registered_students.push(group_member[:email_address]) if new_user.enrolments.where(course: parent_course).empty? - else - new_user = User.create!( - email_address: group_member[:email_address], + if !new_user + result = UserCreator.call( name: group_member[:name], + email: group_member[:email_address], password: SecureRandom.base64(24), - has_registered: false, + verify_only: false, instid: group_member[:instid] ) - new_otp_instance = Otp.create!( - user: new_user, - token: SecureRandom.uuid - ) + GeneralMailer.with( + email_address: group_member[:email], + otp_token: result.otp_instance.token, + ).ProPro_Invite.deliver_later - unregistered_students.add( - { - email_address: group_member[:email_address], - otp_token: new_otp_instance.token - } - ) + new_user = result.user + else + new_user.update!(instid: group_member[:instid]) + + if new_user.enrolments.where(course: parent_course).empty? + GeneralMailer.with( + course: parent_course, + email_address: group_member[:email] + ).Course_Invite_Notification.deliver_later + end end Enrolment.find_or_create_by!( @@ -585,54 +577,34 @@ def create_db_entries_grouped(hash_map, parent_course, unregistered_students, re end end - def send_invite_emails(unregistered_users) - unregistered_users.each do |user| - GeneralMailer.with( - email_address: user[:email_address], - otp_token: user[:otp_token], - from_course: true - ).ProPro_Invite.deliver_later - end - end - - def send_notification_emails(registered_emails, course) - registered_emails.each do |email| - GeneralMailer.with( - course: course, - email_address: email - ).Course_Invite_Notification.deliver_later - end - end - - def create_db_entries_solo(student_set, parent_course, unregistered_students, registered_students) + def create_db_entries_solo(student_set, parent_course) student_set.each do |student| new_user = User.find_by(email_address: student[:email_address]) - if new_user - new_user.update!(instid: student[:instid]) - - registered_students.push(student[:email_address]) if new_user.enrolments.where(course: parent_course).empty? - else - new_user = User.create!( - email_address: student[:email_address], + if !new_user + result = UserCreator.call( name: student[:name], + email: student[:email_address], password: SecureRandom.base64(24), - has_registered: false, + verify_only: false, instid: student[:instid] ) - new_otp_instance = Otp.create!( - user: new_user, - token: SecureRandom.uuid - ) + GeneralMailer.with( + email_address: student[:email_address], + otp_token: result.otp_instance.token, + ).ProPro_Invite.deliver_later - unregistered_students.add( - { - email_address: student[:email_address], - otp_token: new_otp_instance.token, - otp: new_otp_instance.otp - } - ) + new_user = result.user + else + new_user.update!(instid: student[:instid]) + + if new_user.enrolments.where(course: parent_course).empty? + GeneralMailer.with( + course: parent_course, + email_address: student[:email] + ).Course_Invite_Notification.deliver_later + end end Enrolment.find_or_create_by!( @@ -643,33 +615,32 @@ def create_db_entries_solo(student_set, parent_course, unregistered_students, re end end - def create_lecturer_enrolments(lecturer_emails, parent_course, unregistered_lecturers, registered_lecturers) + def create_lecturer_enrolments(lecturer_emails, parent_course) lecturer_emails.each do |email| next if email.blank? new_lecturer = User.find_by(email_address: email) if !new_lecturer - new_lecturer = User.create!( - email_address: email, + result = UserCreator.call( + name: "Lecturer-#{SecureRandom.hex(2)}", + email: email, password: SecureRandom.base64(24), - has_registered: false, - name: "Lecturer-#{SecureRandom.hex(2)}" + verify_only: false ) - new_otp_instance = Otp.create!( - user: new_lecturer, - token: SecureRandom.uuid - ) + GeneralMailer.with( + email_address: email, + otp_token: result.otp_instance.token, + ).ProPro_Invite.deliver_later + + new_lecturer = result.user - unregistered_lecturers.add( - { - email_address: email, - otp_token: new_otp_instance.token - } - ) elsif new_lecturer.enrolments.where(course: parent_course).empty? - registered_lecturers.push(email) + GeneralMailer.with( + course: parent_course, + email_address: email + ).Course_Invite_Notification.deliver_later end Enrolment.find_or_create_by!( diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 220710ba..b0e0cc3b 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -1,5 +1,5 @@ class UserController < ApplicationController - allow_unauthenticated_access only: %i[new create claim handle_claim] + allow_unauthenticated_access only: %i[new create claim handle_claim verify] def resend_invite user = User.find(params[:id]) @@ -14,7 +14,6 @@ def resend_invite GeneralMailer.with( email_address: user.email_address, otp_token: otp_instance.token, - from_course: false ).ProPro_Invite.deliver_later redirect_back_or_to '/', notice: "Invitation resent to #{user.email_address}" @@ -22,6 +21,32 @@ def resend_invite def new; end + def create + name = params[:name].strip + email = params[:email].strip + + result = UserDetailsValidator.call(name: name, password: params[:password], password_confirmation: params[:password_confirmation]) + + unless result.success? + redirect_to user_new_path, alert: result.message + return + end + + result = UserCreator.call(name: name, email: email, password: params[:password], verify_only: true) + + unless result.success? + redirect_back_or_to user_new_path, alert: result.message + return + end + + GeneralMailer.with( + email_address: email, + otp_token: result.otp_instance.token, + ).Signup_Verification.deliver_later + + redirect_to login_path, notice: 'Account created successfully. Check your inbox!' + end + def edit @user = Current.user @@ -57,41 +82,15 @@ def edit end def claim - @email = Otp.find_by(token: params[:token]).user.email_address + @email = Otp.find_by(token: params[:token], verify_only: false).user.email_address rescue StandardError redirect_to login_path, alert: "Invalid token, perhaps you've already claimed your account? Try logging in." end def handle_claim - response = params.permit(:password, :password_confirmation, :name, :instid, :token) - return if response[:token].blank? - - if response[:password].blank? - redirect_back_or_to '/', alert: 'Password cannot be empty' - return - end - - if response[:password_confirmation].blank? - redirect_back_or_to '/', alert: 'Password confirmation cannot be empty' - return - end - - if response[:instid].blank? - redirect_back_or_to '/', alert: 'Institution ID cannot be empty' - return - end + return if params[:token].blank? - if response[:password] != response[:password_confirmation] - redirect_back_or_to '/', alert: 'Passwords are not the same' - return - end - - if response[:password].length > 72 - redirect_back_or_to '/', alert: 'Password must be less than or equal to 72 characters' - return - end - - otp_instance = Otp.find_by(token: response[:token]) + otp_instance = Otp.find_by(token: params[:token], verify_only: false) unless otp_instance redirect_back_or_to '/', alert: 'Something went wrong' @@ -99,57 +98,52 @@ def handle_claim end user = otp_instance.user + name = params[:name].strip - if response[:name].blank? - redirect_back_or_to '/', alert: 'Name cannot be empty' + result = UserDetailsValidator.call(name: name, password: params[:password], password_confirmation: params[:password_confirmation]) + + if !result.success? + redirect_back_or_to '/', alert: result.message return end - if user.update!(has_registered: true, password: response[:password], name: response[:name].strip, instid: response[:instid].strip) - redirect_to '/session/new', notice: 'Account successfully claimed' - else + begin + user.update!(has_registered: true, name: name, password: params[:password]) + otp_instance.destroy + rescue ActiveRecord::RecordInvalid => e + redirect_back_or_to '/', alert: e.message + return + rescue StandardError => e redirect_back_or_to '/', alert: 'Something went wrong' + return end - user.otp.destroy + redirect_to '/session/new', notice: 'Account successfully claimed' end - def create - email = params.require(:email_address).strip + def verify + return if params[:token].blank? + + otp_instance = Otp.find_by(token: params[:token], verify_only: true) - if User.find_by(email_address: email) - redirect_to user_profile_path, notice: 'Your email is already in the system. Check your inbox or login!' + unless otp_instance + redirect_back_or_to new_session_path, alert: 'Something went wrong' return end + user = otp_instance.user + begin ActiveRecord::Base.transaction do - new_user = User.create!( - email_address: email, - name: 'Placeholder Username', - password: SecureRandom.base64(24), - has_registered: false - ) - - Otp.create!( - user: new_user, - token: SecureRandom.uuid - ) + user.update!(has_registered: true) + otp_instance.destroy end rescue StandardError => e - redirect_back_or_to '/', alert: e.message + redirect_to new_session_path, alert: 'Something went wrong' return end - new_user = User.find_by(email_address: email) - - GeneralMailer.with( - email_address: new_user.email_address, - otp_token: new_user.otp.token, - from_course: false - ).ProPro_Invite.deliver_later - - redirect_to login_path, notice: 'Account created successfully. Check your inbox!' + redirect_to new_session_path, notice: 'Account successfully verified' end def profile diff --git a/app/mailers/general_mailer.rb b/app/mailers/general_mailer.rb index 43d4f4f4..7e414355 100644 --- a/app/mailers/general_mailer.rb +++ b/app/mailers/general_mailer.rb @@ -2,10 +2,15 @@ class GeneralMailer < ApplicationMailer def ProPro_Invite @otp_token = params[:otp_token] @email_address = params[:email_address] - @from_course = params[:from_course] mail(to: @email_address, Subject: 'Invitation for ProPro') end + def Signup_Verification + @otp_token = params[:otp_token] + @email_address = params[:email_address] + mail(to: @email_address, Subject: "Verify Your Account on ProPro") + end + def Project_Status_Updated @course = params[:course] @project = params[:project] diff --git a/app/models/user.rb b/app/models/user.rb index 0dac08a3..901f1025 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,6 +17,7 @@ class User < ApplicationRecord has_many :solo_projects, as: :owner, class_name: 'Project' has_many :group_projects, through: :project_groups, source: :project - validates :email_address, presence: { message: 'cannot be empty' }, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create } + validates :email_address, presence: { message: 'cannot be empty' }, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }, uniqueness: { case_sensitive: false } validates :password, length: { maximum: 72, message: 'must be less than 72 characters' } + validates :name, presence: { message: 'cannot be blank' } end diff --git a/app/services/user_creator.rb b/app/services/user_creator.rb new file mode 100644 index 00000000..3c9f4e79 --- /dev/null +++ b/app/services/user_creator.rb @@ -0,0 +1,39 @@ +require 'ostruct' + +class UserCreator + def self.call(name:, email:, password:, verify_only:, instid: nil) + name = name.strip + email = email.strip + + if instid + instid = instid.strip + end + + new_user = nil + new_otp = nil + + begin + ActiveRecord::Base.transaction do + new_user = User.create!( + name: name, + email_address: email, + password: password, + instid: instid, + has_registered: false + ) + + new_otp = Otp.create!( + user: new_user, + token: SecureRandom.uuid, + verify_only: verify_only + ) + end + rescue ActiveRecord::RecordNotUnique + return OpenStruct.new(success?: false, message: 'Your email is already in the system. Check your inbox or login!') + rescue StandardError => e + return OpenStruct.new(success?: false, message: e.message) + end + + return OpenStruct.new(success?: true, user: new_user, otp_instance: new_otp) + end +end diff --git a/app/services/user_details_validator.rb b/app/services/user_details_validator.rb new file mode 100644 index 00000000..c75658ff --- /dev/null +++ b/app/services/user_details_validator.rb @@ -0,0 +1,23 @@ +require 'ostruct' + +class UserDetailsValidator + def self.call(name:, password:, password_confirmation:) + if name.blank? + return OpenStruct.new(success?: false, message: 'Name cannot be empty') + end + + if password.blank? + return OpenStruct.new(success?: false, message: 'Password cannot be empty') + end + + if password_confirmation.blank? + return OpenStruct.new(success?: false, message: 'Password confirmation cannot be empty') + end + + if password != password_confirmation + return OpenStruct.new(success?: false, message: 'Passwords are not the same') + end + + return OpenStruct.new(success?: true) + end +end diff --git a/app/views/general_mailer/ProPro_Invite.html.erb b/app/views/general_mailer/ProPro_Invite.html.erb index d34fc3c8..982c3c6f 100644 --- a/app/views/general_mailer/ProPro_Invite.html.erb +++ b/app/views/general_mailer/ProPro_Invite.html.erb @@ -26,31 +26,21 @@ diff --git a/app/views/general_mailer/ProPro_Invite.text.erb b/app/views/general_mailer/ProPro_Invite.text.erb index baeab90f..1bdf9989 100644 --- a/app/views/general_mailer/ProPro_Invite.text.erb +++ b/app/views/general_mailer/ProPro_Invite.text.erb @@ -1,10 +1,6 @@ Activate Your Account on ProPro! ======================================================================= -<% if @from_course %> You have been invited and enrolled into a ProPro course. -<% else %> -Your email has been used to register for an account on ProPro -<% end %> ProPro is a web app for Project Proposals and Progress monitoring. It is ideal for educators managing student projects where students propose their ideas for approval! You have been added to a course via your email address. An account have automatically been created for you. To claim your account, visit: diff --git a/app/views/general_mailer/Signup_Verification.html.erb b/app/views/general_mailer/Signup_Verification.html.erb new file mode 100644 index 00000000..7f6ef060 --- /dev/null +++ b/app/views/general_mailer/Signup_Verification.html.erb @@ -0,0 +1,64 @@ + + + + + + Verify Your Account on ProPro! + + + +
- <% if @from_course %> -

- You have been invited and enrolled into a ProPro course. -

- <% else %> -

- Your email has been used to register for an account on ProPro -

- <% end %> +

+ You have been invited and enrolled into a ProPro course. +

ProPro is a web app for Project Proposals and Progress monitoring. It is ideal for educators managing student projects where students propose their ideas for approval!

- <% if @from_course %> -

- You have been added to a course via your email address. An account have automatically been created for you. To claim your account, visit: -

- <% end %> +

+ You have been added to a course via your email address. An account have automatically been created for you. To claim your account, visit: +

To claim your account, visit:

- - <%= user_claim_url(token: @otp_token) %> - +

+ + + +
+ + + + + + + + + + +
+

+ Verify Your Account on ProPro! +

+
+ + + + + + + + +
+

+ Your email has been used to register for an account on ProPro +

+

+ ProPro is a web app for Project Proposals and Progress monitoring. It is ideal for educators managing student projects where students propose their ideas for approval! +

+ +

+ To verify your account, visit: +

+

+ +

+
+

+ This is for YOUR account activation only. Do not share this information with a third-party. +

+

+ Welcome to ProPro and we hope you have a great project experience!
+ - ProPro Team +

+
+
+
+ + diff --git a/app/views/general_mailer/Signup_Verification.text.erb b/app/views/general_mailer/Signup_Verification.text.erb new file mode 100644 index 00000000..fae8bde1 --- /dev/null +++ b/app/views/general_mailer/Signup_Verification.text.erb @@ -0,0 +1,7 @@ +Verify Your Account on ProPro! +======================================================================= +Your email has been used to register for an account on ProPro +ProPro is a web app for Project Proposals and Progress monitoring. It is ideal for educators managing student projects where students propose their ideas for approval! + +To verify your account, visit: +<%= user_verify_url(token: @otp_token) %> diff --git a/app/views/user/claim.html.erb b/app/views/user/claim.html.erb index 2bb7d6b2..c3e2aa39 100644 --- a/app/views/user/claim.html.erb +++ b/app/views/user/claim.html.erb @@ -1,26 +1,73 @@ -<% content_for :stylesheets do %> - <%= stylesheet_link_tag "courses" %> - <%= stylesheet_link_tag "tailwind", "new_tailwind" %> -<% end %> -<% content_for :title, "New Lecturer | ProPro" %> - -
-

New User

-
- -
-
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
-
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
-
- - <%= form_with url: user_handle_claim_path do |form| %> - <%= form.hidden_field :token, value: params[:token] %> - <%= form.search_field :email, value: @email, disabled: true %>
- <%= form.search_field :instid, required: true, placeholder: "Institution ID" %>
- <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
- <%= form.password_field :password_confirmation, required: true, placeholder: "Enter password confirmation", maxlength: 72 %>
- <%= form.search_field :name, placeholder: "Name", required: true %>
- <%= form.submit "Claim Account" %> - <% end %> +<% content_for :title, "Claim Account | ProPro" %> +<% content_for :no_sidebar, true %> +<% content_for :no_header, true %> +<% content_for :no_footer, true %> +<% content_for :hide_toggler, true %> +<% content_for :hide_breadcrumbs, true %> +<% content_for :body_class, "bg-surface-tint" %> + +
+
+ + +
+

Claim Your Account

+

+ Complete your details to set up your ProPro account. +

+
+ + +
+ + + <% if flash[:alert].present? %> +
+ error +
<%= flash[:alert] %>
+
+ <% end %> + + <% if flash[:notice].present? %> +
+ check_circle +
<%= flash[:notice] %>
+
+ <% end %> + + + <%= form_with url: user_handle_claim_path, class: "space-y-5" do |form| %> + <%= form.hidden_field :token, value: params[:token] %> + + +
+ + <%= form.search_field :email, value: @email, disabled: true, class: "w-full px-3.5 py-2.5 bg-surface-variant text-on-surface-muted border border-outline-variant rounded-lg text-[14px] cursor-not-allowed" %> +
+ + +
+ + <%= form.search_field :name, placeholder: "Name", required: true, class: "w-full px-3.5 py-2.5 bg-surface text-on-surface border border-outline rounded-lg text-[14px] focus:outline-none focus:border-primary transition-colors" %> +
+ + +
+ + <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "w-full px-3.5 py-2.5 bg-surface text-on-surface border border-outline rounded-lg text-[14px] focus:outline-none focus:border-primary transition-colors" %> +
+ + +
+ + <%= form.password_field :password_confirmation, required: true, placeholder: "Enter password confirmation", maxlength: 72, class: "w-full px-3.5 py-2.5 bg-surface text-on-surface border border-outline rounded-lg text-[14px] focus:outline-none focus:border-primary transition-colors" %> +
+ + +
+ <%= form.submit "Claim Account", class: "w-full bg-primary hover:bg-primary-hover text-white py-2.5 rounded-full font-medium text-[14px] transition-colors cursor-pointer text-center" %> +
+ <% end %> +
diff --git a/app/views/user/new.erb b/app/views/user/new.erb index ab5a9faf..854181a2 100644 --- a/app/views/user/new.erb +++ b/app/views/user/new.erb @@ -1,9 +1,6 @@ -<% content_for :stylesheets do %> - <%= stylesheet_link_tag "tailwind", "new_tailwind" %> -<% end %> <% content_for :hide_toggler, true %> <% content_for :hide_breadcrumbs, true %> -<% content_for :title, "Sign In | ProPro" %> +<% content_for :title, "Sign Up | ProPro" %> <% provide :no_footer, true %>
<% end %> - <%= form_with url: user_create_path, class: "space-y-5" do |form| %> - + <%= form_with url: user_create_path, data: { turbo: false }, class: "space-y-5" do |form| %>
- <%= form.label :email_address, + <%= form.label :email, "Email Address", class: "block text-base font-semibold text-gray-700 mb-1" %> - <%= form.email_field :email_address, + <%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", @@ -99,6 +95,50 @@ "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-1 focus:ring-green-400 focus:border-green-400 outline-none transition-all text-gray-900 placeholder-gray-400" %>
+
+
+ <%= form.label :name, + "Name", + class: "block text-base font-semibold text-gray-700" %> +
+ + <%= form.text_field :name, + required: true, + placeholder: "Enter your username", + class: + "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-1 focus:ring-green-400 focus:border-green-400 outline-none transition-all text-gray-900 placeholder-gray-400" %> +
+ +
+
+ <%= form.label :password, + "Password", + class: "block text-base font-semibold text-gray-700" %> +
+ + <%= form.password_field :password, + required: true, + autocomplete: "current-password", + placeholder: "Enter your password", + class: + "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-1 focus:ring-green-400 focus:border-green-400 outline-none transition-all text-gray-900 placeholder-gray-400" %> +
+ +
+
+ <%= form.label :password_confirmation, + "Password Confirmation", + class: "block text-base font-semibold text-gray-700" %> +
+ + <%= form.password_field :password_confirmation, + required: true, + autocomplete: "current-password", + placeholder: "Re-enter your password", + class: + "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-1 focus:ring-green-400 focus:border-green-400 outline-none transition-all text-gray-900 placeholder-gray-400" %> +
+
<%= form.submit "Sign Up", class: diff --git a/config/routes.rb b/config/routes.rb index f7a01753..3cbceb9b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,7 @@ post 'user/handle_claim' post 'user/edit' post 'user/:id/resend_invite', to: 'user#resend_invite', as: :resend_invite + get 'user/verify' resources :enrolments, only: [:destroy] post 'invite', to: 'courses#enroll_via_coursecode', as: 'invite' diff --git a/db/migrate/20261001123305_add_verify_only_to_otp.rb b/db/migrate/20261001123305_add_verify_only_to_otp.rb new file mode 100644 index 00000000..3e0c6e9c --- /dev/null +++ b/db/migrate/20261001123305_add_verify_only_to_otp.rb @@ -0,0 +1,5 @@ +class AddVerifyOnlyToOtp < ActiveRecord::Migration[8.0] + def change + add_column :otps, :verify_only, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 45c5a861..750b2382 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[8.0].define(version: 2026_08_29_082339) do +ActiveRecord::Schema[8.0].define(version: 2026_10_01_123305) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -74,6 +74,7 @@ t.datetime "updated_at", null: false t.string "token", null: false t.integer "user_id", null: false + t.boolean "verify_only", default: false, null: false t.index ["user_id"], name: "index_otps_on_user_id" end