mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-08 08:22:48 +00:00
authentication with persistence working
This commit is contained in:
7
.exguard.exs
Normal file
7
.exguard.exs
Normal file
@@ -0,0 +1,7 @@
|
||||
use ExGuard.Config
|
||||
|
||||
guard("dialyzer")
|
||||
|> command("mix dialyzer")
|
||||
|> watch(~r{\.(erl|ex|exs|eex|xrl|yrl)\z}i)
|
||||
|> ignore(~r{deps})
|
||||
|> notification(:off)
|
||||
18
README.md
18
README.md
@@ -1,4 +1,4 @@
|
||||
# MyApp
|
||||
# Classic WoW Forums
|
||||
|
||||
## Generate a new app
|
||||
`mix phx.new myapp --module MyApp --no-brunch --no-html --database postgres`
|
||||
@@ -14,11 +14,11 @@ Ready to run in production? Please [check our deployment guides](http://www.phoe
|
||||
|
||||
# Ecto
|
||||
|
||||
## Create table
|
||||
`mix ecto.gen.migration create_user`
|
||||
`mix ecto.migrate`
|
||||
## Create new database table
|
||||
- `mix ecto.gen.migration create_user`
|
||||
- `mix ecto.migrate`
|
||||
|
||||
## Prod
|
||||
## Production
|
||||
Running app
|
||||
`PORT=80 MIX_ENV=prod mix phx.server`
|
||||
|
||||
@@ -26,7 +26,7 @@ Running app
|
||||
- `MIX_ENV=prod mix ecto.create`
|
||||
- `MIX_ENV=prod mix ecto.migrate`
|
||||
|
||||
## Installing Elixir on C9
|
||||
# Installing Elixir on C9
|
||||
```
|
||||
# for some reason C9 complains this file is missing when it tries to remove couchdb
|
||||
sudo touch /etc/init.d/couchdb
|
||||
@@ -40,3 +40,9 @@ sudo apt-get install esl-erlang
|
||||
sudo apt-get install elixir
|
||||
mix local.hex
|
||||
```
|
||||
|
||||
# Using Dialyzer for type checking
|
||||
- [Setup with Phoenix](https://github.com/jeremyjh/dialyxir/wiki/Phoenix-Dialyxir-Quickstart)
|
||||
- Uses [ExGuard](https://github.com/slashmili/ex_guard) to run every time a file is changed.
|
||||
- Run `mix guard` to start watching files.
|
||||
- Check out `.exguard.exs` for configuration.
|
||||
|
||||
@@ -21,7 +21,6 @@ defmodule MyApp.Guardian do
|
||||
# the resource id so here we'll rely on that to look it up.
|
||||
id = claims["sub"]
|
||||
# resource = MyApp.get_resource_by_id(id)
|
||||
IO.inspect(claims)
|
||||
resource = id
|
||||
{:ok, resource}
|
||||
end
|
||||
|
||||
@@ -4,7 +4,8 @@ defmodule MyApp.BattleNet.Auth do
|
||||
|
||||
def token_uri, do: "https://us.battle.net/oauth/token"
|
||||
|
||||
def get_token(code) do
|
||||
@spec get_access_token(String.t) :: {:ok, String.t} | {:error, String.t}
|
||||
def get_access_token(code) do
|
||||
client_id = Application.get_env(:myapp, :bnet_client_id)
|
||||
client_secret = Application.get_env(:myapp, :bnet_client_secret)
|
||||
redirect_uri = Application.get_env(:myapp, :bnet_redirect_uri)
|
||||
@@ -14,28 +15,16 @@ defmodule MyApp.BattleNet.Auth do
|
||||
HTTPoison.request(:post, token_uri, get_req_body(code), [], req_options)
|
||||
|> parse_body
|
||||
|> parse_token
|
||||
|> validate_user
|
||||
|> generate_jwt
|
||||
end
|
||||
|
||||
defp parse_body({:error, err}), do: {:error, err}
|
||||
defp parse_body({:ok, %HTTPoison.Response{body: body}}), do: Poison.decode(body)
|
||||
|
||||
defp parse_token({:ok, %{"access_token" => token}}), do: {:ok, token}
|
||||
defp parse_token({:ok, %{"error" => err}}), do: {:error, err}
|
||||
defp parse_token({:ok, %{"error" => error}}), do: {:error, error}
|
||||
defp parse_token({:error, err}), do: {:error, "Authentication error"}
|
||||
|
||||
defp validate_user({:error, err}), do: {:error, err}
|
||||
defp validate_user({:ok, token}), do: User.get_user(token)
|
||||
|
||||
defp generate_jwt({:error, err}), do: {:error, err}
|
||||
defp generate_jwt({:ok, user}) do
|
||||
case JWT.get_jwt(user, user) do
|
||||
{:ok, token} -> {:ok, Map.merge(user, %{"token" => token})}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_req_body(String.t) :: tuple
|
||||
defp get_req_body(code) do
|
||||
redirect_uri = Application.get_env(:myapp, :bnet_redirect_uri)
|
||||
{:form, [
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
defmodule MyApp.BattleNet.User do
|
||||
defstruct id: nil, battletag: nil
|
||||
|
||||
@type battle_net_user :: %{"battle_net_id": integer, "battletag": String.t, "access_token": String.t}
|
||||
|
||||
def api_url, do: "https://us.api.battle.net"
|
||||
|
||||
def get_user(access_token) do
|
||||
case HTTPoison.get(resource_url("account/user", access_token)) do
|
||||
{:ok, %HTTPoison.Response{body: body}} -> {:ok, Poison.decode!(body, as: Battlenet.User)}
|
||||
{:error, err} -> {:error, err}
|
||||
# grab user information from battle net api - use token for auth
|
||||
@spec get_user(String.t | {atom, any}) :: {:ok, battle_net_user} | {:error, any}
|
||||
def get_user(access_token) when is_binary(access_token) do
|
||||
HTTPoison.get(resource_url("account/user", access_token))
|
||||
|> parse_user_response(access_token)
|
||||
end
|
||||
def get_user({:ok, access_token}), do: get_user(access_token)
|
||||
def get_user({:error, error}), do: {:error, error}
|
||||
|
||||
defp parse_user_response({:error, error}, _), do: {:error, error}
|
||||
defp parse_user_response({:ok, %HTTPoison.Response{body: body}}, access_token) do
|
||||
case Poison.decode(body) do
|
||||
{:ok, user} ->
|
||||
user = user
|
||||
|> Map.merge(%{"access_token" => access_token}) # add access token to return map
|
||||
|> Map.put("battle_net_id", Map.get(user, "id")) # change id key to battle_net_id
|
||||
|> Map.delete("id") # remove id key
|
||||
{:ok, user}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp resource_url(path, access_token) do
|
||||
"#{api_url}/#{path}?access_token=#{access_token}"
|
||||
"#{api_url()}/#{path}?access_token=#{access_token}"
|
||||
end
|
||||
end
|
||||
|
||||
81
lib/myapp/data/user.ex
Normal file
81
lib/myapp/data/user.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule MyApp.Data.User do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
alias MyApp.Repo
|
||||
alias MyApp.Data
|
||||
|
||||
@derive {Poison.Encoder, except: [:__meta__]}
|
||||
schema "user" do
|
||||
field :battle_net_id, :integer
|
||||
field :battletag, :string
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(user, params \\ %{}) do
|
||||
user
|
||||
|> cast(params, [:battle_net_id, :battletag])
|
||||
|> validate_required([:battle_net_id, :battletag])
|
||||
|> unique_constraint(:battle_net_id)
|
||||
end
|
||||
|
||||
@spec get_user(integer) :: nil | map
|
||||
defp get_user(battle_net_id) do
|
||||
query = from u in "user",
|
||||
where: u.battle_net_id == ^battle_net_id,
|
||||
select: [:id, :battle_net_id, :battletag]
|
||||
Repo.one(query)
|
||||
end
|
||||
|
||||
# insert user info in database - if not exists - update battletag if it has changed
|
||||
@spec upsert_user(%{"battle_net_id": integer, "battletag": String.t} | tuple) :: {:ok, map} | {:error, any}
|
||||
def upsert_user(params) when is_map(params) do
|
||||
# check for current user in database
|
||||
case get_user(Map.get(params, "battle_net_id")) do
|
||||
nil -> insert_user(params)
|
||||
user ->
|
||||
# update user if battletag has changed
|
||||
if Map.get(user, :battletag) != Map.get(params, "battletag") do
|
||||
update_battletag(user, params)
|
||||
else
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|> add_access_token(Map.get(params, "access_token"))
|
||||
end
|
||||
def upsert_user({:ok, params}), do: upsert_user(params)
|
||||
def upsert_user({:error, error}), do: {:error, error}
|
||||
|
||||
# need to add token back to map because we don't store it in the database
|
||||
defp add_access_token({:error, error}, _), do: {:error, error}
|
||||
defp add_access_token({:ok, user}, access_token) do
|
||||
{:ok, Map.merge(user, %{access_token: access_token})}
|
||||
end
|
||||
|
||||
defp insert_user(params) do
|
||||
cs = changeset(%Data.User{}, params)
|
||||
cs
|
||||
|> Repo.insert
|
||||
|> process_insert_or_update
|
||||
end
|
||||
|
||||
# it's possible for a user's battle tag to change - if so update it
|
||||
defp update_battletag(user, params) do
|
||||
cs = Data.User.changeset(Map.merge(%Data.User{}, user), %{battletag: Map.get(params, "battletag")})
|
||||
cs
|
||||
|> Repo.update
|
||||
|> process_insert_or_update
|
||||
end
|
||||
|
||||
defp process_insert_or_update({:error, changeset}), do: {:error, map_changeset(changeset)}
|
||||
defp process_insert_or_update({:ok, user}) do
|
||||
{:ok, Map.take(user, [:id, :battle_net_id, :battletag])} # only grab the fields we need
|
||||
end
|
||||
|
||||
defp map_changeset(changeset) do
|
||||
Enum.map(changeset.errors, fn {key, val} ->
|
||||
%{key => elem(val, 0)}
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -4,11 +4,15 @@ defmodule MyApp.JWT do
|
||||
# ~1 year
|
||||
defp tokenTTL(), do: {52, :weeks}
|
||||
|
||||
def get_jwt(user, claims) do
|
||||
case Guardian.encode_and_sign(user, claims, ttl: tokenTTL()) do
|
||||
{:ok, token, _claims} -> {:ok, token}
|
||||
{:error, _token, _claims} -> {:error, "JWT error"}
|
||||
@spec add_jwt(map | {atom, any}) :: {:ok, map} | {:error, String.t}
|
||||
def add_jwt(user) when is_map(user) do
|
||||
case Guardian.encode_and_sign(user, user, ttl: tokenTTL()) do
|
||||
{:ok, token, _claims} -> {:ok, Map.merge(user, %{token: token})}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def add_jwt({:ok, user}), do: add_jwt(user)
|
||||
def add_jwt({:error, error}), do: {:error, error}
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
defmodule MyApp.Repo do
|
||||
use Ecto.Repo, otp_app: :myapp
|
||||
@dialyzer {:nowarn_function, rollback: 1}
|
||||
|
||||
@doc """
|
||||
Dynamically loads the repository url from the
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
defmodule MyAppWeb.BattleNetController do
|
||||
use MyAppWeb, :controller
|
||||
alias MyAppWeb.Response
|
||||
alias MyApp.BattleNet.Auth
|
||||
alias MyApp.BattleNet
|
||||
alias MyApp.Data
|
||||
alias MyApp.JWT
|
||||
|
||||
# https://us.battle.net/oauth/authorize?redirect_uri=https://localhost/api/battlenet/authorize&scope=wow.profile&client_id=vxqv32fddxsy6cmk6259amtymbuzmfrq&response_type=code
|
||||
|
||||
@spec authorize(map, map) :: any
|
||||
def authorize(conn, %{"code" => code}) when not is_nil(code) do
|
||||
|
||||
{output, status} = case Auth.get_token(code) do
|
||||
{:ok, token} -> {token, 200}
|
||||
{:error, err} -> {err, 400}
|
||||
end
|
||||
{output, status} = code
|
||||
|> BattleNet.Auth.get_access_token
|
||||
|> BattleNet.User.get_user
|
||||
|> Data.User.upsert_user
|
||||
|> JWT.add_jwt
|
||||
|> Response.put_resp
|
||||
|
||||
conn
|
||||
|>put_status(status)
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule MyAppWeb.UserController do
|
||||
use MyAppWeb, :controller
|
||||
alias MyAppWeb.Response
|
||||
|
||||
@spec index(map, map) :: any
|
||||
def index(conn, params) do
|
||||
IO.inspect(conn)
|
||||
IO.inspect(params)
|
||||
|
||||
@@ -19,4 +19,9 @@ defmodule MyAppWeb.Response do
|
||||
|
||||
Phoenix.Controller.json(conn, output)
|
||||
end
|
||||
|
||||
# generatic function for converting data to response code
|
||||
@spec put_resp({:ok, any} | {:error, any}) :: {any, integer}
|
||||
def put_resp({:ok, data}), do: {data, 200}
|
||||
def put_resp({:error, error}), do: {error, 400}
|
||||
end
|
||||
|
||||
5
mix.exs
5
mix.exs
@@ -10,7 +10,8 @@ defmodule MyApp.Mixfile do
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers,
|
||||
start_permanent: Mix.env == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
deps: deps(),
|
||||
dialyzer: [plt_add_deps: :transitive]
|
||||
]
|
||||
end
|
||||
|
||||
@@ -43,6 +44,8 @@ defmodule MyApp.Mixfile do
|
||||
{:argon2_elixir, "~> 1.2"},
|
||||
{:guardian, "~> 1.0"},
|
||||
{:httpoison, "~> 0.13"},
|
||||
{:dialyxir, "~> 0.5", only: [:dev], runtime: false},
|
||||
{:ex_guard, "~> 1.3", only: :dev},
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
3
mix.lock
3
mix.lock
@@ -7,8 +7,11 @@
|
||||
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], [], "hexpm"},
|
||||
"db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [], [], "hexpm"},
|
||||
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "2.2.7", "2074106ff4a5cd9cb2b54b12ca087c4b659ddb3f6b50be4562883c1d763fb031", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"},
|
||||
"ex_guard": {:hex, :ex_guard, "1.3.0", "0f5c50b90a7e4c599b45d02448ae53eabffc33adb7bfdfc5f5507715e7662a25", [], [{:fs, "~> 0.9", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], [], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [], [], "hexpm"},
|
||||
"guardian": {:hex, :guardian, "1.0.0", "21bae2a8c0b4ed5943d9da0c6aeb16e52874c1f675de5d7920ae35471c6263f9", [], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
||||
14
priv/repo/migrations/20180101200459_create_user.exs
Normal file
14
priv/repo/migrations/20180101200459_create_user.exs
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule MyApp.Repo.Migrations.CreateUser do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:user) do
|
||||
add :battle_net_id, :integer
|
||||
add :battletag, :string
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:user, [:battle_net_id])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user