1
0
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:
2018-01-01 18:55:00 -06:00
parent e856cc5172
commit 135ce3a5d6
14 changed files with 172 additions and 38 deletions

7
.exguard.exs Normal file
View 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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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, [

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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"},

View 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