Merge pull request #331 from AyuntamientoMadrid/letter_verification
Letter verification
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
class Verification::LetterController < ApplicationController
|
class Verification::LetterController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :verify_resident!
|
before_action :verify_resident!
|
||||||
before_action :verify_phone_or_email!
|
before_action :verify_phone!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@@ -11,20 +11,35 @@ class Verification::LetterController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@letter = Verification::Letter.new(user: current_user)
|
@letter = Verification::Letter.new(user: current_user)
|
||||||
if @letter.save
|
if @letter.save
|
||||||
redirect_to account_path, notice: t('verification.letter.create.flash.success')
|
redirect_to edit_letter_path, notice: t('verification.letter.create.flash.success')
|
||||||
else
|
else
|
||||||
flash.now.alert = t('verification.letter.create.alert.failure')
|
flash.now.alert = t('verification.letter.create.alert.failure')
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@letter = Verification::Letter.new(user: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@letter = Verification::Letter.new(letter_params.merge(user: current_user))
|
||||||
|
if @letter.verify?
|
||||||
|
current_user.update(verified_at: Time.now)
|
||||||
|
redirect_to account_path, notice: t('verification.letter.update.flash.success')
|
||||||
|
else
|
||||||
|
@error = t('verification.letter.update.error')
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def letter_params
|
def letter_params
|
||||||
params.require(:letter).permit()
|
params.require(:letter).permit(:verification_code)
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_phone_or_email!
|
def verify_phone!
|
||||||
unless current_user.confirmed_phone?
|
unless current_user.confirmed_phone?
|
||||||
redirect_to verified_user_path, alert: t('verification.letter.alert.unconfirmed_code')
|
redirect_to verified_user_path, alert: t('verification.letter.alert.unconfirmed_code')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
class Verification::Letter
|
class Verification::Letter
|
||||||
include ActiveModel::Model
|
include ActiveModel::Model
|
||||||
|
|
||||||
attr_accessor :user, :address
|
attr_accessor :user, :address, :verification_code
|
||||||
|
|
||||||
validates :user, presence: true
|
validates :user, presence: true
|
||||||
validates :address, presence: true
|
validates :address, presence: true
|
||||||
validate :correct_address
|
validate :correct_address
|
||||||
|
|
||||||
def initialize(attrs={})
|
|
||||||
@user = attrs[:user]
|
|
||||||
end
|
|
||||||
|
|
||||||
def save
|
def save
|
||||||
valid? &&
|
valid? &&
|
||||||
letter_requested! &&
|
letter_requested! &&
|
||||||
@@ -22,7 +18,11 @@ class Verification::Letter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def letter_requested!
|
def letter_requested!
|
||||||
user.update(letter_requested_at: Time.now)
|
user.update(letter_requested_at: Time.now, letter_verification_code: four_digit_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify?
|
||||||
|
user.letter_verification_code == verification_code
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_user_address
|
def update_user_address
|
||||||
@@ -50,4 +50,8 @@ class Verification::Letter
|
|||||||
district: address[:nombre_distrito] }
|
district: address[:nombre_distrito] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def four_digit_code
|
||||||
|
rand.to_s[2..5]
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
37
app/views/verification/letter/edit.html.erb
Normal file
37
app/views/verification/letter/edit.html.erb
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="verification account row">
|
||||||
|
<div class="small-12 column">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="small-4 column verification-step completed">
|
||||||
|
<%= t("verification.step_1") %>
|
||||||
|
</div>
|
||||||
|
<div class="small-4 column verification-step completed">
|
||||||
|
<%= t("verification.step_2") %>
|
||||||
|
</div>
|
||||||
|
<div class="small-4 column verification-step active">
|
||||||
|
<%= t("verification.step_3") %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress small-12 success round">
|
||||||
|
<span class="meter" style="width: 100%"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small-12 medium-12 column">
|
||||||
|
|
||||||
|
<h1 class="inline-block"><%= t("verification.letter.edit.title") %></h1>
|
||||||
|
|
||||||
|
<div class="small-12 medium-6">
|
||||||
|
<%= form_for @letter, as: "letter", url: letter_path, method: :put do |f| %>
|
||||||
|
<% if @error %>
|
||||||
|
<div class="alert-box alert radius"><%= @error %></div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= f.text_field :verification_code, label: t("verification.letter.edit.confirmation_code") %>
|
||||||
|
<%= f.submit t("verification.letter.new.send_code"), class: "button radius success" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -28,22 +28,9 @@
|
|||||||
%>
|
%>
|
||||||
|
|
||||||
<%= form_for @letter, as: "letter", url: letter_path do |f| %>
|
<%= form_for @letter, as: "letter", url: letter_path do |f| %>
|
||||||
<%= render "shared/errors", resource: @letter %>
|
|
||||||
<%= f.submit t("verification.letter.new.send_letter"), class: "button radius secondary inline-block" %>
|
<%= f.submit t("verification.letter.new.send_letter"), class: "button radius secondary inline-block" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Show this if user clics on 'Send me a letter' -->
|
|
||||||
<div class="alert-box success radius">
|
|
||||||
Gracias por solicitar tu código de máxima seguridad, en unos días te lo enviaremos a la dirección que figura en tus datos del padrón. Recuerda que puedes ahorrar el envío recogiendo tu código en cualquiera de las Oficinas de Atención al Ciudadano.
|
|
||||||
</div>
|
|
||||||
<div class="small-12 medium-6">
|
|
||||||
<%= form_tag do %>
|
|
||||||
<%= label_tag t("verification.letter.new.introduce_code") %>
|
|
||||||
<%= text_field_tag(:q) %>
|
|
||||||
<%= submit_tag t("verification.letter.new.send_code"), class: "button radius success" %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<!-- /. Show this if user clics on 'Send me a letter' -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ en:
|
|||||||
accept_terms: I accept the privacy policy and the legal terms
|
accept_terms: I accept the privacy policy and the legal terms
|
||||||
user: account
|
user: account
|
||||||
debate: debate
|
debate: debate
|
||||||
sms: phone
|
verification::sms: phone
|
||||||
application:
|
application:
|
||||||
alert:
|
alert:
|
||||||
only_beta_testers: "Sorry only Beta Testers are allowed access at the moment"
|
only_beta_testers: "Sorry only Beta Testers are allowed access at the moment"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ es:
|
|||||||
accept_terms: Acepto la política de privacidad y el aviso legal
|
accept_terms: Acepto la política de privacidad y el aviso legal
|
||||||
user: la cuenta
|
user: la cuenta
|
||||||
debate: el debate
|
debate: el debate
|
||||||
sms: el teléfono
|
verification::sms: el teléfono
|
||||||
application:
|
application:
|
||||||
alert:
|
alert:
|
||||||
only_beta_testers: "Lo sentimos sólo los usuarios de pruebas tienen acceso de momento"
|
only_beta_testers: "Lo sentimos sólo los usuarios de pruebas tienen acceso de momento"
|
||||||
|
|||||||
@@ -67,13 +67,19 @@ en:
|
|||||||
offices: "See Office of Citizen"
|
offices: "See Office of Citizen"
|
||||||
offices_url: "http://www.madrid.es/portales/munimadrid/es/Inicio/El-Ayuntamiento/Atencion-al-ciudadano/Oficinas-de-Atencion-al-Ciudadano?vgnextfmt=default&vgnextchannel=5b99cde2e09a4310VgnVCM1000000b205a0aRCRD"
|
offices_url: "http://www.madrid.es/portales/munimadrid/es/Inicio/El-Ayuntamiento/Atencion-al-ciudadano/Oficinas-de-Atencion-al-Ciudadano?vgnextfmt=default&vgnextchannel=5b99cde2e09a4310VgnVCM1000000b205a0aRCRD"
|
||||||
send_letter: "Send me a letter with the code"
|
send_letter: "Send me a letter with the code"
|
||||||
introduce_code: "Enter the security code"
|
|
||||||
send_code: "Send"
|
send_code: "Send"
|
||||||
create:
|
create:
|
||||||
flash:
|
flash:
|
||||||
success: "Thank you for requesting a code maximum security in a few days we will send it to the address on your census data. Remember that you can save shipping collecting your code in any of the Office of Citizen Services."
|
success: "Thank you for requesting a maximum security code in a few days we will send it to the address on your census data. Remember that you can save shipping collecting your code in any of the Office of Citizen Services."
|
||||||
alert:
|
alert:
|
||||||
failure: "We could not verify your address with the Census please try again later"
|
failure: "We could not verify your address with the Census please try again later"
|
||||||
|
edit:
|
||||||
|
title: "Security code confirmation"
|
||||||
|
confirmation_code: "Enter the security code in your letter"
|
||||||
|
update:
|
||||||
|
error: "Incorrect confirmation code"
|
||||||
|
flash:
|
||||||
|
success: "Correct code. Your account is verified"
|
||||||
alert:
|
alert:
|
||||||
unconfirmed_code: "You have not yet enter the confirmation code"
|
unconfirmed_code: "You have not yet enter the confirmation code"
|
||||||
verified_user:
|
verified_user:
|
||||||
|
|||||||
@@ -67,13 +67,19 @@ es:
|
|||||||
offices: "Ver Oficinas de Atención al Ciudadano"
|
offices: "Ver Oficinas de Atención al Ciudadano"
|
||||||
offices_url: "http://www.madrid.es/portales/munimadrid/es/Inicio/El-Ayuntamiento/Atencion-al-ciudadano/Oficinas-de-Atencion-al-Ciudadano?vgnextfmt=default&vgnextchannel=5b99cde2e09a4310VgnVCM1000000b205a0aRCRD"
|
offices_url: "http://www.madrid.es/portales/munimadrid/es/Inicio/El-Ayuntamiento/Atencion-al-ciudadano/Oficinas-de-Atencion-al-Ciudadano?vgnextfmt=default&vgnextchannel=5b99cde2e09a4310VgnVCM1000000b205a0aRCRD"
|
||||||
send_letter: "Enviarme una carta con el código"
|
send_letter: "Enviarme una carta con el código"
|
||||||
introduce_code: "Introduce el código de seguridad"
|
|
||||||
send_code: "Enviar"
|
send_code: "Enviar"
|
||||||
create:
|
create:
|
||||||
flash:
|
flash:
|
||||||
success: "Gracias por solicitar tu código de máxima seguridad, en unos días te lo enviaremos a la dirección que figura en tus datos del padrón. Recuerda que puedes ahorrar el envío recogiendo tu código en cualquiera de las Oficinas de Atención al Ciudadano."
|
success: "Gracias por solicitar tu código de máxima seguridad, en unos días te lo enviaremos a la dirección que figura en tus datos del padrón. Recuerda que puedes ahorrar el envío recogiendo tu código en cualquiera de las Oficinas de Atención al Ciudadano."
|
||||||
alert:
|
alert:
|
||||||
failure: "No podemos verificar tu dirección con el Padrón, por favor inténtalo otra vez más tarde"
|
failure: "No podemos verificar tu dirección con el Padrón, por favor inténtalo otra vez más tarde"
|
||||||
|
edit:
|
||||||
|
title: "Confirmación de código de seguridad"
|
||||||
|
confirmation_code: "Introduce el código que has recibido en tu carta"
|
||||||
|
update:
|
||||||
|
error: "Código de verificación incorrecto"
|
||||||
|
flash:
|
||||||
|
success: "Código correcto. Tu cuenta ya está verificada"
|
||||||
alert:
|
alert:
|
||||||
unconfirmed_code: "Todavía no has introducido el código de confirmación"
|
unconfirmed_code: "Todavía no has introducido el código de confirmación"
|
||||||
verified_user:
|
verified_user:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Rails.application.routes.draw do
|
|||||||
resource :sms, controller: "sms", only: [:new, :create, :edit, :update]
|
resource :sms, controller: "sms", only: [:new, :create, :edit, :update]
|
||||||
resource :verified_user, controller: "verified_user", only: [:show]
|
resource :verified_user, controller: "verified_user", only: [:show]
|
||||||
resource :email, controller: "email", only: [:new, :show, :create]
|
resource :email, controller: "email", only: [:new, :show, :create]
|
||||||
resource :letter, controller: "letter", only: [:new, :create]
|
resource :letter, controller: "letter", only: [:new, :create, :edit, :update]
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddLetterVerificationCodeToUsers < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :users, :letter_verification_code, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
13
db/schema.rb
13
db/schema.rb
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20150830212600) do
|
ActiveRecord::Schema.define(version: 20150902191315) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -67,8 +67,8 @@ ActiveRecord::Schema.define(version: 20150830212600) do
|
|||||||
t.integer "rgt"
|
t.integer "rgt"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.integer "children_count", default: 0
|
|
||||||
t.datetime "hidden_at"
|
t.datetime "hidden_at"
|
||||||
|
t.integer "children_count", default: 0
|
||||||
t.integer "flags_count", default: 0
|
t.integer "flags_count", default: 0
|
||||||
t.datetime "ignored_flag_at"
|
t.datetime "ignored_flag_at"
|
||||||
t.integer "moderator_id"
|
t.integer "moderator_id"
|
||||||
@@ -92,8 +92,8 @@ ActiveRecord::Schema.define(version: 20150830212600) do
|
|||||||
t.integer "author_id"
|
t.integer "author_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "hidden_at"
|
|
||||||
t.string "visit_id"
|
t.string "visit_id"
|
||||||
|
t.datetime "hidden_at"
|
||||||
t.integer "flags_count", default: 0
|
t.integer "flags_count", default: 0
|
||||||
t.datetime "ignored_flag_at"
|
t.datetime "ignored_flag_at"
|
||||||
t.integer "cached_votes_total", default: 0
|
t.integer "cached_votes_total", default: 0
|
||||||
@@ -200,13 +200,12 @@ ActiveRecord::Schema.define(version: 20150830212600) do
|
|||||||
t.string "unconfirmed_email"
|
t.string "unconfirmed_email"
|
||||||
t.boolean "email_on_debate_comment", default: false
|
t.boolean "email_on_debate_comment", default: false
|
||||||
t.boolean "email_on_comment_reply", default: false
|
t.boolean "email_on_comment_reply", default: false
|
||||||
|
t.string "phone_number", limit: 30
|
||||||
t.string "official_position"
|
t.string "official_position"
|
||||||
t.integer "official_level", default: 0
|
t.integer "official_level", default: 0
|
||||||
t.datetime "hidden_at"
|
t.datetime "hidden_at"
|
||||||
t.string "phone_number", limit: 30
|
|
||||||
t.string "username"
|
|
||||||
t.datetime "confirmed_hide_at"
|
|
||||||
t.string "sms_confirmation_code"
|
t.string "sms_confirmation_code"
|
||||||
|
t.string "username"
|
||||||
t.string "document_number"
|
t.string "document_number"
|
||||||
t.string "document_type"
|
t.string "document_type"
|
||||||
t.datetime "residence_verified_at"
|
t.datetime "residence_verified_at"
|
||||||
@@ -218,6 +217,8 @@ ActiveRecord::Schema.define(version: 20150830212600) do
|
|||||||
t.string "unconfirmed_phone"
|
t.string "unconfirmed_phone"
|
||||||
t.string "confirmed_phone"
|
t.string "confirmed_phone"
|
||||||
t.datetime "letter_requested_at"
|
t.datetime "letter_requested_at"
|
||||||
|
t.datetime "confirmed_hide_at"
|
||||||
|
t.string "letter_verification_code"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||||
|
|||||||
@@ -2,15 +2,45 @@ require 'rails_helper'
|
|||||||
|
|
||||||
feature 'Verify Letter' do
|
feature 'Verify Letter' do
|
||||||
|
|
||||||
scenario 'Send letter level 2 verified with phone' do
|
scenario 'Verify' do
|
||||||
user = create(:user, residence_verified_at: Time.now, confirmed_phone: "611111111")
|
user = create(:user, residence_verified_at: Time.now, confirmed_phone: "611111111")
|
||||||
|
|
||||||
login_as(user)
|
login_as(user)
|
||||||
visit new_letter_path
|
visit new_letter_path
|
||||||
|
|
||||||
click_button "Send me a letter"
|
click_button "Send me a letter with the code"
|
||||||
|
|
||||||
expect(page).to have_content "Thank you for requesting a code maximum security in a few days we will send it to the address on your census data. Remember that you can save shipping collecting your code in any of the Office of Citizen Services."
|
expect(page).to have_content "Thank you for requesting a maximum security code in a few days we will send it to the address on your census data."
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
fill_in "letter_verification_code", with: user.letter_verification_code
|
||||||
|
click_button "Send"
|
||||||
|
|
||||||
|
expect(page).to have_content "Correct code. Your account is verified"
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Go to office instead of send letter' do
|
||||||
|
user = create(:user, residence_verified_at: Time.now, confirmed_phone: "611111111")
|
||||||
|
|
||||||
|
login_as(user)
|
||||||
|
visit new_letter_path
|
||||||
|
|
||||||
|
expect(page).to have_link "Office of Citizen", href: "http://www.madrid.es/portales/munimadrid/es/Inicio/El-Ayuntamiento/Atencion-al-ciudadano/Oficinas-de-Atencion-al-Ciudadano?vgnextfmt=default&vgnextchannel=5b99cde2e09a4310VgnVCM1000000b205a0aRCRD"
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Errors on verification code' do
|
||||||
|
user = create(:user, residence_verified_at: Time.now, confirmed_phone: "611111111")
|
||||||
|
|
||||||
|
login_as(user)
|
||||||
|
visit new_letter_path
|
||||||
|
|
||||||
|
click_button "Send me a letter with the code"
|
||||||
|
expect(page).to have_content "Thank you for requesting a maximum security code in a few days we will send it to the address on your census data."
|
||||||
|
|
||||||
|
fill_in "letter_verification_code", with: "1"
|
||||||
|
click_button "Send"
|
||||||
|
|
||||||
|
expect(page).to have_content "Incorrect confirmation code"
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Error accessing address from CensusApi" do
|
scenario "Error accessing address from CensusApi" do
|
||||||
@@ -26,17 +56,6 @@ feature 'Verify Letter' do
|
|||||||
expect(page).to have_content "We could not verify your address with the Census please try again later"
|
expect(page).to have_content "We could not verify your address with the Census please try again later"
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'Send letter level 2 user verified with email' do
|
|
||||||
user = create(:user, residence_verified_at: Time.now, confirmed_phone: "611111111")
|
|
||||||
|
|
||||||
login_as(user)
|
|
||||||
visit new_letter_path
|
|
||||||
|
|
||||||
click_button "Send me a letter"
|
|
||||||
|
|
||||||
expect(page).to have_content "Thank you for requesting a code maximum security in a few days we will send it to the address on your census data. Remember that you can save shipping collecting your code in any of the Office of Citizen Services."
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario "Deny access unless verified residence" do
|
scenario "Deny access unless verified residence" do
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ feature 'Level three verification' do
|
|||||||
fill_in 'sms_confirmation_code', with: user.sms_confirmation_code
|
fill_in 'sms_confirmation_code', with: user.sms_confirmation_code
|
||||||
click_button 'Send'
|
click_button 'Send'
|
||||||
|
|
||||||
expect(page).to have_content 'Correct code'
|
|
||||||
|
|
||||||
expect(page).to have_content "Correct code. Your account is verified"
|
expect(page).to have_content "Correct code. Your account is verified"
|
||||||
|
|
||||||
expect(page).to_not have_link "Verify my account"
|
expect(page).to_not have_link "Verify my account"
|
||||||
@@ -85,8 +83,14 @@ feature 'Level three verification' do
|
|||||||
|
|
||||||
expect(page).to have_content 'Correct code'
|
expect(page).to have_content 'Correct code'
|
||||||
|
|
||||||
click_button "Send me a letter"
|
click_button "Send me a letter with the code"
|
||||||
|
|
||||||
expect(page).to have_content "Thank you for requesting a code maximum security in a few days we will send it to the address on your census data. Remember that you can save shipping collecting your code in any of the Office of Citizen Services."
|
expect(page).to have_content "Thank you for requesting a maximum security code in a few days we will send it to the address on your census data."
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
fill_in "letter_verification_code", with: user.letter_verification_code
|
||||||
|
click_button "Send"
|
||||||
|
|
||||||
|
expect(page).to have_content "Correct code. Your account is verified"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user