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:
Juan Rodriguez
2021-06-14 11:46:25 -05:00
parent f63be42b4c
commit 9c7146820c
28 changed files with 288 additions and 58 deletions
+1 -1
View File
@@ -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
+4
View File
@@ -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
+5 -3
View File
@@ -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
+26
View File
@@ -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
-4
View File
@@ -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?
+11
View File
@@ -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
-11
View File
@@ -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
+12 -7
View File
@@ -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)
}
}
+27 -1
View File
@@ -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
View File
@@ -1,3 +1,4 @@
a {
cursor: pointer;
text-decoration: underline;
}
+2 -1
View File
@@ -2,6 +2,7 @@
class User < ApplicationRecord
validates_uniqueness_of :username
has_secure_password
has_many :links
end
+2
View File
@@ -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 -1
View File
@@ -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
+7
View File
@@ -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>
+1 -1
View File
@@ -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">
+61
View File
@@ -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">&#8203;</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>
+13
View File
@@ -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>
+61
View File
@@ -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">&#8203;</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>
+4
View File
@@ -0,0 +1,4 @@
<% if logged_in? %>
<h1>You are Logged In, <%= @current_user.username %></h1>
<%= button_to "Logout", '/logout', method: :post %>
<%end%>
-10
View File
@@ -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
View File
@@ -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
View File
@@ -1,7 +1,6 @@
#!/bin/sh
set -e
echo "Environment: $RAILS_ENV"
bundle check || bundle install --jobs 20 --retry 5
-3
View File
@@ -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
+11 -6
View File
@@ -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
+4 -4
View File
@@ -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') %>