feat: ✨ Sessions controller
Sessions helper methods, login and signup modals, load partial views with stimulus Situmuls usersController, Create and Destroy user sessions
This commit is contained in:
@@ -19,7 +19,7 @@ gem 'webpacker'
|
||||
# gem 'mini_racer', platforms: :ruby
|
||||
|
||||
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
||||
# gem 'turbolinks', '~> 5'
|
||||
gem 'turbolinks', '~> 5'
|
||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||
gem 'jbuilder', '~> 2.5'
|
||||
# Use Redis adapter to run Action Cable in production
|
||||
|
||||
@@ -173,6 +173,9 @@ GEM
|
||||
thor (1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
thread_safe (0.3.6-java)
|
||||
turbolinks (5.2.1)
|
||||
turbolinks-source (~> 5.2)
|
||||
turbolinks-source (5.2.0)
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (4.2.0)
|
||||
@@ -220,6 +223,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
spring
|
||||
spring-watcher-listen (~> 2.0.0)
|
||||
turbolinks (~> 5)
|
||||
uglifier (>= 1.3.0)
|
||||
web-console (>= 3.3.0)
|
||||
webdrivers
|
||||
|
||||
@@ -33,7 +33,9 @@ docker-compose run --rm app rubocop
|
||||
- [x] Link controller (handle redirection)
|
||||
- [x] Main page with input box
|
||||
- [x] Create user model
|
||||
- [ ] User unit tests
|
||||
- [ ] Add userId key to link model
|
||||
- [ ] Login and logout (sessions)
|
||||
- [x] User unit tests
|
||||
- [x] Add userId key to link model
|
||||
- [x] Login and logout (sessions)
|
||||
- [x] User links view
|
||||
- [ ] Modals layout
|
||||
- [ ] Setup Redis for production cache_store
|
||||
@@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include SessionsHelper
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SessionsController < ApplicationController
|
||||
before_action :authenticate, except: %i[create]
|
||||
|
||||
def create
|
||||
@user = User.find_by(username: session_params[:username])
|
||||
if @user&.authenticate(session_params[:password])
|
||||
session[:user_id] = @user.id
|
||||
redirect_to '/'
|
||||
else
|
||||
render json: nil, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
reset_session
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def session_params
|
||||
params.permit(:username, :password)
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsersController < ApplicationController
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.create(user_params)
|
||||
if @user.errors.any?
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SessionsHelper
|
||||
def logged_in?
|
||||
!@current_user.nil?
|
||||
end
|
||||
|
||||
def authenticate
|
||||
@current_user = User.find_by(id: session[:user_id])
|
||||
end
|
||||
end
|
||||
@@ -1,15 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module UsersHelper
|
||||
def current_user
|
||||
User.find_by(id: session[:user_id])
|
||||
end
|
||||
|
||||
def logged_in?
|
||||
!current_user.nil?
|
||||
end
|
||||
|
||||
def authorized
|
||||
redirect_to '/welcome' unless logged_in?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Controller } from "stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["url", "output"]
|
||||
static targets = ["url", "output", "userLinks"]
|
||||
|
||||
onSuccess(event) {
|
||||
const [, , xhr] = event.detail
|
||||
this.outputTarget.innerHTML = xhr.response
|
||||
initialize() {
|
||||
this.loggedIn = Boolean(document.querySelector('meta[name="logged-in"]').getAttribute('content') === 'true')
|
||||
}
|
||||
|
||||
onError(event) {
|
||||
onCreateLinkSuccess(event) {
|
||||
const [, , xhr] = event.detail
|
||||
this.outputTarget.innerHTML = xhr.response
|
||||
if (this.loggedIn && !this.userLinksTarget.innerHTML.includes(xhr.response)) {
|
||||
this.userLinksTarget.innerHTML = xhr.response + this.userLinksTarget.innerHTML
|
||||
}
|
||||
}
|
||||
|
||||
onCreateLinkError(event) {
|
||||
const [data, ,] = event.detail
|
||||
|
||||
const urlError = `Url: ${data.url.join(' ')}`
|
||||
|
||||
alert(urlError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
import { Controller } from "stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["signupModal", "loginModal"]
|
||||
|
||||
openLoginModal() {
|
||||
this.loginModalTarget.classList.remove("hidden")
|
||||
}
|
||||
|
||||
closeLoginModal() {
|
||||
this.loginModalTarget.classList.add("hidden")
|
||||
}
|
||||
|
||||
openSignupModal() {
|
||||
this.signupModalTarget.classList.remove("hidden")
|
||||
}
|
||||
|
||||
closeSignupModal() {
|
||||
this.signupModalTarget.classList.add("hidden")
|
||||
}
|
||||
|
||||
onSignupSuccess() {
|
||||
this.closeSignupModal();
|
||||
}
|
||||
|
||||
onLoginSuccess() {
|
||||
this.closeLoginModal();
|
||||
}
|
||||
|
||||
onError(event) {
|
||||
const [data, ,] = event.detail
|
||||
|
||||
|
||||
const usernameError = `Username: ${data.username.join(' ')}`
|
||||
const passwordError = `Password: ${data.username.join(' ')}`
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
class User < ApplicationRecord
|
||||
validates_uniqueness_of :username
|
||||
|
||||
has_secure_password
|
||||
|
||||
has_many :links
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<meta name="logged-in" content="<%= logged_in? %>" />
|
||||
|
||||
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
||||
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
</head>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= form_with model: Link.new, url: links_path(@link), data: { action: 'ajax:success->links#onSuccess ajax:error->links#onError' } do |f| %>
|
||||
<%= form_with model: Link.new, url: links_path(@link), data: { action: 'ajax:success->links#onCreateLinkSuccess ajax:error->links#onCreateLinkError' } do |f| %>
|
||||
<div class="col-span-3 sm:col-span-2">
|
||||
<label for="company_website" class="block text-sm font-medium text-gray-700">
|
||||
Website
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<div data-links-target="userLinks">
|
||||
<% if logged_in? %>
|
||||
<% @current_user.links.each do |link| %>
|
||||
<%= render partial: "links/show", locals: { link: link } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,11 +1,10 @@
|
||||
<div data-controller="links" class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="py-2 align-middle inline-block sm:px-6 lg:px-8">
|
||||
<div class="shadow sm:rounded-md sm:overflow-hidden">
|
||||
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<%= render partial: "links/form" %>
|
||||
<div data-links-target="output"></div>
|
||||
<%= button_to "Sign Up", '/users/new', method: :get%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<label for="about" class="block text-sm font-medium text-gray-700">
|
||||
Short url
|
||||
</label>
|
||||
<a data-links-target="shortUrl" href="<%= link.short %>" class="mt-1 text-sm text-gray-600" target="_blank" rel="noreferrer"><%= link.short %></a>
|
||||
<a href="<%= link.short %>" class="mt-1 text-sm text-gray-600" target="_blank" rel="noreferrer"><%= link.short %></a>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 sm:col-span-2">
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<div data-users-target="loginModal" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<%= form_with url: '/login', data: { action: 'ajax:success->users#onLoginSuccess ajax:error->users#onError' }, class: "inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" do |f| %>
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Login
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
<%= f.label :username%><br>
|
||||
<%= f.text_field :username%><br>
|
||||
<%= f.label :password%><br>
|
||||
<%= f.password_field :password%><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<%= f.submit "Login", class: "mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" %>
|
||||
<button data-action="users#closeLoginModal" type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<%end%>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div data-controller="users">
|
||||
<button data-action="users#openLoginModal">Login</button>
|
||||
<button data-action="users#openSignupModal">Sign Up</button>
|
||||
|
||||
<%= render partial: "users/show" %>
|
||||
<%= render partial: "users/new" %>
|
||||
<%= render partial: "sessions/new" %>
|
||||
</div>
|
||||
|
||||
<div data-controller="links">
|
||||
<%= render partial: "links/new" %>
|
||||
<%= render partial: "links/index" %>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
<div data-users-target="signupModal" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<%= form_with model: User.new, url: users_path(@user), data: { action: 'ajax:success->users#onSignupSuccess ajax:error->users#onError' }, class: "inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" do |f| %>
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Deactivate account
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
<%= f.label :username%><br>
|
||||
<%= f.text_field :username%><br>
|
||||
<%= f.label :password%><br>
|
||||
<%= f.password_field :password%><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<%= f.submit "Submit", class: "mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" %>
|
||||
<button data-action="users#closeSignupModal" type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<%end%>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<% if logged_in? %>
|
||||
<h1>You are Logged In, <%= @current_user.username %></h1>
|
||||
<%= button_to "Logout", '/logout', method: :post %>
|
||||
<%end%>
|
||||
@@ -1,10 +0,0 @@
|
||||
<h1>Sign Up</h1>
|
||||
<div data-controller="users">
|
||||
<%= form_for @user, data: { action: 'ajax:error->users#onError' } do |f| %>
|
||||
<%= f.label :username%><br>
|
||||
<%= f.text_field :username%><br>
|
||||
<%= f.label :password%><br>
|
||||
<%= f.password_field :password%><br>
|
||||
<%= f.submit %>
|
||||
<%end%>
|
||||
</div>
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.routes.draw do
|
||||
root 'links#index'
|
||||
root 'sessions#welcome'
|
||||
|
||||
get '/:slug', to: 'links#redirect', as: :short
|
||||
|
||||
post 'login', to: 'sessions#create', as: :login
|
||||
post 'logout', to: 'sessions#destroy', as: :logout
|
||||
|
||||
resources :links, only: %i[create]
|
||||
resources :users, only: %i[new create]
|
||||
resources :users, only: %i[create]
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
echo "Environment: $RAILS_ENV"
|
||||
|
||||
bundle check || bundle install --jobs 20 --retry 5
|
||||
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
"autoprefixer": "^9",
|
||||
"css-loader": "^5.2.6",
|
||||
"css-minimizer-webpack-plugin": "^3.0.1",
|
||||
"css-what": "^5.0.1",
|
||||
"glob-parent": "^5.1.2",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"normalize-url": "^4.5.1",
|
||||
"postcss": "^8.2.10",
|
||||
"stimulus": "^2.0.0",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
test 'Should create an user session' do
|
||||
user = users(:one)
|
||||
params = { username: user.username, password: '12345' }
|
||||
post login_url, params: params
|
||||
|
||||
assert_redirected_to '/'
|
||||
end
|
||||
|
||||
test 'Should return 401 with wrong credentials' do
|
||||
user = users(:one)
|
||||
params = { username: user.username, password: 'test' }
|
||||
post login_url, params: params
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test 'Should destroy session' do
|
||||
post logout_url
|
||||
|
||||
assert_redirected_to '/'
|
||||
end
|
||||
end
|
||||
@@ -3,13 +3,18 @@
|
||||
require 'test_helper'
|
||||
|
||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
test 'should get new' do
|
||||
get users_new_url
|
||||
assert_response :success
|
||||
test 'Should create an user' do
|
||||
params = { user: { username: 'testing', password: 'testing' } }
|
||||
post users_url, params: params
|
||||
|
||||
assert_redirected_to '/'
|
||||
end
|
||||
|
||||
test 'should get create' do
|
||||
get users_create_url
|
||||
assert_response :success
|
||||
test 'Should return 422 with existing username' do
|
||||
user = users(:one)
|
||||
params = { user: { username: user.username, password: 'testing' } }
|
||||
post users_url, params: params
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
Vendored
+4
-4
@@ -1,9 +1,9 @@
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
username: MyString
|
||||
password_digest: MyString
|
||||
username: sjdonado
|
||||
password_digest: <%= BCrypt::Password.create('12345') %>
|
||||
|
||||
two:
|
||||
username: MyString
|
||||
password_digest: MyString
|
||||
username: other_user
|
||||
password_digest: <%= BCrypt::Password.create('67891') %>
|
||||
|
||||
Reference in New Issue
Block a user