Fix sign up and log in

This commit is contained in:
Les De Ridder 2020-03-07 23:32:07 +01:00
parent b2ede45d82
commit bf415d7a2e
28 changed files with 66 additions and 182 deletions

View File

@ -7,9 +7,7 @@ AppDatabase.configure do |settings|
settings.url = ENV["DATABASE_URL"]? || Avram::PostgresURL.build( settings.url = ENV["DATABASE_URL"]? || Avram::PostgresURL.build(
database: database_name, database: database_name,
hostname: ENV["DB_HOST"]? || "localhost", hostname: ENV["DB_HOST"]? || "localhost",
# Some common usernames are "postgres", "root", or your system username (run 'whoami')
username: ENV["DB_USERNAME"]? || "postgres", username: ENV["DB_USERNAME"]? || "postgres",
# Some Postgres installations require no password. Use "" if that is the case.
password: ENV["DB_PASSWORD"]? || "postgres" password: ENV["DB_PASSWORD"]? || "postgres"
) )
end end

View File

@ -1,31 +0,0 @@
require "../spec_helper"
describe "Authentication flow" do
it "works" do
flow = AuthenticationFlow.new("test@example.com")
flow.sign_up "password"
flow.should_be_signed_in
flow.sign_out
flow.sign_in "wrong-password"
flow.should_have_password_error
flow.sign_in "password"
flow.should_be_signed_in
end
# This is to show you how to sign in as a user during tests.
# Use the `visit` method's `as` option in your tests to sign in as that user.
#
# Feel free to delete this once you have other tests using the 'as' option.
it "allows sign in through backdoor when testing" do
user = UserBox.create
flow = BaseFlow.new
flow.visit Me::Show, as: user
should_be_signed_in(flow)
end
end
private def should_be_signed_in(flow)
flow.el("@sign-out-button").should be_on_page
end

View File

@ -1,18 +0,0 @@
require "../spec_helper"
describe "Reset password flow" do
it "works" do
user = UserBox.create
flow = ResetPasswordFlow.new(user)
flow.request_password_reset
flow.should_have_sent_reset_email
flow.reset_password "new-password"
flow.should_be_signed_in
flow.sign_out
flow.sign_in "wrong-password"
flow.should_have_password_error
flow.sign_in "new-password"
flow.should_be_signed_in
end
end

View File

@ -1,11 +1,11 @@
require "../../../spec_helper" require "../../../spec_helper"
describe Api::SignIns::Create do describe Api::SignIn::Create do
it "returns a token" do it "returns a token" do
UserToken.stub_token("fake-token") do UserToken.stub_token("fake-token") do
user = UserBox.create user = UserBox.create
response = AppClient.exec(Api::SignIns::Create, user: valid_params(user)) response = AppClient.exec(Api::SignIn::Create, user: valid_params(user))
response.should send_json(200, token: "fake-token") response.should send_json(200, token: "fake-token")
end end
@ -15,7 +15,7 @@ describe Api::SignIns::Create do
user = UserBox.create user = UserBox.create
invalid_params = valid_params(user).merge(password: "incorrect") invalid_params = valid_params(user).merge(password: "incorrect")
response = AppClient.exec(Api::SignIns::Create, user: invalid_params) response = AppClient.exec(Api::SignIn::Create, user: invalid_params)
response.should send_json( response.should send_json(
400, 400,
@ -27,7 +27,7 @@ end
private def valid_params(user : User) private def valid_params(user : User)
{ {
email: user.email, name: user.name,
password: "password", password: "password",
} }
end end

View File

@ -1,12 +1,13 @@
require "../../../spec_helper" require "../../../spec_helper"
describe Api::SignUps::Create do describe Api::SignUp::Create do
it "creates user on sign up" do it "creates user on sign up" do
UserToken.stub_token("fake-token") do UserToken.stub_token("fake-token") do
response = AppClient.exec(Api::SignUps::Create, user: valid_params) response = AppClient.exec(Api::SignUp::Create, user: valid_params)
response.should send_json(200, token: "fake-token") response.should send_json(200, token: "fake-token")
new_user = UserQuery.first new_user = UserQuery.first
new_user.name.should eq(valid_params[:name])
new_user.email.should eq(valid_params[:email]) new_user.email.should eq(valid_params[:email])
end end
end end
@ -14,7 +15,7 @@ describe Api::SignUps::Create do
it "returns error for invalid params" do it "returns error for invalid params" do
invalid_params = valid_params.merge(password_confirmation: "wrong") invalid_params = valid_params.merge(password_confirmation: "wrong")
response = AppClient.exec(Api::SignUps::Create, user: invalid_params) response = AppClient.exec(Api::SignUp::Create, user: invalid_params)
UserQuery.new.select_count.should eq(0) UserQuery.new.select_count.should eq(0)
response.should send_json( response.should send_json(
@ -27,6 +28,7 @@ end
private def valid_params private def valid_params
{ {
name: "test",
email: "test@email.com", email: "test@email.com",
password: "password", password: "password",
password_confirmation: "password", password_confirmation: "password",

View File

@ -1,3 +1,7 @@
require "../../tasks/create_required_seeds"
Spec.before_each do Spec.before_each do
AppDatabase.truncate AppDatabase.truncate
Db::CreateRequiredSeeds.new(quiet: true).call
end end

View File

@ -1,6 +1,8 @@
class UserBox < Avram::Box class UserBox < Avram::Box
def initialize def initialize
name "#{sequence("test")}"
email "#{sequence("test-email")}@example.com" email "#{sequence("test-email")}@example.com"
encrypted_password Authentic.generate_encrypted_password("password") encrypted_password Authentic.generate_encrypted_password("password")
primary_role_id RoleQuery.new.name("user").first.id
end end
end end

View File

@ -1,40 +0,0 @@
class AuthenticationFlow < BaseFlow
private getter email
def initialize(@email : String)
end
def sign_up(password)
visit SignUps::New
fill_form SignUpUser,
email: email,
password: password,
password_confirmation: password
click "@sign-up-button"
end
def sign_out
visit Me::Show
sign_out_button.click
end
def sign_in(password)
visit SignIns::New
fill_form SignInUser,
email: email,
password: password
click "@sign-in-button"
end
def should_be_signed_in
sign_out_button.should be_on_page
end
def should_have_password_error
el("body", text: "Password is wrong").should be_on_page
end
private def sign_out_button
el("@sign-out-button")
end
end

View File

@ -1,42 +0,0 @@
class ResetPasswordFlow < BaseFlow
private getter user, authentication_flow
delegate sign_in, sign_out, should_have_password_error, should_be_signed_in,
to: authentication_flow
delegate email, to: user
def initialize(@user : User)
@authentication_flow = AuthenticationFlow.new(user.email)
end
def request_password_reset
with_fake_token do
visit PasswordResetRequests::New
fill_form RequestPasswordReset,
email: email
click "@request-password-reset-button"
end
end
def should_have_sent_reset_email
with_fake_token do
user = UserQuery.new.email(email).first
PasswordResetRequestEmail.new(user).should be_delivered
end
end
def reset_password(password)
user = UserQuery.new.email(email).first
token = Authentic.generate_password_reset_token(user)
visit PasswordResets::New.with(user.id, token)
fill_form ResetPassword,
password: password,
password_confirmation: password
click "@update-password-button"
end
private def with_fake_token
PasswordResetRequestEmail.temp_config(stubbed_token: "fake") do
yield
end
end
end

View File

@ -1,4 +1,4 @@
class Api::SignIns::Create < ApiAction class Api::SignIn::Create < ApiAction
include Api::Auth::SkipRequireAuthToken include Api::Auth::SkipRequireAuthToken
route do route do

View File

@ -1,4 +1,4 @@
class Api::SignUps::Create < ApiAction class Api::SignUp::Create < ApiAction
include Api::Auth::SkipRequireAuthToken include Api::Auth::SkipRequireAuthToken
route do route do

View File

@ -7,12 +7,12 @@ class Home::Index < BrowserAction
else else
# When you're ready change this line to: # When you're ready change this line to:
# #
# redirect SignIns::New # redirect SignIn::New
# #
# Or maybe show signed out users a marketing page: # Or maybe show signed out users a marketing page:
# #
# html Marketing::IndexPage # html Marketing::IndexPage
redirect SignIns::New redirect SignIn::New
end end
end end
end end

View File

@ -9,7 +9,7 @@ module Auth::RequireSignIn
else else
Authentic.remember_requested_path(self) Authentic.remember_requested_path(self)
flash.info = "Please sign in first" flash.info = "Please sign in first"
redirect to: SignIns::New redirect to: SignIn::New
end end
end end

View File

@ -6,7 +6,7 @@ class PasswordResetRequests::Create < BrowserAction
if user if user
PasswordResetRequestEmail.new(user).deliver PasswordResetRequestEmail.new(user).deliver
flash.success = "You should receive an email on how to reset your password shortly" flash.success = "You should receive an email on how to reset your password shortly"
redirect SignIns::New redirect SignIn::New
else else
html NewPage, operation: operation html NewPage, operation: operation
end end

View File

@ -1,4 +1,4 @@
class SignIns::Create < BrowserAction class SignIn::Create < BrowserAction
include Auth::RedirectSignedInUsers include Auth::RedirectSignedInUsers
route do route do

View File

@ -1,7 +1,7 @@
class SignIns::Delete < BrowserAction class SignIn::Delete < BrowserAction
delete "/sign_out" do delete "/sign_out" do
sign_out sign_out
flash.info = "You have been signed out" flash.info = "You have been signed out"
redirect to: SignIns::New redirect to: SignIn::New
end end
end end

View File

@ -1,4 +1,4 @@
class SignIns::New < BrowserAction class SignIn::New < BrowserAction
include Auth::RedirectSignedInUsers include Auth::RedirectSignedInUsers
get "/sign_in" do get "/sign_in" do

View File

@ -1,4 +1,4 @@
class SignUps::Create < BrowserAction class SignUp::Create < BrowserAction
include Auth::RedirectSignedInUsers include Auth::RedirectSignedInUsers
route do route do

View File

@ -1,4 +1,4 @@
class SignUps::New < BrowserAction class SignUp::New < BrowserAction
include Auth::RedirectSignedInUsers include Auth::RedirectSignedInUsers
get "/sign_up" do get "/sign_up" do

View File

@ -6,7 +6,7 @@ class User < BaseModel
column name : String column name : String
column encrypted_password : String column encrypted_password : String
column email : String column email : String
column created_at : Time column created_at : Time, autogenerated: true
belongs_to primary_role : Role belongs_to primary_role : Role
has_many posts : Post, foreign_key: :creator_id has_many posts : Post, foreign_key: :creator_id

View File

@ -0,0 +1,7 @@
module UserFromName
private def user_from_name : User?
name.value.try do |value|
UserQuery.new.name(value).first?
end
end
end

View File

@ -1,14 +1,14 @@
class SignInUser < Avram::Operation class SignInUser < Avram::Operation
param_key :user param_key :user
# You can modify this in src/operations/mixins/user_from_email.cr
include UserFromEmail
attribute email : String include UserFromName
attribute name : String
attribute password : String attribute password : String
# Run validations and yields the operation and the user if valid # Run validations and yields the operation and the user if valid
def submit def submit
user = user_from_email user = user_from_name
validate_credentials(user) validate_credentials(user)
if valid? if valid?
@ -18,23 +18,15 @@ class SignInUser < Avram::Operation
end end
end end
# `validate_credentials` determines if a user can sign in.
#
# If desired, you can add additional checks in this method, e.g.
#
# if user.locked?
# email.add_error "is locked out"
# end
private def validate_credentials(user) private def validate_credentials(user)
# TODO: If banned, disallow login
if user if user
unless Authentic.correct_password?(user, password.value.to_s) unless Authentic.correct_password?(user, password.value.to_s)
password.add_error "is wrong" password.add_error "is wrong"
end end
else else
# Usually ok to say that an email is not in the system: name.add_error "is not in our system"
# https://kev.inburke.com/kevin/invalid-username-or-password-useless/
# https://github.com/luckyframework/lucky_cli/issues/192
email.add_error "is not in our system"
end end
end end
end end

View File

@ -1,14 +1,18 @@
class SignUpUser < User::SaveOperation class SignUpUser < User::SaveOperation
param_key :user param_key :user
# Change password validations in src/operations/mixins/password_validations.cr
include PasswordValidations include PasswordValidations
permit_columns email permit_columns name, email
attribute password : String attribute password : String
attribute password_confirmation : String attribute password_confirmation : String
before_save do before_save do
primary_role_id.value = RoleQuery.new.name("user").first.id
validate_uniqueness_of name
validate_uniqueness_of email validate_uniqueness_of email
Authentic.copy_and_encrypt password, to: encrypted_password Authentic.copy_and_encrypt password, to: encrypted_password
end end
end end

View File

@ -31,8 +31,8 @@ abstract class MainLayout
end end
private def render_signed_in_user private def render_signed_in_user
text @current_user.email text @current_user.name
text " - " text " - "
link "Sign out", to: SignIns::Delete, flow_id: "sign-out-button" link "Sign out", to: SignIn::Delete, flow_id: "sign-out-button"
end end
end end

View File

@ -1,4 +1,4 @@
class SignIns::NewPage < AuthLayout class SignIn::NewPage < AuthLayout
needs operation : SignInUser needs operation : SignInUser
def content def content
@ -7,17 +7,17 @@ class SignIns::NewPage < AuthLayout
end end
private def render_sign_in_form(op) private def render_sign_in_form(op)
form_for SignIns::Create do form_for SignIn::Create do
sign_in_fields(op) sign_in_fields(op)
submit "Sign In", flow_id: "sign-in-button" submit "Sign In", flow_id: "sign-in-button"
end end
link "Reset password", to: PasswordResetRequests::New link "Reset password", to: PasswordResetRequests::New
text " | " text " | "
link "Sign up", to: SignUps::New link "Sign up", to: SignUp::New
end end
private def sign_in_fields(op) private def sign_in_fields(op)
mount Shared::Field.new(op.email), &.email_input(autofocus: "true") mount Shared::Field.new(op.name), &.text_input(autofocus: "true")
mount Shared::Field.new(op.password), &.password_input mount Shared::Field.new(op.password), &.password_input
end end
end end

View File

@ -1,4 +1,4 @@
class SignUps::NewPage < AuthLayout class SignUp::NewPage < AuthLayout
needs operation : SignUpUser needs operation : SignUpUser
def content def content
@ -7,15 +7,16 @@ class SignUps::NewPage < AuthLayout
end end
private def render_sign_up_form(op) private def render_sign_up_form(op)
form_for SignUps::Create do form_for SignUp::Create do
sign_up_fields(op) sign_up_fields(op)
submit "Sign Up", flow_id: "sign-up-button" submit "Sign Up", flow_id: "sign-up-button"
end end
link "Sign in instead", to: SignIns::New link "Sign in instead", to: SignIn::New
end end
private def sign_up_fields(op) private def sign_up_fields(op)
mount Shared::Field.new(op.email), &.email_input(autofocus: "true") mount Shared::Field.new(op.name), &.text_input(autofocus: "true")
mount Shared::Field.new(op.email), &.email_input
mount Shared::Field.new(op.password), &.password_input mount Shared::Field.new(op.password), &.password_input
mount Shared::Field.new(op.password_confirmation), &.password_input mount Shared::Field.new(op.password_confirmation), &.password_input
end end

View File

@ -3,6 +3,6 @@ class UserSerializer < Lucky::Serializer
end end
def render def render
{email: @user.email} {name: @user.name, email: @user.email}
end end
end end

View File

@ -7,6 +7,9 @@ require "../spec/support/boxes/**"
# Use `Db::CreateSampleSeeds` if your only want to add sample data helpful for # Use `Db::CreateSampleSeeds` if your only want to add sample data helpful for
# development. # development.
class Db::CreateRequiredSeeds < LuckyCli::Task class Db::CreateRequiredSeeds < LuckyCli::Task
def initialize(@quiet : Bool = false)
end
summary "Add database records required for the app to work" summary "Add database records required for the app to work"
def call def call
@ -20,6 +23,8 @@ class Db::CreateRequiredSeeds < LuckyCli::Task
Role::SaveOperation.create!(id: 1000, name: "user", description: "Regular user") Role::SaveOperation.create!(id: 1000, name: "user", description: "Regular user")
end end
puts "Done adding required data" unless @quiet
puts "Done adding required data"
end
end end
end end