I have been looking for a somewhat challenging app apart from the simple demos to write in Elixir using Phoenix framework for quite sometime. Having Ruby/Rails familiarity, I picked the Ruby on Rails Tutorial — Learn Web Development with Rails (https://www.railstutorial.org) by Michael Hartl (in fact, I learned Rails reading this tutorial!) and decided to convert the twitter like Sample App presented pretty much throughout the tutorial.

I called it Minitwitter (probably not a good choice of name but such difficult is naming in Computer Science!). I think it’s a fairly complex app with many features typically not covered in many books. So, I took the challenge to convert this app to Elixir/Phoenix and share my experience with Elixir/Phoenix community.

Minitwitter App Homepage

Complete Code: Complete code for the Elixir/Phoenix version of the app is available on my github here — https://github.com/imeraj/Phoenix_Playground/tree/master/1.4/minitwitter

In case, anyone is interested in the Ruby/Rails version, I have complete code with some additional features (like presence) here too — https://github.com/imeraj/miniTwitter

Prerequisites: I assume that you are familiar with both Ruby/Rails and Elixir/Phoenix. Knowing Ruby/Rails is not much of a requirement to understand the code except that you should be able to follow the tutorial to understand the features of the app and how it’s implemented to be able to follow the code. I will cover some highlights of the app here but due to the complexity and scope of the app it’s not possible to cover complete implementation here; that will probably require writing another tutorial/book! I will refer to Michael’s tutorial to follow the code.

Elixir/Phoenix versions: I have used Elixir 1.7.4 with Erlang/OTP 21 and Phoenix 1.4.

What’s covered as part of this rewrite: Pretty much all the features work as is from the tutorial. Not being a frontend developer, I had to sacrifice some aesthetic beauty and some fine-tuning here and there in frontend.

What’s not covered as part of this rewrite: Mostly tests. I started writing the tests but I became impatient writing all the tests. So I skipped writing the tests. I think Programming Phoenix 1.4 (https://pragprog.com/book/phoenix14/programming-phoenix-1-4) covers all details to write tests in Phoenix and if you follow this book you should be able to add the tests required for this app.

I will provide the tools/libraries used with some code excerpts day by day basis. I have provided good description in git commit messages so that those along with this write-up and Michael’s tutorial can help you understand the Elixir/Phoenix version.

Day 1 (Jan 10, 2019)

What’s covered: Chapter 3 & 4 of the tutorial

Features covered: Mostly app setup and homepage, contact page setup

Libraries/Dependencies used: N/A

I was getting a strange error related to MySQL and mariex (https://github.com/xerions/mariaex) when specifying password in config file. I found out that it’s been fixed in the latest code. So I had to use the latest git dependency and use override true to make it work with mysql —

From mix.exs —

{:mariaex, git: “https://github.com/xerions/mariaex.git", override: true}

Commits for day 1:

Day 2 (Jan 11, 2019)

What’s covered: Chapter 5

Features covered: Mostly fixing the app layout

Libraries/Dependencies used: N/A

Commits for day 2:

Description: Fixed the app layout. Added some missing files from day 1’s commit.

Day 3 (Jan 12, 2019)

What’s covered: Chapter 6

Features covered: Modeling users

Libraries/Dependencies used: N/A

Commits for day 3:

Description: Added Accounts context and User schema with migration file.

User schema at this stage looked like below -

defmodule Minitwitter.Accounts.User do

use Ecto.Schema

import Ecto.Changeset





schema "users" do

field :name, :string

field :email, :string



timestamps()

end



@email_regex ~r/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i



@doc false

def changeset(user, attrs) do

user

|> cast(attrs, [:name, :email])

|> validate_required([:name, :email])

|> validate_length(:name, min: 3, max: 50)

|> validate_length(:email, max: 255)

|> validate_format(:email, @email_regex)

|> downcase_email()

|> unique_constraint(:email)

end



defp downcase_email(changeset) do

case fetch_change(changeset, :email) do

{:ok, email} -> put_change(changeset, :email, String.downcase(email))

:error -> changeset

end

end

end

@email_regex was used to help validate email and downcase_email to convert provided email address to lowercase before persisting users.

Corresponding migration file looks as below —

defmodule Minitwitter.Repo.Migrations.CreateUsers do

use Ecto.Migration



def change do

create table(:users) do

add :name, :string, null: false

add :email, :string, null: false



timestamps()

end



create unique_index(:users, [:email])

end

end

Day 4 (Jan 13, 2019)

What’s covered: Chapter 7, 8, and 9

Features covered: Mostly covered sign-up users, basic login and advanced login with remember user on computer

Libraries/Dependencies used: Had to add additional dependencies in mix.exs to support sign-up and login.

From mix.exs —

{:comeonin, "~> 4.1"},

{:bcrypt_elixir, "~> 1.0"},

{:pbkdf2_elixir, "~> 0.12"}

Commits for day 4:

User schema at his stage looked like —

defmodule Minitwitter.Accounts.User do

use Ecto.Schema

import Ecto.Changeset



schema "users" do

field :name, :string

field :email, :string

field :password, :string, virtual: true

field :password_confirmation, :string, virtual: true

field :password_hash, :string

field :remember_hash, :string



timestamps()

end



@email_regex ~r/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i



@doc false

def changeset(user, attrs) do

user

|> cast(attrs, [:name, :email, :password, :password_confirmation])

|> validate_required([:name, :email, :password, :password_confirmation])

|> validate_length(:name, min: 3, max: 50)

|> validate_length(:email, max: 255)

|> validate_format(:email, @email_regex)

|> validate_length(:password, min: 6, max: 32)

|> validate_confirmation(:password)

|> downcase_email()

|> unique_constraint(:email)

|> put_pass_hash()

end



def update_changeset(user, attrs) do

user

|> cast(attrs, [:name, :email, :password, :password_confirmation, :remember_hash])

|> validate_length(:name, min: 3, max: 50)

|> validate_length(:email, max: 255)

|> validate_format(:email, @email_regex)

|> validate_length(:password, min: 6, max: 32)

|> validate_confirmation(:password)

|> downcase_email()

|> unique_constraint(:email)

|> put_pass_hash()

end



def new_token(size \\ 64) do

:crypto.strong_rand_bytes(size)

|> Base.url_encode64(padding: false)

end



defp put_pass_hash(changeset) do

case changeset do

%Ecto.Changeset{valid?: true, changes: %{password: pass}} ->

put_change(changeset, :password_hash, Comeonin.Pbkdf2.hashpwsalt(pass))



_ ->

changeset

end

end



defp downcase_email(changeset) do

case fetch_change(changeset, :email) do

{:ok, email} -> put_change(changeset, :email, String.downcase(email))

:error -> changeset

end

end

end

Necessary migration files were added too. Also, wrote Auth plug to authenticate users —

defmodule MinitwitterWeb.Auth do

import Plug.Conn

import Phoenix.Controller



alias Minitwitter.Accounts

alias MinitwitterWeb.Router.Helpers, as: Routes



@max_age 7 * 24 * 60 * 60



def init(opts), do: opts



def call(conn, _opts) do

user_id = get_session(conn, :user_id)



cond do

user = conn.assigns[:current_user] ->

put_current_user(conn, user)



user = user_id && Accounts.get_user(user_id) ->

put_current_user(conn, user)



true ->

assign(conn, :current_user, nil)

end

end



def login(conn, user) do

conn

|> put_current_user(user)

|> put_session(:user_id, user.id)

|> configure_session(renew: true)

end



defp put_current_user(conn, user) do

conn

|> assign(:current_user, user)

end



def authenticate_user(conn, _opts) do

cond do

conn.assigns.current_user ->

conn



remember_token = conn.cookies["remember_token"] ->

user_id = conn.cookies["user_id"]

user = Accounts.get_user(user_id)



if user && Accounts.authenticated?(user, remember_token) do

login(conn, user)

else

halt_connection(conn)

end



true ->

halt_connection(conn)

end

end



defp halt_connection(conn) do

conn

|> put_flash(:error, "You must be logged in to access that page!")

|> redirect(to: Routes.page_path(conn, :home))

|> halt()

end



def login_by_email_and_pass(conn, email, given_pass) do

case Accounts.authenticate_by_email_and_pass(email, given_pass) do

{:ok, user} -> {:ok, login(conn, user)}

{:error, :unauthorized} -> {:error, :unauthorized, conn}

{:error, :not_found} -> {:error, :not_found, conn}

end

end



def logout(conn) do

if conn.assigns.current_user do

Accounts.forget_user(conn.assigns.current_user)



conn

|> delete_resp_cookie("user_id")

|> delete_resp_cookie("remember_token")

|> configure_session(drop: true)

end

end



def remember(conn, user, token) do

conn =

conn

|> put_resp_cookie("user_id", Integer.to_string(user.id), max_age: @max_age)

|> put_resp_cookie("remember_token", token, max_age: @max_age)



conn

end

end

Both session and persistent cookies were used to remember necessary tokens. Acccounts context were updated too with necessary functions.

Sign up page

Log in page

Day 5 (Jan 14, 2019)

What’s covered: Chapter 10

Features covered: Updating, showing, and deleting users

Libraries/Dependencies used: Had to add additional dependencies in mix.exs to support pagination of users and fake data generation for DB seeds.

From mix.exs —

{:faker, "~> 0.11"},

{:scrivener_ecto, "~> 2.0"}

Commits for day 5:

For fake data generation below code was used (excerpt from final commit of seeds.exs) —

# Script for populating the database. You can run it as:

#

# mix run priv/repo/seeds.exs

#

# Inside the script, you can read and write to any of your

# repositories directly:

#

# Minitwitter.Repo.insert!(%Minitwitter.SomeSchema{})

#

# We recommend using the bang functions (`insert!`, `update!`

# and so on) as they will fail if something goes wrong.

defmodule MinitwitterWeb.DevelopmentSeeder do

import Ecto.Query, only: [from: 2]



alias Minitwitter.Accounts

alias Minitwitter.Accounts.User

alias Minitwitter.Microposts

alias Minitwitter.Repo



def insert_data do

# users

Repo.insert!(%Accounts.User{

name: "Admin",

email: "demo.rails007@gmail.com",

password: "phoenix",

password_hash: Comeonin.Pbkdf2.hashpwsalt("phoenix"),

admin: true,

activated: true,

activated_at: DateTime.truncate(DateTime.utc_now(), :second)

})



for _ <- 1..50,

do:

Repo.insert!(%Accounts.User{

name: Faker.Name.name(),

email: Faker.Internet.email(),

password: "phoenix",

password_hash: Comeonin.Pbkdf2.hashpwsalt("phoenix"),

activated: true,

activated_at: DateTime.truncate(DateTime.utc_now(), :second)

})



# Microposts

query =

from u in "users",

select: u.id



ids = Repo.all(query)



Enum.each(ids, fn id ->

for _ <- 1..50,

do:

Repo.insert!(%Microposts.Post{

content: Faker.Lorem.sentence(5),

user_id: id

})

end)



# Follwing relationships

users = Repo.all(User)

user = Enum.at(users, 0)

following = Enum.slice(users, 2, 50)

followers = Enum.slice(users, 3, 40)

for followed <- following, do: Minitwitter.Accounts.follow(followed, user)



for follower <- followers, do: Minitwitter.Accounts.follow(user, follower)

end

end



case Mix.env() do

:dev ->

MinitwitterWeb.DevelopmentSeeder.insert_data()



_ ->

:ignore

end

Update profile page

Showing users page

Day 6 (Jan 15, 2019)

What’s covered: Chapter 11

Features covered: Account activation support with email

Libraries/Dependencies used: Had to add additional dependencies in mix.exs to support email sending using smtp.

From mix.exs —

{:bamboo, "~> 1.1"},

{:bamboo_smtp, "~> 1.6.0"}

Commits for day 6:

Configuration for mailer from config.exs —

config :minitwitter, Minitwitter.Mailer,

adapter: Bamboo.SMTPAdapter,

server: "smtp.gmail.com",

port: 587,

username: "demo.rails007",

password: "XXXXXXX",

# can be `:always` or `:never`

tls: :if_available,

# can be `true`

ssl: false,

retries: 3

Email template code goes as below —

<h1>Minitwitter App</h1>

<p>Hi <%= @user.name %>,</p>

<p>

Welcome to the Minitwitter! Click on the link below to activate your account:

</p> <%= Routes.account_activations_url(@conn, :edit, @user.activation_token, email: @user.email) %>

Day 7 (Jan 16, 2019)

What’s covered: Chapter 12

Features covered: Password reset support with email

Libraries/Dependencies used: N/A

Commits for day 7:

Forgot password page

Reset password page

Day 8 & 9 (Jan 18 & 19, 2019)

What’s covered: Chapter 13

Features covered: Microposts support with picture upload

Libraries/Dependencies used: Had to add additional dependencies in mix.exs to support time formatting in microposts and picture upload —

From mix.exs —

{:timex, "~> 3.4"},

{:arc, "~> 0.11.0"},

{:arc_ecto, "~> 0.11.1"}

Commits for day 8 & 9:

Added Post schema for microposts with pictures —

defmodule Minitwitter.Microposts.Post do

use Ecto.Schema

use Arc.Ecto.Schema

import Ecto.Changeset



schema "posts" do

field :content, :string

field :user_id, :id

field :picture, Minitwitter.ImageUploader.Type



timestamps()

end



@doc false

def changeset(post, attrs) do

post

|> cast(attrs, [:content, :user_id])

|> cast_attachments(attrs, [:picture])

|> validate_required([:content, :user_id])

|> validate_length(:content, min: 1, max: 140)

|> foreign_key_constraint(:user_id)

end

end

Also, added necessary migration files.

Microposts with picture and time format

Day 10 (Jan 20, 2019)

What’s covered: Chapter 14

Features covered: Following users

Libraries/Dependencies used: N/A

Commits for day 8 & 9:

Added Relationship schema to support user following —

defmodule Minitwitter.Accounts.Relationship do

use Ecto.Schema

import Ecto.Changeset



schema "relationships" do

belongs_to(:follower, Minitwitter.Accounts.User)

belongs_to(:followed, Minitwitter.Accounts.User)



timestamps()

end



@doc false

def changeset(relationship, attrs) do

relationship

|> cast(attrs, [:follower_id, :followed_id])

|> validate_required([:follower_id, :followed_id])

|> foreign_key_constraint(:follower_id)

|> foreign_key_constraint(:followed_id)

end

end

Updated User schema to support user following —

defmodule Minitwitter.Accounts.User do

use Ecto.Schema

import Ecto.Changeset



schema "users" do

field :name, :string

field :email, :string

field :password, :string, virtual: true

field :password_confirmation, :string, virtual: true

field :password_hash, :string

field :remember_hash, :string

field :admin, :boolean, dafault: false

field :activation_token, :string, virtual: true

field :activation_hash, :string

field :activated, :boolean, default: false

field :activated_at, :utc_datetime

field :reset_hash, :string

field :reset_sent_at, :utc_datetime



has_many(:posts, Minitwitter.Microposts.Post)



has_many(:active_relationships, Minitwitter.Accounts.Relationship, foreign_key: :follower_id)

has_many(:passive_relationships, Minitwitter.Accounts.Relationship, foreign_key: :followed_id)

has_many(:following, through: [:active_relationships, :followed])

has_many(:followers, through: [:passive_relationships, :follower])



timestamps()

end



@email_regex ~r/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i



@doc false

def changeset(user, attrs) do

user

|> cast(attrs, [:name, :email, :password, :password_confirmation])

|> validate_required([:name, :email, :password, :password_confirmation])

|> validate_length(:name, min: 3, max: 50)

|> validate_length(:email, max: 255)

|> validate_format(:email, @email_regex)

|> validate_length(:password, min: 6, max: 32)

|> validate_confirmation(:password)

|> downcase_email()

|> unique_constraint(:email)

|> put_pass_hash()

|> put_activation_hash()

end



def reset_pass_changeset(user, attrs) do

user

|> cast(attrs, [:password, :password_confirmation, :reset_hash])

|> validate_required([:password, :password_confirmation])

|> validate_length(:password, min: 6, max: 32)

|> validate_confirmation(:password)

|> put_pass_hash()

end



def update_changeset(user, attrs) do

user

|> cast(attrs, [

:name,

:email,

:password,

:password_confirmation,

:remember_hash,

:activated,

:activated_at,

:reset_hash,

:reset_sent_at

])

|> validate_required([:name, :email])

|> validate_length(:name, min: 3, max: 50)

|> validate_length(:email, max: 255)

|> validate_format(:email, @email_regex)

|> validate_length(:password, min: 6, max: 32)

|> validate_confirmation(:password)

|> downcase_email()

|> unique_constraint(:email)

|> put_pass_hash()

end



def new_token(size \\ 64) do

:crypto.strong_rand_bytes(size)

|> Base.url_encode64(padding: false)

end



defp put_pass_hash(changeset) do

case changeset do

%Ecto.Changeset{valid?: true, changes: %{password: pass}} ->

put_change(changeset, :password_hash, Comeonin.Pbkdf2.hashpwsalt(pass))



_ ->

changeset

end

end



defp put_activation_hash(changeset) do

case changeset do

%Ecto.Changeset{valid?: true} ->

token = new_token()



changeset

|> put_change(:activation_token, token)

|> put_change(:activation_hash, Comeonin.Pbkdf2.hashpwsalt(token))



_ ->

changeset

end

end



defp downcase_email(changeset) do

case fetch_change(changeset, :email) do

{:ok, email} -> put_change(changeset, :email, String.downcase(email))

:error -> changeset

end

end

end

Following, Followers support

Follow/Unfollow support

This is pretty much the complete application. I suggest you take Michael’s tutorial and read the code day by day basis to get whole idea of the project.