diff --git a/app/controllers/devise/omniauth_callbacks_controller.rb b/app/controllers/devise/omniauth_callbacks_controller.rb index 98a607a8..23ff20eb 100644 --- a/app/controllers/devise/omniauth_callbacks_controller.rb +++ b/app/controllers/devise/omniauth_callbacks_controller.rb @@ -1,9 +1,48 @@ class Devise::OmniauthCallbacksController < ApplicationController + rescue_from AuthUser::MissingAuthInfoError, with: :missing_auth_info + rescue_from ActiveRecord::RecordInvalid, with: :validation_failed + def neopass - render plain: request.env["omniauth.auth"].uid + @auth_user = AuthUser.from_omniauth(request.env["omniauth.auth"]) + + if @auth_user.previously_new_record? + flash[:notice] = + "Welcome to Dress to Impress, #{@auth_user.name}! We've set up a DTI " + + "account for you. Click Settings in the top right to learn more, or " + + "just go ahead and get started!" + else + flash[:notice] = "Welcome back, #{@auth_user.name}! 💖" + end + + sign_in_and_redirect @auth_user, event: :authentication end def failure - render plain: "Failure" + flash[:warning] = + "Hrm, something went wrong in our connection to NeoPass, sorry! " + + "Maybe wait a moment and try again?" + redirect_to new_auth_user_session_path + end + + def missing_auth_info(error) + Sentry.capture_exception error + Rails.logger.error error.full_message + + flash[:warning] = + "Hrm, our connection with NeoPass didn't return the information we " + + "usually expect, sorry! If this keeps happening, please email me at " + + "matchu@openneo.net so I can help investigate!" + redirect_to new_auth_user_session_path + end + + def validation_failed(error) + Sentry.capture_exception error + Rails.logger.error error.full_message + + flash[:warning] = + "Hrm, the connection with NeoPass worked, but we had trouble saving " + + "your account, sorry! If this keeps happening, please email me at " + + "matchu@openneo.net so I can help investigate!" + redirect_to new_auth_user_session_path end end diff --git a/app/models/auth_user.rb b/app/models/auth_user.rb index 0dacacfe..89b4a226 100644 --- a/app/models/auth_user.rb +++ b/app/models/auth_user.rb @@ -27,4 +27,59 @@ class AuthUser < AuthRecord user.name = name user.save! end + + def uses_omniauth? + provider? && uid? + end + + def email_required? + !uses_omniauth? + end + + def password_required? + !uses_omniauth? + end + + def self.from_omniauth(auth) + raise MissingAuthInfoError, "Username missing" if auth.uid.blank? + raise MissingAuthInfoError, "Email missing" if auth.info.email.blank? + + transaction do + find_or_create_by!(provider: auth.provider, uid: auth.uid) do |user| + # Use the Neopets username if possible, or a unique username if not. + dti_username = build_unique_username_like(auth.uid) + user.name = dti_username + + # Copy the email address from their Neopets account to their DTI + # account, unless they already have a DTI account with this email, in + # which case, ignore it. (It's primarily for their own convenience with + # password recovery!) + email_exists = AuthUser.where(email: auth.info.email).exists? + user.email = auth.info.email unless email_exists + end + end + end + + def self.build_unique_username_like(name) + name_query = sanitize_sql_like(name) + "%" + similar_names = where("name LIKE ?", name_query).pluck(:name) + + # Use the given name itself, if we can. + return name unless similar_names.include?(name) + + # If not, try appending "-neopass". + return "#{name}-neopass" unless similar_names.include?("#{name}-neopass") + + # After that, try appending "-neopass-1", "-neopass-2", etc, until a + # unique name arises. (We don't expect this to happen basically ever, but + # it's nice to have a guarantee!) + max = similar_names.size + 1 + candidates = (1..max).map { |n| "#{name}-neopass-#{n}"} + numerical_name = candidates.find { |name| !similar_names.include?(name) } + return numerical_name unless numerical_name.nil? + + raise "Failed to build unique username (shouldn't be possible?)" + end + + class MissingAuthInfoError < ArgumentError;end end \ No newline at end of file diff --git a/bin/neopass-server b/bin/neopass-server index 6e61576d..ba0713b7 100755 --- a/bin/neopass-server +++ b/bin/neopass-server @@ -21,9 +21,10 @@ const urlLib = require("node:url"); const { OAuth2Server } = require("oauth2-mock-server"); const express = require("express"); -// This is the Neopets username we'll report back to DTI when you authenticate -// through here. -const USERNAME = "theneopetsteam"; +// This is the Neopets username and email we'll report back to DTI when you +// authenticate through here. +const USERNAME = "test"; +const EMAIL = "theneopetsteam@neopets.com"; const certPath = pathLib.join(__dirname, "..", "tmp", "localhost.pem"); const keyPath = pathLib.join(__dirname, "..", "tmp", "localhost-key.pem"); @@ -88,10 +89,14 @@ async function startServer(port) { n: "svVfGU4NGcfBCmQiIOW5uzg5SAN2CWSIQSstnhqZoCdjy5OoKpKVR8O9TbDvxixrvkFyAav90Q0Xse8iFTcjfCKuqINYiuYMXhCvfBlc_DVVOQca9pMpN03LaDofd5Ll4_BFTtt1nSPahwWU7xDM-Bkkh_TcS2qS4N2xbpEGi0q0ZkrJN4WyiDBC2k9WbK-YHr4Rj4JKypFVSeBIrjxVPmlPzgfqlLGGIB0l92SnJDXDMlkWcCCTyLgqSBM04nkxGDSykq_ei76qCdRd7b10wMBaoS9DeBThAyHpur2LoPdH3gxbcwoWExi-jPlNP1LdKVZD8b95OY3CRyMAAMGdKQ", }); - server.service.on("beforeTokenSigning", (token, req) => { + server.service.on("beforeTokenSigning", (token) => { token.payload.sub = USERNAME; }); + server.service.on("beforeUserinfo", (userInfoResponse) => { + userInfoResponse.body.email = EMAIL; + }); + await server.start(port, "localhost"); console.log(`Started NeoPass development server at: ${server.issuer.url}`); } diff --git a/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb b/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb new file mode 100644 index 00000000..eb3c79ea --- /dev/null +++ b/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb @@ -0,0 +1,7 @@ +class AllowNullEmailAndPasswordForUsers < ActiveRecord::Migration[7.1] + def change + change_column_null :users, :email, true + change_column_null :users, :encrypted_password, true + change_column_null :users, :password_salt, true + end +end diff --git a/db/openneo_id_schema.rb b/db/openneo_id_schema.rb index 4a630c3b..8df274bc 100644 --- a/db/openneo_id_schema.rb +++ b/db/openneo_id_schema.rb @@ -10,12 +10,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_13_200849) do +ActiveRecord::Schema[7.1].define(version: 2024_03_15_020053) do create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t| t.string "name", limit: 20, null: false - t.string "encrypted_password", limit: 64, null: false - t.string "email", limit: 50, null: false - t.string "password_salt", limit: 32, null: false + t.string "encrypted_password", limit: 64 + t.string "email", limit: 50 + t.string "password_salt", limit: 32 t.string "reset_password_token" t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at", precision: nil