Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/drujensen/amber-auth-example
Example of how to perform authentication in an Amber App
https://github.com/drujensen/amber-auth-example
Last synced: 20 days ago
JSON representation
Example of how to perform authentication in an Amber App
- Host: GitHub
- URL: https://github.com/drujensen/amber-auth-example
- Owner: drujensen
- License: mit
- Created: 2017-07-28T00:28:35.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2017-07-28T06:18:30.000Z (over 7 years ago)
- Last Synced: 2024-08-01T00:41:16.206Z (3 months ago)
- Language: Crystal
- Size: 2.24 MB
- Stars: 6
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Amber Auth Example
This is an example of how to handle authentication with Amber.
## Setup
Let's create a new application:
```bash
amber new auth -d mysql --deps
cd auth
```Next, we scaffold a new User model:
```bash
amber generate scaffold User email:string encrypted_password:string
```and we will need a sessions controller to handle login and logout:
```bash
amber generate controller Session new create delete
```Create the database using mysql console:
```mysql
create database auth_development;
```Run migrations:
```bash
amber migrate up
```If this fails, check your `config/database.yml` and make sure it points to your mysql db.
## Code
Now for some code. In the `models/user.cr`, we need to create the `encrypted_password` and verify that the passwords match. We will use the standard library `crypto/bcrypt/password`
```crystal
require "kemalyst-model/adapter/mysql"
require "crypto/bcrypt/password"class User < Kemalyst::Model
adapter mysql# id : Int64 primary key is created for you
field email : String
field encrypted_password : String
timestampsdef password=(password)
@encrypted_password = Crypto::Bcrypt::Password.create(password, cost: 10).to_s
enddef authenticate(password : String)
if enc = @encrypted_password
bcrypt_password = Crypto::Bcrypt::Password.new(enc)
return bcrypt_password == password
else
return false
end
end
end
```We added two methods. A setter for `password=` and an `authenticate` method.
The setter will encrypt the password and store it in the `encrypted_password` field in the database.
The `authenticate` method will check to make sure the password provided matches the `encrypted_password`.
Next, we need to update the controller and the view to ask for the `password` instead of the `encrypted_password`.
In the `controllers/user_controller.cr`
```crystal
def create
user = User.new(params.to_h.select(["email"))
user.password = params["password"]
...
end...
def update
if user = User.find(params["id"])
user.set_attributes(params.to_h.select(["email"]))
user.password = params["password"]
...
end
```We remove setting the `encrypted_password` directly and use the new `password=` setter instead.
In the `views/users/_form.slang`:
```slang
...
form action="#{ action }" method="post"
== csrf_tag
- if user.id
input type="hidden" name="_method" value="patch"
div.form-group
input.form-control type="text" name="email" placeholder="Email" value="#{ user.email }"
div.form-group
input.form-control type="password" name="password" placeholder="password"
button.btn.btn-primary.btn-xs type="submit" Submit
a.btn.btn-default.btn-xs href="/users" back
```We add the `== csrf_tag` and replace the `encrypted_password` input with the `password` input. We also change the type of input to `password`.
Ok. Now lets move on to the `SessionController` and create the login screen.
In the `controllers/session_controller.cr`:
```crystal
class SessionController < ApplicationController
def new
render("new.slang")
enddef create
email = params["email"]
password = params["password"]
user = User.find_by :email, email
if user && user.authenticate(password)
session["user_id"] = user.id.to_s
flash["info"] = "Successfully logged in"
redirect_to "/"
else
flash["danger"] = "Invalid email or password"
render("new.slang")
end
enddef delete
context.clear_session
flash["info"] = "Logged out. See ya later!"
redirect_to "/"
end
end
```Now we need a login page. Remove the `views/session/create.slang` and `views/session/delete.slang` and update the
`views/session/new.slang`:
```slang
form action="/session" method="post"
== csrf_tag
div.form-group
input.form-control type="email" name="email" placeholder="Email"
div.form-group
input.form-control type="password" name="password" placeholder="Password"
button.btn.btn-primary.btn-xs type="submit" Login
```Let's update the `config/routes.cr` and replace the `session` paths with `/login` and `/logout` paths:
```crystal
...
routes :web do
get "/login", SessionController, :new
post "/session", SessionController, :create
get "/logout", SessionController, :delete
resources "/users", UserController
get "/", HomeController, :index
end
...
```Ok, we are almost there.
The next part is the most complicated. We are going to create some middleware that will perform the authentication.
Create a new file in `src/middleware/authenticate.cr`:
```crystal
class HTTP::Server::Context
property current_user : User?
endclass Authenticate < Amber::Pipe::Base
def call(context)
user_id = context.session["user_id"]?
if user = User.find(user_id.to_s)
context.current_user = user
call_next(context)
else
return call_next(context) if ["/login","/session"].includes?(context.request.path)
context.flash["warning"] = "Please login"
context.response.headers.add "Location", "/login"
context.response.status_code = 302
end
end
end
```This change may need some explanation.
The first thing we are doing is re-opening the context and adding a `current_user` property. This allows us to access the `context.current_user` on any page including the layout.
Next we are creating a new Amber::Pipe that will perform the authentication. This is similar to what Devise does in a rails app.
If we find the user, we set the `current_user` in the context, otherwise we redirect to the `/login` page.
One exception is if we are on the `/login` or `/session` path, we want to skip redirecting to the login page again.
Now we need to add this Pipe to the `:web` pipeline in the `config/routes.cr:
```crystal
require "../src/middleware/*"Amber::Server.instance.config do |app|
pipeline :web do
# Plug is the method to use connect a pipe (middleware)
# A plug accepts an instance of HTTP::Handler
plug Amber::Pipe::Logger.new
plug Amber::Pipe::Session.new
plug Amber::Pipe::Flash.new
plug Amber::Pipe::CSRF.new
plug Authenticate.new
end
...
```
We enable the `CSRF` pipe and add the `Authenticate` pipe.Don't forget to require the `..src/middleware/*`.
Finally, let's update the `views/layouts/_nav.slang` so it hides the `users` navigation and shows a `login`, `logout`:
```slang
- active = context.request.path == "/" ? "active" : ""
a class="nav-item #{active}" href="/" Home
- if context.current_user
a class="nav-item pull-right" href="/logout" Logout
- active = context.request.path == "/users" ? "active" : ""
a class="nav-item #{active}" href="/users" Users
- else
- active = context.request.path == "/login" ? "active" : ""
a class="nav-item #{active} pull-right" href="/login" Login
```
## SeedsCreate a new file `db/seeds.cr`:
```crystal
require "amber"
require "../src/models/*"user = User.new
user.email = "[email protected]"
user.password = "password"
user.save
```Seed the database by running:
```bash
crystal db/seeds.cr
```## Run
```bash
amber w
```