Compare commits
2 Commits
aab65e17b3
...
bf415d7a2e
Author | SHA1 | Date |
---|---|---|
Les De Ridder | bf415d7a2e | |
Les De Ridder | b2ede45d82 |
|
@ -7,9 +7,7 @@ AppDatabase.configure do |settings|
|
|||
settings.url = ENV["DATABASE_URL"]? || Avram::PostgresURL.build(
|
||||
database: database_name,
|
||||
hostname: ENV["DB_HOST"]? || "localhost",
|
||||
# Some common usernames are "postgres", "root", or your system username (run 'whoami')
|
||||
username: ENV["DB_USERNAME"]? || "postgres",
|
||||
# Some Postgres installations require no password. Use "" if that is the case.
|
||||
password: ENV["DB_PASSWORD"]? || "postgres"
|
||||
)
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class CreateUsers::V00000000000001 < Avram::Migrator::Migration::V1
|
|||
|
||||
add name : String, unique: true
|
||||
add encrypted_password : String # NOTE: Should really be called 'password_hash'
|
||||
add email : String?, unique: true
|
||||
add email : String, unique: true # NOTE: Should be nilable, but Carbon::Emailable doesn't really support it
|
||||
add created_at : Time
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,11 +1,11 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
describe Api::SignIns::Create do
|
||||
describe Api::SignIn::Create do
|
||||
it "returns a token" do
|
||||
UserToken.stub_token("fake-token") do
|
||||
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")
|
||||
end
|
||||
|
@ -15,7 +15,7 @@ describe Api::SignIns::Create do
|
|||
user = UserBox.create
|
||||
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(
|
||||
400,
|
||||
|
@ -27,7 +27,7 @@ end
|
|||
|
||||
private def valid_params(user : User)
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
password: "password",
|
||||
}
|
||||
end
|
|
@ -1,12 +1,13 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
describe Api::SignUps::Create do
|
||||
describe Api::SignUp::Create do
|
||||
it "creates user on sign up" 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")
|
||||
new_user = UserQuery.first
|
||||
new_user.name.should eq(valid_params[:name])
|
||||
new_user.email.should eq(valid_params[:email])
|
||||
end
|
||||
end
|
||||
|
@ -14,7 +15,7 @@ describe Api::SignUps::Create do
|
|||
it "returns error for invalid params" do
|
||||
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)
|
||||
response.should send_json(
|
||||
|
@ -27,6 +28,7 @@ end
|
|||
|
||||
private def valid_params
|
||||
{
|
||||
name: "test",
|
||||
email: "test@email.com",
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
|
@ -1,3 +1,7 @@
|
|||
require "../../tasks/create_required_seeds"
|
||||
|
||||
Spec.before_each do
|
||||
AppDatabase.truncate
|
||||
|
||||
Db::CreateRequiredSeeds.new(quiet: true).call
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class UserBox < Avram::Box
|
||||
def initialize
|
||||
name "#{sequence("test")}"
|
||||
email "#{sequence("test-email")}@example.com"
|
||||
encrypted_password Authentic.generate_encrypted_password("password")
|
||||
primary_role_id RoleQuery.new.name("user").first.id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
class Api::SignIns::Create < ApiAction
|
||||
class Api::SignIn::Create < ApiAction
|
||||
include Api::Auth::SkipRequireAuthToken
|
||||
|
||||
route do
|
|
@ -1,4 +1,4 @@
|
|||
class Api::SignUps::Create < ApiAction
|
||||
class Api::SignUp::Create < ApiAction
|
||||
include Api::Auth::SkipRequireAuthToken
|
||||
|
||||
route do
|
|
@ -7,12 +7,12 @@ class Home::Index < BrowserAction
|
|||
else
|
||||
# When you're ready change this line to:
|
||||
#
|
||||
# redirect SignIns::New
|
||||
# redirect SignIn::New
|
||||
#
|
||||
# Or maybe show signed out users a marketing page:
|
||||
#
|
||||
# html Marketing::IndexPage
|
||||
redirect SignIns::New
|
||||
redirect SignIn::New
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Auth::RequireSignIn
|
|||
else
|
||||
Authentic.remember_requested_path(self)
|
||||
flash.info = "Please sign in first"
|
||||
redirect to: SignIns::New
|
||||
redirect to: SignIn::New
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class PasswordResetRequests::Create < BrowserAction
|
|||
if user
|
||||
PasswordResetRequestEmail.new(user).deliver
|
||||
flash.success = "You should receive an email on how to reset your password shortly"
|
||||
redirect SignIns::New
|
||||
redirect SignIn::New
|
||||
else
|
||||
html NewPage, operation: operation
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class SignIns::Create < BrowserAction
|
||||
class SignIn::Create < BrowserAction
|
||||
include Auth::RedirectSignedInUsers
|
||||
|
||||
route do
|
|
@ -1,7 +1,7 @@
|
|||
class SignIns::Delete < BrowserAction
|
||||
class SignIn::Delete < BrowserAction
|
||||
delete "/sign_out" do
|
||||
sign_out
|
||||
flash.info = "You have been signed out"
|
||||
redirect to: SignIns::New
|
||||
redirect to: SignIn::New
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
class SignIns::New < BrowserAction
|
||||
class SignIn::New < BrowserAction
|
||||
include Auth::RedirectSignedInUsers
|
||||
|
||||
get "/sign_in" do
|
|
@ -1,4 +1,4 @@
|
|||
class SignUps::Create < BrowserAction
|
||||
class SignUp::Create < BrowserAction
|
||||
include Auth::RedirectSignedInUsers
|
||||
|
||||
route do
|
|
@ -1,4 +1,4 @@
|
|||
class SignUps::New < BrowserAction
|
||||
class SignUp::New < BrowserAction
|
||||
include Auth::RedirectSignedInUsers
|
||||
|
||||
get "/sign_up" do
|
|
@ -3,6 +3,6 @@ class Role < BaseModel
|
|||
column name : String
|
||||
column description : String?
|
||||
|
||||
has_many users : User, foreign_key: :primary_role_id #####
|
||||
has_many users : User, foreign_key: :primary_role_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,8 @@ class User < BaseModel
|
|||
table do
|
||||
column name : String
|
||||
column encrypted_password : String
|
||||
column email : String?
|
||||
column created_at : Time
|
||||
column email : String
|
||||
column created_at : Time, autogenerated: true
|
||||
|
||||
belongs_to primary_role : Role
|
||||
has_many posts : Post, foreign_key: :creator_id
|
||||
|
|
|
@ -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
|
|
@ -1,14 +1,14 @@
|
|||
class SignInUser < Avram::Operation
|
||||
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
|
||||
|
||||
# Run validations and yields the operation and the user if valid
|
||||
def submit
|
||||
user = user_from_email
|
||||
user = user_from_name
|
||||
validate_credentials(user)
|
||||
|
||||
if valid?
|
||||
|
@ -18,23 +18,15 @@ class SignInUser < Avram::Operation
|
|||
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)
|
||||
# TODO: If banned, disallow login
|
||||
|
||||
if user
|
||||
unless Authentic.correct_password?(user, password.value.to_s)
|
||||
password.add_error "is wrong"
|
||||
end
|
||||
else
|
||||
# Usually ok to say that an email is not in the 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"
|
||||
name.add_error "is not in our system"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
class SignUpUser < User::SaveOperation
|
||||
param_key :user
|
||||
# Change password validations in src/operations/mixins/password_validations.cr
|
||||
|
||||
include PasswordValidations
|
||||
|
||||
permit_columns email
|
||||
permit_columns name, email
|
||||
attribute password : String
|
||||
attribute password_confirmation : String
|
||||
|
||||
before_save do
|
||||
primary_role_id.value = RoleQuery.new.name("user").first.id
|
||||
|
||||
validate_uniqueness_of name
|
||||
validate_uniqueness_of email
|
||||
|
||||
Authentic.copy_and_encrypt password, to: encrypted_password
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,8 +31,8 @@ abstract class MainLayout
|
|||
end
|
||||
|
||||
private def render_signed_in_user
|
||||
text @current_user.email
|
||||
text @current_user.name
|
||||
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class SignIns::NewPage < AuthLayout
|
||||
class SignIn::NewPage < AuthLayout
|
||||
needs operation : SignInUser
|
||||
|
||||
def content
|
||||
|
@ -7,17 +7,17 @@ class SignIns::NewPage < AuthLayout
|
|||
end
|
||||
|
||||
private def render_sign_in_form(op)
|
||||
form_for SignIns::Create do
|
||||
form_for SignIn::Create do
|
||||
sign_in_fields(op)
|
||||
submit "Sign In", flow_id: "sign-in-button"
|
||||
end
|
||||
link "Reset password", to: PasswordResetRequests::New
|
||||
text " | "
|
||||
link "Sign up", to: SignUps::New
|
||||
link "Sign up", to: SignUp::New
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
class SignUps::NewPage < AuthLayout
|
||||
class SignUp::NewPage < AuthLayout
|
||||
needs operation : SignUpUser
|
||||
|
||||
def content
|
||||
|
@ -7,15 +7,16 @@ class SignUps::NewPage < AuthLayout
|
|||
end
|
||||
|
||||
private def render_sign_up_form(op)
|
||||
form_for SignUps::Create do
|
||||
form_for SignUp::Create do
|
||||
sign_up_fields(op)
|
||||
submit "Sign Up", flow_id: "sign-up-button"
|
||||
end
|
||||
link "Sign in instead", to: SignIns::New
|
||||
link "Sign in instead", to: SignIn::New
|
||||
end
|
||||
|
||||
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_confirmation), &.password_input
|
||||
end
|
|
@ -3,6 +3,6 @@ class UserSerializer < Lucky::Serializer
|
|||
end
|
||||
|
||||
def render
|
||||
{email: @user.email}
|
||||
{name: @user.name, email: @user.email}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,9 @@ require "../spec/support/boxes/**"
|
|||
# Use `Db::CreateSampleSeeds` if your only want to add sample data helpful for
|
||||
# development.
|
||||
class Db::CreateRequiredSeeds < LuckyCli::Task
|
||||
def initialize(@quiet : Bool = false)
|
||||
end
|
||||
|
||||
summary "Add database records required for the app to work"
|
||||
|
||||
def call
|
||||
|
@ -20,6 +23,8 @@ class Db::CreateRequiredSeeds < LuckyCli::Task
|
|||
Role::SaveOperation.create!(id: 1000, name: "user", description: "Regular user")
|
||||
end
|
||||
|
||||
unless @quiet
|
||||
puts "Done adding required data"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue