
Add Member Signin
Add a Signup Context
(Use git checkout 6.usersignin
to catch up with the class)
Now that we have a schedule let’s add the ability for members to join the site - this will allow us to let people curate their own agenda at the conference.
We’re going to have several user resources as part of this application - at the very least we’ll have signup, login, and profile contexts. We’ll start with signup first.
Paste the following in our commandline:
mix phx.gen.html Signup User users username:string:unique password:string --web Signup
This should seem rather familiar by now - it will generate all the files for a Signup.User context. The web
flag makes Phoenix generate a namespace for the web parts. This will help us separate our authentication resources from our signup resources (more on that later).
The migration that was generated will ensure a unique username:
defmodule Fawkes.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :username, :string
add :password, :string
timestamps()
end
create unique_index(:users, [:username])
end
end
If we run the migration now we’ll get an error because we’re missing the router - so let’s hold off on that a bit.
The generated Fawkes.Signup
context will contain a lot of things we don’t need. For signup we will only need the new
and create
actions and related functionality - so let’s delete everything except change_user/1 and create_user/1. Since we no longer run queries in this context we can also delete the import Ecto.Query
line. The end should look something like this:
defmodule Fawkes.Signup do
@moduledoc """
The Signup context.
"""
alias Fawkes.Repo
alias Fawkes.Signup.User
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
end
We’ll do something similar to the controller - remove everything from FawkesWeb.Signup.UserController
that isn’t new/2
or create/2
. We’ll also change the redirect on success in create/2
to point to slot_path(conn, :index)
. It’ll look like this:
defmodule FawkesWeb.Signup.UserController do
use FawkesWeb, :controller
alias Fawkes.Signup
alias Fawkes.Signup.User
def new(conn, _params) do
changeset = Signup.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Signup.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: slot_path(conn, :index))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end
Next we’ll remove the templates we don’t need - delete templates/signup/user/edit.html.eex
, templates/signup/user/index.html.eex
, and templates/signup/user/show.html.eex
. There is also a Back
link in our templates/signup/user/new.html.eex
template that points to a route we’re not going to have yet. Let’s delete the line. Change the h2
title to Create Account
. Wrap the HTML in a div with a class named container
. The result looks like this:
<div class="container">
<h2>Create Account</h2>
<%= render "form.html", Map.put(assigns, :action, signup_user_path(@conn, :create)) %>
</div>
Now that we’ve removed what we don’t need we can add what we do need.
First - passwords should use an HTML input element with the type=password
attribute. Open templates/signup/user/form.html.eex
and change our password text_input
to password_input
.
<%= password_input f, :password, class: "form-control" %>
Now we’ll add the resources to our router - let’s have a the url for a new user be “localhost:4000/signup/new”. The generator suggests using a scope - we’ll copy and paste the suggested code:
scope "/signup", FawkesWeb.Signup, as: :signup do
pipe_through :browser
resources "/users", UserController
end
This makes our url for UserController
“localhost:4000/signup/users/new”… close - but let’s remove the users
segment.
resources "/", UserController, only: [:new, :create]
Path helpers look at the names of modules (as opposed to the url strings) to name our paths - so this change will not affect anything the generator created.
Now that we have a the router updated we can run our migration with mix ecto.migrate
.
Hash and Validate Passwords
(Use git checkout 6a.password
to catch up with the class)
We can now signup new users! We can improve things a bit with some validators… and we really should hash our passwords before we store them. All that happens will happen in Fawkes.Signup.User.changeset/2
.
We’ll start by adding an encryption library. There are a ton of great libraries available through hex (see Riverrun’s choosing the password algorithm for examples). We’ll use bcrypt_elixir by adding it (and comeonin) to our mix.exs
:
defp deps do
[
{:phoenix, "~> 1.3.2"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
# For datetime formating
{:timex, "~> 3.3"},
# For authentication
{:bcrypt_elixir, "~> 1.0"},
{:comeonin, "~>4.0"},
]
end
… and runing mix deps.get
on our commandline.
Now we’ll edit our Fawkes.Signup.User.changeset/2 - add a step in our pipeline which calls a private method to replace our plain text password with a hashed version.
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password])
|> validate_required([:username, :password])
|> unique_constraint(:username)
|> put_pass_hash()
end
This method takes and returns a changeset. We can transform a changeset using Ecto.Changeset.change/2
(available as just change/2
because we import Ecto.Changeset
). We’ll use a pattern match in the method signature to so that when a changeset is valid we hash the password with Comeonin.Bcrypt.hashpwsalt/1
. If the pattern match fails we’ll change the password to an empty string:
defp put_pass_hash(%{valid?: true, changes: params} = changeset) do
password = Comeonin.Bcrypt.hashpwsalt(params[:password])
change(changeset, password: password)
end
defp put_pass_hash(changeset) do
change(changeset, password: "")
end
While we’re here let’s add a few other useful validators to our changeset - we can do things like blacklist passwords with validate_exclusion
or ensure the length of the password with validate_length
. The resulting schema will look something like this:
defmodule Fawkes.Signup.User do
use Ecto.Schema
import Ecto.Changeset
@bad_passwords ~w(
12345678
password1
qwertyuiop
)
schema "users" do
field :password, :string
field :username, :string
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password])
|> validate_required([:username, :password])
|> unique_constraint(:username)
|> validate_exclusion(
:password,
@bad_passwords,
message: "That password is too common.")
|> validate_length(:password, min: 8)
|> put_pass_hash()
end
defp put_pass_hash(%{valid?: true, changes: params} = changeset) do
password = Comeonin.Bcrypt.hashpwsalt(params[:password])
change(changeset, password: password)
end
defp put_pass_hash(changeset) do
change(changeset, password: "")
end
end
You can read more on validations on the Ecto.Changeset Hexdocs.
Add an Auth Context
(Use git checkout 6b.auth
to catch up with the class)
Now that we can sign up we need to be able to log in. The process will be almost identical. Start with the generator:
mix phx.gen.html Auth User users username:string password:string --web Auth
This time we can delete the migration (since our new context will just use the same users table we generated before.)
We don’t create anything when a user logs in - so delete everything except change_user/1
from the Fawkes.Auth
context.
defmodule Fawkes.Auth do
@moduledoc """
The Auth context.
"""
alias Fawkes.Auth.User
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
end
Remove everything but new/2
and create/2
from FawkesWeb.Auth.UserController
. (We’ll eventually need a delete/2
) but the implementation is different so we’ll delete the generated default. Change the redirect on successs in create/2
to slot_path(conn, :index)
. The result will look like:
defmodule FawkesWeb.Auth.UserController do
use FawkesWeb, :controller
alias Fawkes.Auth
alias Fawkes.Auth.User
def new(conn, _params) do
changeset = Auth.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Auth.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: slot_path(conn, :index))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end
Delete templates/auth/user/edit.html.eex
, templates/auth/user/index.html.eex
, and templates/auth/user/show.html.eex
. Remove the Back
link in our templates/auth/user/new.html.eex
. Add a div with the class container
around it. Change the title to Log in
. The final result looks like this:
<div class="container">
<h2>Log in</h2>
<%= render "form.html", Map.put(assigns, :action, auth_user_path(@conn, :create)) %>
</div>
Change the input in templates/auth/user/form.html.eex
<%= password_input f, :password, class: "form-control" %>
Next we’ll return to the Fawkes.Auth context and add an authenticate_user/2
method. This method contain a pipeline to that takes the params and returns a boolean indicating whether the user/password combo is authentic.
def authenticate_user(%{"username" => user, "password" => pass}) do
user
|> fetch_user_by_username()
|> check_password(pass)
end
fetch_user_by username()
is a simple ecto query:
import Ecto.Query, warn: false
alias Fawkes.Repo
# . . .
def fetch_user_by_username(username) do
User
|> where([user], user.username == ^username)
|> Repo.one
end
For check_password/2
we’ll pattern match to ensure that if we don’t have a user we just return false:
defp check_password(%User{} = user, password) do
# Check the password
end
defp check_password(_, _), do: {:error, :incorrect}
… and then leverage Comeonin.Bcrypt.checkpw/2
to check the given password against the password on the found user. If it succeeds we’ll return an :ok tagged tuple… otherwise we’ll return an :error tagged tuple after running Comeonin.Bcrypt.dummy_checkpw/0
. (dummy_checkpw/0
defends against timing attacks.) Alias Bcrypt and add the check password function.
alias Comeonin.Bcrypt
# . . .
defp check_password(%User{} = user, password) do
password
|> Bcrypt.checkpw(user.password)
|> case do
true ->
{:ok, user}
false ->
Bcrypt.dummy_checkpw()
{:error, :incorrect}
end
end
Our final result will look something like this:
defmodule Fawkes.Auth do
@moduledoc """
The Auth context.
"""
import Ecto.Query, warn: false
alias Comeonin.Bcrypt
alias Fawkes.Auth.User
alias Fawkes.Repo
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
def authenticate_user(%{"username" => user, "password" => pass}) do
user
|> fetch_user_by_username()
|> check_password(pass)
end
defp fetch_user_by_username(username) do
User
|> where([user], user.username == ^username)
|> Repo.one
end
defp check_password(%User{} = user, password) do
password
|> Bcrypt.checkpw(user.password)
|> case do
true ->
{:ok, user}
false ->
Bcrypt.dummy_checkpw()
{:error, :incorrect}
end
end
defp check_password(_, _), do: false
end
Now we can change our FawkesWeb.Auth.UserController
to leverage authenticate_user/1
(instead of the non-existent Auth.create_user/1
)
defmodule FawkesWeb.Auth.UserController do
use FawkesWeb, :controller
alias Fawkes.Auth
alias Fawkes.Auth.User
def new(conn, _params) do
changeset = Auth.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Auth.authenticate_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: slot_path(conn, :index))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end
Add a Membership Context
(Use git checkout 6c.membership
to catch up with the class)
Now that we’re able to assess whether a user is authentic we need to make that information available to our controllers somehow. We need a user data type to leverage outside the Auth and Signup contexts. This new user context will lack access to password
and will give us a place to relate profiles and agendas without muddying the data types we use to signin and authenticate. To pull this off we’ll generate a new Membership
context. First the User
:
defmodule Fawkes.Membership.User do
use Ecto.Schema
schema "users" do
field(:username, :string)
timestamps()
end
end
And then a context with a method to find those users based on their id:
defmodule Fawkes.Membership do
@moduledoc """
Context responsible for managing profile information
"""
import Ecto.Query
alias Fawkes.Membership.User
alias Fawkes.Repo
def get_user(id) do
User
|> where([user], user.id == ^id)
|> Repo.one()
end
end
Sign and Verify (or reject) Authentication Tokens
(Use git checkout 6d.signin
to catch up with the class)
Now that we’ve defined Fawkes.Membership.User
to use throughout the app - we need to figure out how to keep that data handy in a session once the user has authenticated. For that we’ll use Guardian - library that signs and verifies JSON Web Tokens. That topic is deep so we’re gonna accept the magic here. For now - Guardian is how we’ll log people in. We’ll add it (and Comeonin as a dependency) to our mix.exs
file.
defp deps do
[
{:phoenix, "~> 1.3.2"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
# For datetime formating
{:timex, "~> 3.3"},
# For authentication
{:bcrypt_elixir, "~> 1.0"},
{:comeonin, "~> 4.0"},
{:guardian, "~> 1.0"},
]
end
…then run mix deps.get
in our terminal.
Guardian requires a bit of setup.
First we need to create something to parse token information. According to the documentation this module needs to use
Guardian and implement two methods. subject_for_token/2
needs to generate a value in session which can later be used to look up a user. We’ll use the user’s id so we can leverage that Fawkes.Membership.get_user/1
method we created. Then we need a resource_from_claims/1
which takes the id (called a “sub” … short for “subject”) and returns a Fawkes.Membership.User. We can then access that user in our application.
This module will be responsible for translating iodata from the web - so let’s create FawkesWeb.Guardian.Tokenizer
in the lib/fawkes_web/guardian
folder - adding use Guardian, otp_app: <app_name>
the way the documentation asks:
defmodule FawkesWeb.Guardian.Tokenizer do
use Guardian, otp_app: :fawkes
end
Then add to it our subject_for_token/2
. Later on we’ll hand it a Signup or Auth User and it needs to return an :ok tagged tuple containing the id of the given user as a string.:
def subject_for_token(%{id: id}, _) do
{:ok, to_string(id)}
end
Then we add resource_from_claims/1
- this will need to return with an :ok tagged tuple with the Membership User or an :error tagged tuple. Guardian will hand it a JWT “claim” - which will (by convention) store our token subject (the id) in a key called "sub"
. Add alias Fawkes.Membership
. Then add this function:
def resource_from_claims(claims) do
case Membership.get_user(claims["sub"]) do
nil -> {:error, :resource_not_found}
user -> {:ok, user}
end
end
The resulting module should look like this:
defmodule FawkesWeb.Guardian.Tokenizer do
use Guardian, otp_app: :fawkes
alias Fawkes.Membership
def subject_for_token(%{id: id}, _) do
{:ok, to_string(id)}
end
def resource_from_claims(claims) do
case Membership.get_user(claims["sub"]) do
nil -> {:error, :resource_not_found}
user -> {:ok, user}
end
end
end
Next we have some configuration to do - handing guardian a secret key to use for encryption and telling it where our Token logic lives. Run mix phx.gen.secret
to get a random string to use for this - then in both config/dev.exs
and config/test.exs
add the following:
config :fawkes, FawkesWeb.Guardian.Tokenizer,
issuer: "fawkes",
secret_key: "our random string"
Then in config/prod.exs
we’ll want to leverage environment vars for our secret key for security:
config :fawkes,
FawkesWeb.Guardian.Tokenizer,
issuer: "fawkes",
secret_key: Map.fetch!(System.get_env(),
"GUARDIAN_KEY")
(If you’ve got phx.server running you’ll need to restart it to see this change.)
Now we’re ready to sign our JWT. In FawkesWeb.Auth.UserController
we’ll do that by calling a method from a meta-programmed module Guardian creates called FawkesWeb.Guardian.Tokenizer.Plug.sign_in/2
. Add this line |> GuardianPlug.sign_in(user)
below the flash message in the create function. The result (after some alias goodness) looks like this:
defmodule FawkesWeb.Auth.UserController do
use FawkesWeb, :controller
alias Fawkes.Auth
alias Fawkes.Auth.User
alias FawkesWeb.Guardian.Tokenizer.Plug, as: GuardianPlug
def new(conn, _params) do
changeset = Auth.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Auth.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> GuardianPlug.sign_in(user)
|> redirect(to: slot_path(conn, :index))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end
While we’re here let’s add a delete/2
action to allow us to logout. This will call FawkesWeb.Guardian.Tokenizer.Plug.sign_out/2
def delete(conn, _) do
conn
|> GuardianPlug.sign_out()
|> redirect(to: page_path(conn, :index))
end
We’ll come back to delete later - for now let’s add login new and create to our routes:
scope "/login", FawkesWeb.Auth, as: :auth do
pipe_through :browser
resources "/", UserController, only: [:new, :create]
end
Adding a Plug Pipelines for Authentication
(Use git checkout 6e.auth_pipeline
to catch up with the class)
Ok - now we can sign and unsign authentication tokens… but to make the resource available we need to add a plug.
Plug is the core of the Phoenix Framework. It defines a type - called a Connection Struct (or conn) which is passed through a “plug pipeline”. The connection struct holds the state of the request - and is transformed at each step of the pipeline. For most pages on the site we want Guardian to add verify the JWT we created during signup and then add it to the conn for later use.
Ok… a personal note - Phoenix isn’t super magical… but Guardian kinda is. Getting in to why this thing works the way it does is probably not “basic” - so I’m gonna get hand wavy here. Follow along and it will probably make more sense in a couple minutes. Ask questions if you have them - and plan to come back and read the documentation for Guardian at some point in the future to get a better understanding.
To do that we first need to tell Guardian how to handle an invalid JWT. Following the documentation - this module we generate needs to implement an auth_error/3
method. We’ll just use the boilerplate provided by the documentation and add the following to lib/fawkes_web/guardian/error_handler.ex
:
defmodule FawkesWeb.Guardian.ErrorHandler do
@moduledoc """
Logic for handling errors during the auth process
"""
import Plug.Conn
def auth_error(conn, {type, _reason}, _opts) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(401, to_string(type))
end
end
Now we can generate a module to act as plug pipeline for Guardian to verify tokens. Following documentation we’ll add a lib/fawkes_web/guardian/plug.ex
defmodule FawkesWeb.Guardian.Plug do
@moduledoc """
Pipeline which ensures a user is authenticated
"""
use Guardian.Plug.Pipeline,
otp_app: :fawkes,
error_handler: FawkesWeb.Guardian.ErrorHandler,
module: FawkesWeb.Guardian.Tokenizer
# If there is a session token, validate it
plug(Guardian.Plug.VerifySession, claims: %{"typ" => "access"})
# If there is an authorization header, validate it
plug(Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"})
# Load the user if either of the verifications worked
plug(Guardian.Plug.LoadResource, allow_blank: true)
end
Adding this plug to a pipeline (back to that in a sec) will verify the request and stash the information in our connection struct. We’ll have a method available, FawkesWeb.Guardian.Tokenizer.Plug.current_resource/1
, which takes our conn and returns the Membership User. We can generate a plug of our own (in lib/fawkes_web/guardian/current_user_plug.ex
) to place this in an easier-to-access location using Plug.Conn.assign/3:
defmodule FawkesWeb.Guardian.CurrentUserPlug do
@moduledoc """
Plug that populates the current_user assigns
"""
alias FawkesWeb.Guardian.Tokenizer.Plug, as: GuardianPlug
alias Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
Conn.assign(conn, :current_user, GuardianPlug.current_resource(conn))
end
end
Once we use this we can get our current_user by using conn.assigns.current_user
.
Using Our New Pipelines in the Router
(Use git checkout 6f.router_pipeline
to catch up with the class)
That was a lot - let’s set up our router and then show it in action. In the router we’ll create a new pipeline called guardian which uses the two plugs we just created to verify JWTs:
pipeline :guardian do
plug FawkesWeb.Guardian.Plug
plug FawkesWeb.Guardian.CurrentUserPlug
end
Then we’ll create another pipeline to force authentication - this will leverage a default plug from Guardian which will use the error handler module we created if no verified JWT is found.
pipeline :ensure_auth do
plug Guardian.Plug.EnsureAuthenticated
end
The order of these is important - we’ll need our :ensure_auth pipeline to only run after the :guardian pipeline (otherwise the connection struct will not have a verified token yet). We’ll only want to let users logout if they are authenticated - so let’s try that out with a logout route:
scope "/", FawkesWeb do
pipe_through [:browser, :guardian, :ensure_auth]
post("/logout", Auth.UserController, :delete)
end
And add just the guardian pipeline to the rest of our routes (so we have current_user available)
scope "/", FawkesWeb do
pipe_through [:browser, :guardian]
# ...
end
scope "/signup", FawkesWeb.Signup, as: :signup do
pipe_through [:browser, :guardian]
# ...
end
scope "/login", FawkesWeb.Auth, as: :auth do
pipe_through [:browser, :guardian]
# ...
end
The result should look like this:
defmodule FawkesWeb.Router do
use FawkesWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
pipeline :guardian do
plug FawkesWeb.Guardian.Plug
plug FawkesWeb.Guardian.CurrentUserPlug
end
pipeline :ensure_auth do
plug Guardian.Plug.EnsureAuthenticated
end
scope "/", FawkesWeb do
pipe_through [:browser, :guardian, :ensure_auth]
post("/logout", PageController, :logout)
end
scope "/", FawkesWeb do
pipe_through [:browser, :guardian]
get "/", PageController, :index
resources("/schedule", ScheduleController, only: [:index, :show])
resources("/audience", AudienceController, only: [:show])
resources("/category", CategoryController, only: [:show])
resources("/location", LocationController, only: [:show])
resources("/speaker", SpeakerController, only: [:index, :show])
resources("/talk", TalkController, only: [:show])
end
scope "/signup", FawkesWeb.Signup, as: :signup do
pipe_through [:browser, :guardian]
resources "/", UserController, only: [:new, :create]
end
scope "/login", FawkesWeb.Auth, as: :auth do
pipe_through [:browser, :guardian]
resources "/", UserController, only: [:new, :create]
end
# Other scopes may use custom stacks.
# scope "/api", FawkesWeb do
# pipe_through :api
# end
end
Adding Authentication Links to the Layout
Finally we need to add links so that users can access signup, login and logout globally. In our lib/fawkes_web/templates/layout/app.html.eex
render a partial. After the link for talk (maybe line 31), add this code to render a partial for session list.
<%= render(__MODULE__, "session_list_items.html", conn: @conn) %>
Create a new file in the layout template folder lib/fawkes_web/templates/layout/session_list_items.html.eex
. Add the following code to create an account and login.
<%= if is_nil @conn.assigns[:current_user] do %>
<li><%= link gettext("Create Account"), to: signup_user_path(@conn, :new) %></li>
<li><%= link gettext("Log In"), to: auth_user_path(@conn, :new) %></li>
<% else %>
<li><%= link gettext("Logout"), to: user_path(@conn, :delete), method: :post%></li>
<% end %>