aboutsummaryrefslogtreecommitdiff
path: root/src/actions
diff options
context:
space:
mode:
Diffstat (limited to 'src/actions')
-rw-r--r--src/actions/api/me/show.cr5
-rw-r--r--src/actions/api/sign_ins/create.cr13
-rw-r--r--src/actions/api/sign_ups/create.cr8
-rw-r--r--src/actions/api_action.cr17
-rw-r--r--src/actions/browser_action.cr45
-rw-r--r--src/actions/errors/show.cr63
-rw-r--r--src/actions/home/index.cr18
-rw-r--r--src/actions/me/show.cr5
-rw-r--r--src/actions/mixins/.keep0
-rw-r--r--src/actions/mixins/api/auth/helpers.cr28
-rw-r--r--src/actions/mixins/api/auth/require_auth_token.cr34
-rw-r--r--src/actions/mixins/api/auth/skip_require_auth_token.cr10
-rw-r--r--src/actions/mixins/auth/allow_guests.cr10
-rw-r--r--src/actions/mixins/auth/password_resets/base.cr7
-rw-r--r--src/actions/mixins/auth/password_resets/find_user.cr5
-rw-r--r--src/actions/mixins/auth/password_resets/require_token.cr17
-rw-r--r--src/actions/mixins/auth/password_resets/token_from_session.cr5
-rw-r--r--src/actions/mixins/auth/redirect_signed_in_users.cr19
-rw-r--r--src/actions/mixins/auth/require_sign_in.cr21
-rw-r--r--src/actions/mixins/auth/test_backdoor.cr13
-rw-r--r--src/actions/password_reset_requests/create.cr15
-rw-r--r--src/actions/password_reset_requests/new.cr7
-rw-r--r--src/actions/password_resets/create.cr17
-rw-r--r--src/actions/password_resets/edit.cr8
-rw-r--r--src/actions/password_resets/new.cr20
-rw-r--r--src/actions/sign_ins/create.cr16
-rw-r--r--src/actions/sign_ins/delete.cr7
-rw-r--r--src/actions/sign_ins/new.cr7
-rw-r--r--src/actions/sign_ups/create.cr16
-rw-r--r--src/actions/sign_ups/new.cr7
30 files changed, 463 insertions, 0 deletions
diff --git a/src/actions/api/me/show.cr b/src/actions/api/me/show.cr
new file mode 100644
index 0000000..0060271
--- /dev/null
+++ b/src/actions/api/me/show.cr
@@ -0,0 +1,5 @@
+class Api::Me::Show < ApiAction
+ get "/api/me" do
+ json UserSerializer.new(current_user)
+ end
+end
diff --git a/src/actions/api/sign_ins/create.cr b/src/actions/api/sign_ins/create.cr
new file mode 100644
index 0000000..3670356
--- /dev/null
+++ b/src/actions/api/sign_ins/create.cr
@@ -0,0 +1,13 @@
+class Api::SignIns::Create < ApiAction
+ include Api::Auth::SkipRequireAuthToken
+
+ post "/api/sign_ins" do
+ SignInUser.run(params) do |operation, user|
+ if user
+ json({token: UserToken.generate(user)})
+ else
+ raise Avram::InvalidOperationError.new(operation)
+ end
+ end
+ end
+end
diff --git a/src/actions/api/sign_ups/create.cr b/src/actions/api/sign_ups/create.cr
new file mode 100644
index 0000000..15bbd04
--- /dev/null
+++ b/src/actions/api/sign_ups/create.cr
@@ -0,0 +1,8 @@
+class Api::SignUps::Create < ApiAction
+ include Api::Auth::SkipRequireAuthToken
+
+ post "/api/sign_ups" do
+ user = SignUpUser.create!(params)
+ json({token: UserToken.generate(user)})
+ end
+end
diff --git a/src/actions/api_action.cr b/src/actions/api_action.cr
new file mode 100644
index 0000000..a16fd09
--- /dev/null
+++ b/src/actions/api_action.cr
@@ -0,0 +1,17 @@
+# Include modules and add methods that are for all API requests
+abstract class ApiAction < Lucky::Action
+ # APIs typically do not need to send cookie/session data.
+ # Remove this line if you want to send cookies in the response header.
+ disable_cookies
+ accepted_formats [:json]
+
+ include Api::Auth::Helpers
+
+ # By default all actions require sign in.
+ # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests.
+ include Api::Auth::RequireAuthToken
+
+ # By default all actions are required to use underscores to separate words.
+ # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+end
diff --git a/src/actions/browser_action.cr b/src/actions/browser_action.cr
new file mode 100644
index 0000000..674f208
--- /dev/null
+++ b/src/actions/browser_action.cr
@@ -0,0 +1,45 @@
+abstract class BrowserAction < Lucky::Action
+ include Lucky::ProtectFromForgery
+
+ # By default all actions are required to use underscores.
+ # Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes.
+ include Lucky::EnforceUnderscoredRoute
+
+ # This module disables Google FLoC by setting the
+ # [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`.
+ #
+ # This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used
+ # to track browsing history instead of using 3rd-party cookies.
+ #
+ # Remove this include if you want to use the FLoC tracking.
+ include Lucky::SecureHeaders::DisableFLoC
+
+ accepted_formats [:html, :json], default: :html
+
+ # This module provides current_user, sign_in, and sign_out methods
+ include Authentic::ActionHelpers(User)
+
+ # When testing you can skip normal sign in by using `visit` with the `as` param
+ #
+ # flow.visit Me::Show, as: UserFactory.create
+ include Auth::TestBackdoor
+
+ # By default all actions that inherit 'BrowserAction' require sign in.
+ #
+ # You can remove the 'include Auth::RequireSignIn' below to allow anyone to
+ # access actions that inherit from 'BrowserAction' or you can
+ # 'include Auth::AllowGuests' in individual actions to skip sign in.
+ include Auth::RequireSignIn
+
+ # `expose` means that `current_user` will be passed to pages automatically.
+ #
+ # In default Lucky apps, the `MainLayout` declares it `needs current_user : User`
+ # so that any page that inherits from MainLayout can use the `current_user`
+ expose current_user
+
+ # This method tells Authentic how to find the current user
+ # The 'memoize' macro makes sure only one query is issued to find the user
+ private memoize def find_current_user(id : String | User::PrimaryKeyType) : User?
+ UserQuery.new.id(id).first?
+ end
+end
diff --git a/src/actions/errors/show.cr b/src/actions/errors/show.cr
new file mode 100644
index 0000000..d01ed54
--- /dev/null
+++ b/src/actions/errors/show.cr
@@ -0,0 +1,63 @@
+# This class handles error responses and reporting.
+#
+# https://luckyframework.org/guides/http-and-routing/error-handling
+class Errors::Show < Lucky::ErrorAction
+ DEFAULT_MESSAGE = "Something went wrong."
+ default_format :html
+ dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
+
+ def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
+ if html?
+ error_html "Sorry, we couldn't find that page.", status: 404
+ else
+ error_json "Not found", status: 404
+ end
+ end
+
+ # When the request is JSON and an InvalidOperationError is raised, show a
+ # helpful error with the param that is invalid, and what was wrong with it.
+ def render(error : Avram::InvalidOperationError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json \
+ message: error.renderable_message,
+ details: error.renderable_details,
+ param: error.invalid_attribute_name,
+ status: 400
+ end
+ end
+
+ # Always keep this below other 'render' methods or it may override your
+ # custom 'render' methods.
+ def render(error : Lucky::RenderableError)
+ if html?
+ error_html DEFAULT_MESSAGE, status: error.renderable_status
+ else
+ error_json error.renderable_message, status: error.renderable_status
+ end
+ end
+
+ # If none of the 'render' methods return a response for the raised Exception,
+ # Lucky will use this method.
+ def default_render(error : Exception) : Lucky::Response
+ if html?
+ error_html DEFAULT_MESSAGE, status: 500
+ else
+ error_json DEFAULT_MESSAGE, status: 500
+ end
+ end
+
+ private def error_html(message : String, status : Int)
+ context.response.status_code = status
+ html_with_status Errors::ShowPage, status, message: message, status_code: status
+ end
+
+ private def error_json(message : String, status : Int, details = nil, param = nil)
+ json ErrorSerializer.new(message: message, details: details, param: param), status: status
+ end
+
+ private def report(error : Exception) : Nil
+ # Send to Rollbar, send an email, etc.
+ end
+end
diff --git a/src/actions/home/index.cr b/src/actions/home/index.cr
new file mode 100644
index 0000000..f780130
--- /dev/null
+++ b/src/actions/home/index.cr
@@ -0,0 +1,18 @@
+class Home::Index < BrowserAction
+ include Auth::AllowGuests
+
+ get "/" do
+ if current_user?
+ redirect Me::Show
+ else
+ # When you're ready change this line to:
+ #
+ # redirect SignIns::New
+ #
+ # Or maybe show signed out users a marketing page:
+ #
+ # html Marketing::IndexPage
+ html Lucky::WelcomePage
+ end
+ end
+end
diff --git a/src/actions/me/show.cr b/src/actions/me/show.cr
new file mode 100644
index 0000000..5e35848
--- /dev/null
+++ b/src/actions/me/show.cr
@@ -0,0 +1,5 @@
+class Me::Show < BrowserAction
+ get "/me" do
+ html ShowPage
+ end
+end
diff --git a/src/actions/mixins/.keep b/src/actions/mixins/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/actions/mixins/.keep
diff --git a/src/actions/mixins/api/auth/helpers.cr b/src/actions/mixins/api/auth/helpers.cr
new file mode 100644
index 0000000..6b51cb5
--- /dev/null
+++ b/src/actions/mixins/api/auth/helpers.cr
@@ -0,0 +1,28 @@
+module Api::Auth::Helpers
+ # The 'memoize' macro makes sure only one query is issued to find the user
+ memoize def current_user? : User?
+ auth_token.try do |value|
+ user_from_auth_token(value)
+ end
+ end
+
+ private def auth_token : String?
+ bearer_token || token_param
+ end
+
+ private def bearer_token : String?
+ context.request.headers["Authorization"]?
+ .try(&.gsub("Bearer", ""))
+ .try(&.strip)
+ end
+
+ private def token_param : String?
+ params.get?(:auth_token)
+ end
+
+ private def user_from_auth_token(token : String) : User?
+ UserToken.decode_user_id(token).try do |user_id|
+ UserQuery.new.id(user_id).first?
+ end
+ end
+end
diff --git a/src/actions/mixins/api/auth/require_auth_token.cr b/src/actions/mixins/api/auth/require_auth_token.cr
new file mode 100644
index 0000000..e018638
--- /dev/null
+++ b/src/actions/mixins/api/auth/require_auth_token.cr
@@ -0,0 +1,34 @@
+module Api::Auth::RequireAuthToken
+ macro included
+ before require_auth_token
+ end
+
+ private def require_auth_token
+ if current_user?
+ continue
+ else
+ json auth_error_json, 401
+ end
+ end
+
+ private def auth_error_json
+ ErrorSerializer.new(
+ message: "Not authenticated.",
+ details: auth_error_details
+ )
+ end
+
+ private def auth_error_details : String
+ if auth_token
+ "The provided authentication token was incorrect."
+ else
+ "An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header."
+ end
+ end
+
+ # Tells the compiler that the current_user is not nil since we have checked
+ # that the user is signed in
+ private def current_user : User
+ current_user?.as(User)
+ end
+end
diff --git a/src/actions/mixins/api/auth/skip_require_auth_token.cr b/src/actions/mixins/api/auth/skip_require_auth_token.cr
new file mode 100644
index 0000000..68098cf
--- /dev/null
+++ b/src/actions/mixins/api/auth/skip_require_auth_token.cr
@@ -0,0 +1,10 @@
+module Api::Auth::SkipRequireAuthToken
+ macro included
+ skip require_auth_token
+ end
+
+ # Since sign in is not required, current_user might be nil
+ def current_user : User?
+ current_user?
+ end
+end
diff --git a/src/actions/mixins/auth/allow_guests.cr b/src/actions/mixins/auth/allow_guests.cr
new file mode 100644
index 0000000..3961399
--- /dev/null
+++ b/src/actions/mixins/auth/allow_guests.cr
@@ -0,0 +1,10 @@
+module Auth::AllowGuests
+ macro included
+ skip require_sign_in
+ end
+
+ # Since sign in is not required, current_user might be nil
+ def current_user : User?
+ current_user?
+ end
+end
diff --git a/src/actions/mixins/auth/password_resets/base.cr b/src/actions/mixins/auth/password_resets/base.cr
new file mode 100644
index 0000000..77166a9
--- /dev/null
+++ b/src/actions/mixins/auth/password_resets/base.cr
@@ -0,0 +1,7 @@
+module Auth::PasswordResets::Base
+ macro included
+ include Auth::RedirectSignedInUsers
+ include Auth::PasswordResets::FindUser
+ include Auth::PasswordResets::RequireToken
+ end
+end
diff --git a/src/actions/mixins/auth/password_resets/find_user.cr b/src/actions/mixins/auth/password_resets/find_user.cr
new file mode 100644
index 0000000..cab02d5
--- /dev/null
+++ b/src/actions/mixins/auth/password_resets/find_user.cr
@@ -0,0 +1,5 @@
+module Auth::PasswordResets::FindUser
+ private def user : User
+ UserQuery.find(user_id)
+ end
+end
diff --git a/src/actions/mixins/auth/password_resets/require_token.cr b/src/actions/mixins/auth/password_resets/require_token.cr
new file mode 100644
index 0000000..15da423
--- /dev/null
+++ b/src/actions/mixins/auth/password_resets/require_token.cr
@@ -0,0 +1,17 @@
+module Auth::PasswordResets::RequireToken
+ macro included
+ before require_valid_password_reset_token
+ end
+
+ abstract def token : String
+ abstract def user : User
+
+ private def require_valid_password_reset_token
+ if Authentic.valid_password_reset_token?(user, token)
+ continue
+ else
+ flash.failure = "The password reset link is incorrect or expired. Please try again."
+ redirect to: PasswordResetRequests::New
+ end
+ end
+end
diff --git a/src/actions/mixins/auth/password_resets/token_from_session.cr b/src/actions/mixins/auth/password_resets/token_from_session.cr
new file mode 100644
index 0000000..820b91b
--- /dev/null
+++ b/src/actions/mixins/auth/password_resets/token_from_session.cr
@@ -0,0 +1,5 @@
+module Auth::PasswordResets::TokenFromSession
+ private def token : String
+ session.get?(:password_reset_token) || raise "Password reset token not found in session"
+ end
+end
diff --git a/src/actions/mixins/auth/redirect_signed_in_users.cr b/src/actions/mixins/auth/redirect_signed_in_users.cr
new file mode 100644
index 0000000..546bf7b
--- /dev/null
+++ b/src/actions/mixins/auth/redirect_signed_in_users.cr
@@ -0,0 +1,19 @@
+module Auth::RedirectSignedInUsers
+ macro included
+ include Auth::AllowGuests
+ before redirect_signed_in_users
+ end
+
+ private def redirect_signed_in_users
+ if current_user?
+ flash.success = "You are already signed in"
+ redirect to: Home::Index
+ else
+ continue
+ end
+ end
+
+ # current_user returns nil because signed in users are redirected.
+ def current_user
+ end
+end
diff --git a/src/actions/mixins/auth/require_sign_in.cr b/src/actions/mixins/auth/require_sign_in.cr
new file mode 100644
index 0000000..27a6f5e
--- /dev/null
+++ b/src/actions/mixins/auth/require_sign_in.cr
@@ -0,0 +1,21 @@
+module Auth::RequireSignIn
+ macro included
+ before require_sign_in
+ end
+
+ private def require_sign_in
+ if current_user?
+ continue
+ else
+ Authentic.remember_requested_path(self)
+ flash.info = "Please sign in first"
+ redirect to: SignIns::New
+ end
+ end
+
+ # Tells the compiler that the current_user is not nil since we have checked
+ # that the user is signed in
+ private def current_user : User
+ current_user?.as(User)
+ end
+end
diff --git a/src/actions/mixins/auth/test_backdoor.cr b/src/actions/mixins/auth/test_backdoor.cr
new file mode 100644
index 0000000..68c9d91
--- /dev/null
+++ b/src/actions/mixins/auth/test_backdoor.cr
@@ -0,0 +1,13 @@
+module Auth::TestBackdoor
+ macro included
+ before test_backdoor
+ end
+
+ private def test_backdoor
+ if LuckyEnv.test? && (user_id = params.get?(:backdoor_user_id))
+ user = UserQuery.find(user_id)
+ sign_in user
+ end
+ continue
+ end
+end
diff --git a/src/actions/password_reset_requests/create.cr b/src/actions/password_reset_requests/create.cr
new file mode 100644
index 0000000..8f3c513
--- /dev/null
+++ b/src/actions/password_reset_requests/create.cr
@@ -0,0 +1,15 @@
+class PasswordResetRequests::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/password_reset_requests" do
+ RequestPasswordReset.run(params) do |operation, user|
+ if user
+ PasswordResetRequestEmail.new(user).deliver
+ flash.success = "You should receive an email on how to reset your password shortly"
+ redirect SignIns::New
+ else
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/src/actions/password_reset_requests/new.cr b/src/actions/password_reset_requests/new.cr
new file mode 100644
index 0000000..7d16a7d
--- /dev/null
+++ b/src/actions/password_reset_requests/new.cr
@@ -0,0 +1,7 @@
+class PasswordResetRequests::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/password_reset_requests/new" do
+ html NewPage, operation: RequestPasswordReset.new
+ end
+end
diff --git a/src/actions/password_resets/create.cr b/src/actions/password_resets/create.cr
new file mode 100644
index 0000000..da1e711
--- /dev/null
+++ b/src/actions/password_resets/create.cr
@@ -0,0 +1,17 @@
+class PasswordResets::Create < BrowserAction
+ include Auth::PasswordResets::Base
+ include Auth::PasswordResets::TokenFromSession
+
+ post "/password_resets/:user_id" do
+ ResetPassword.update(user, params) do |operation, user|
+ if operation.saved?
+ session.delete(:password_reset_token)
+ sign_in user
+ flash.success = "Your password has been reset"
+ redirect to: Home::Index
+ else
+ html NewPage, operation: operation, user_id: user_id.to_i64
+ end
+ end
+ end
+end
diff --git a/src/actions/password_resets/edit.cr b/src/actions/password_resets/edit.cr
new file mode 100644
index 0000000..9408109
--- /dev/null
+++ b/src/actions/password_resets/edit.cr
@@ -0,0 +1,8 @@
+class PasswordResets::Edit < BrowserAction
+ include Auth::PasswordResets::Base
+ include Auth::PasswordResets::TokenFromSession
+
+ get "/password_resets/:user_id/edit" do
+ html NewPage, operation: ResetPassword.new, user_id: user_id.to_i64
+ end
+end
diff --git a/src/actions/password_resets/new.cr b/src/actions/password_resets/new.cr
new file mode 100644
index 0000000..5503468
--- /dev/null
+++ b/src/actions/password_resets/new.cr
@@ -0,0 +1,20 @@
+class PasswordResets::New < BrowserAction
+ include Auth::PasswordResets::Base
+
+ param token : String
+
+ get "/password_resets/:user_id" do
+ redirect_to_edit_form_without_token_param
+ end
+
+ # This is to prevent password reset tokens from being scraped in the HTTP Referer header
+ # See more info here: https://github.com/thoughtbot/clearance/pull/707
+ private def redirect_to_edit_form_without_token_param
+ make_token_available_to_future_actions
+ redirect to: PasswordResets::Edit.with(user_id)
+ end
+
+ private def make_token_available_to_future_actions
+ session.set(:password_reset_token, token)
+ end
+end
diff --git a/src/actions/sign_ins/create.cr b/src/actions/sign_ins/create.cr
new file mode 100644
index 0000000..af22588
--- /dev/null
+++ b/src/actions/sign_ins/create.cr
@@ -0,0 +1,16 @@
+class SignIns::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/sign_in" do
+ SignInUser.run(params) do |operation, authenticated_user|
+ if authenticated_user
+ sign_in(authenticated_user)
+ flash.success = "You're now signed in"
+ Authentic.redirect_to_originally_requested_path(self, fallback: Home::Index)
+ else
+ flash.failure = "Sign in failed"
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/src/actions/sign_ins/delete.cr b/src/actions/sign_ins/delete.cr
new file mode 100644
index 0000000..8d34612
--- /dev/null
+++ b/src/actions/sign_ins/delete.cr
@@ -0,0 +1,7 @@
+class SignIns::Delete < BrowserAction
+ delete "/sign_out" do
+ sign_out
+ flash.info = "You have been signed out"
+ redirect to: SignIns::New
+ end
+end
diff --git a/src/actions/sign_ins/new.cr b/src/actions/sign_ins/new.cr
new file mode 100644
index 0000000..3275b40
--- /dev/null
+++ b/src/actions/sign_ins/new.cr
@@ -0,0 +1,7 @@
+class SignIns::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/sign_in" do
+ html NewPage, operation: SignInUser.new
+ end
+end
diff --git a/src/actions/sign_ups/create.cr b/src/actions/sign_ups/create.cr
new file mode 100644
index 0000000..a291ca6
--- /dev/null
+++ b/src/actions/sign_ups/create.cr
@@ -0,0 +1,16 @@
+class SignUps::Create < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ post "/sign_up" do
+ SignUpUser.create(params) do |operation, user|
+ if user
+ flash.info = "Thanks for signing up"
+ sign_in(user)
+ redirect to: Home::Index
+ else
+ flash.info = "Couldn't sign you up"
+ html NewPage, operation: operation
+ end
+ end
+ end
+end
diff --git a/src/actions/sign_ups/new.cr b/src/actions/sign_ups/new.cr
new file mode 100644
index 0000000..2299df6
--- /dev/null
+++ b/src/actions/sign_ups/new.cr
@@ -0,0 +1,7 @@
+class SignUps::New < BrowserAction
+ include Auth::RedirectSignedInUsers
+
+ get "/sign_up" do
+ html NewPage, operation: SignUpUser.new
+ end
+end