Web Client #11

Merged
Simponic merged 19 commits from web into main 2023-01-19 16:04:10 -05:00
12 changed files with 335 additions and 12 deletions
Showing only changes of commit d71138900c - Show all commits

View File

@ -18,4 +18,11 @@ config :chessh, RateLimits,
player_session_message_burst_ms: 500,
player_session_message_burst_rate: 8
config :chessh, Web,
port: 8080,
github_oauth_login_url: "https://github.com/login/oauth/access_token",
github_user_api_url: "https://api.github.com/user"
config :joken, default_signer: "secret"
import_config "#{config_env()}.exs"

23
config/runtime.exs Normal file
View File

@ -0,0 +1,23 @@
import Config
config :chessh, Web,
github_client_id: System.get_env("GITHUB_CLIENT_ID"),
github_client_secret: System.get_env("GITHUB_CLIENT_SECRET"),
github_user_agent: System.get_env("GITHUB_USER_AGENT")
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []
config :chessh, Chessh.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
end

View File

@ -1,5 +0,0 @@
defmodule Chessh do
def hello() do
:world
end
end

View File

@ -15,7 +15,16 @@ defmodule Chessh.Application do
end
def start(_, _) do
children = [Chessh.Repo, Chessh.SSH.Daemon]
children = [
Chessh.Repo,
Chessh.SSH.Daemon,
Plug.Cowboy.child_spec(
scheme: :http,
plug: Chessh.Web.Endpoint,
options: [port: Application.get_env(:chessh, Web)[:port]]
)
]
opts = [strategy: :one_for_one, name: Chessh.Supervisor]
with {:ok, pid} <- Supervisor.start_link(children, opts) do

View File

@ -11,13 +11,20 @@ defmodule Chessh.Key do
timestamps()
end
defimpl Jason.Encoder, for: Chessh.Key do
def encode(value, opts) do
Jason.Encode.map(Map.take(value, [:id, :key, :name]), opts)
end
end
def changeset(key, attrs) do
key
|> cast(update_encode_key(attrs, :key), [:key])
|> cast(update_encode_key(attrs, :key), [:key, :player_id])
|> cast(attrs, [:name])
|> validate_required([:key, :name])
|> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid public ssh key")
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
|> unique_constraint([:player_id, :key], message: "Player already has that key")
end
def encode_key(key) do

View File

@ -5,6 +5,8 @@ defmodule Chessh.Player do
@derive {Inspect, except: [:password]}
schema "players" do
field(:github_id, :integer)
field(:username, :string)
field(:password, :string, virtual: true)
@ -26,7 +28,7 @@ defmodule Chessh.Player do
def registration_changeset(player, attrs, opts \\ []) do
player
|> cast(attrs, [:username, :password])
|> cast(attrs, [:username, :password, :github_id])
|> validate_username()
|> validate_password(opts)
end

3
lib/chessh/web/token.ex Normal file
View File

@ -0,0 +1,3 @@
defmodule Chessh.Web.Token do
use Joken.Config
end

262
lib/chessh/web/web.ex Normal file
View File

@ -0,0 +1,262 @@
defmodule Chessh.Web.Endpoint do
alias Chessh.{Player, Repo, Key}
alias Chessh.Web.Token
use Plug.Router
require Logger
plug(Plug.Logger)
plug(:match)
plug(Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Jason
)
plug(:dispatch)
get "/oauth/redirect" do
[github_login_url, client_id, client_secret, github_user_api_url, github_user_agent] =
get_github_configs()
resp =
case conn.params do
%{"code" => req_token} ->
case :httpc.request(
:post,
{String.to_charlist(
"#{github_login_url}?client_id=#{client_id}&client_secret=#{client_secret}&code=#{req_token}"
), [], 'application/json', ''},
[],
[]
) do
{:ok, {{_, 200, 'OK'}, _, resp}} ->
URI.decode_query(String.Chars.to_string(resp))
end
end
{status, body} =
case resp do
%{"access_token" => access_token} ->
case :httpc.request(
:get,
{String.to_charlist(github_user_api_url),
[
{'Authorization', String.to_charlist("Bearer #{access_token}")},
{'User-Agent', github_user_agent}
]},
[],
[]
) do
{:ok, {{_, 200, 'OK'}, _, user_details}} ->
%{"login" => username, "id" => github_id} =
Jason.decode!(String.Chars.to_string(user_details))
%Player{id: id} =
Repo.insert!(%Player{github_id: github_id, username: username},
on_conflict: [set: [github_id: github_id]],
conflict_target: :github_id
)
{200,
%{
success: true,
jwt:
Token.generate_and_sign!(%{
"uid" => id
})
}}
_ ->
{400, %{errors: "Access token was incorrect. Try again."}}
end
_ ->
{400, %{errors: "Failed to retrieve token from GitHub. Try again."}}
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
put "/player/password" do
jwt =
Enum.find_value(conn.req_headers, fn {header, value} ->
if header === "authorization", do: value
end)
{:ok, %{"uid" => uid}} = Token.verify_and_validate(jwt)
player = Repo.get(Player, uid)
{status, body} =
case conn.body_params do
%{"password" => password, "password_confirmation" => password_confirmation} ->
case Player.password_changeset(player, %{
password: password,
password_confirmation: password_confirmation
})
|> Repo.update() do
{:ok, player} ->
{200, %{success: true, id: player.id}}
{:error, %{valid?: false} = changeset} ->
{400, %{errors: format_errors(changeset)}}
end
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
post "/player/login" do
{status, body} =
case conn.body_params do
%{"username" => username, "password" => password} ->
player = Repo.get_by(Player, username: username)
case Player.valid_password?(player, password) do
true ->
{
200,
%{
token:
Token.generate_and_sign!(%{
"uid" => player.id
})
}
}
_ ->
{
400,
%{
errors: "Invalid credentials"
}
}
end
_ ->
{
400,
%{errors: "Username and password must be defined"}
}
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
post "/player/keys" do
jwt =
Enum.find_value(conn.req_headers, fn {header, value} ->
if header === "authorization", do: value
end)
{:ok, %{"uid" => uid}} = Token.verify_and_validate(jwt)
{status, body} =
case conn.body_params do
%{"key" => key, "name" => name} ->
case Key.changeset(%Key{}, %{player_id: uid, key: key, name: name}) |> Repo.insert() do
{:ok, _new_key} ->
{
200,
%{
success: true
}
}
{:error, %{valid?: false} = changeset} ->
{
400,
%{
errors: format_errors(changeset)
}
}
end
_ ->
{
400,
%{errors: "Must define key and name"}
}
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
get "/player/:id/keys" do
%{"id" => player_id} = conn.path_params
keys = (Repo.get(Player, player_id) |> Repo.preload([:keys])).keys
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{keys: keys}))
end
delete "/keys/:id" do
%{"id" => key_id} = conn.path_params
jwt =
Enum.find_value(conn.req_headers, fn {header, value} ->
if header === "authorization", do: value
end)
{:ok, %{"uid" => uid}} = Token.verify_and_validate(jwt)
key = Repo.get(Key, key_id)
{status, body} =
if key && uid == key.player_id do
case Repo.delete(key) do
{:ok, _} ->
{200, %{success: true}}
{:error, changeset} ->
{400, %{errors: format_errors(changeset)}}
end
else
if !key do
{404, %{errors: "Key not found"}}
else
{401, %{errors: "You cannot delete that key"}}
end
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(body))
end
match _ do
send_resp(conn, 404, "Route undefined")
end
defp format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
defp get_github_configs() do
Enum.map(
[
:github_oauth_login_url,
:github_client_id,
:github_client_secret,
:github_user_api_url,
:github_user_agent
],
fn key -> Application.get_env(:chessh, Web)[key] end
)
end
end

View File

@ -17,7 +17,7 @@ defmodule Chessh.MixProject do
def application do
[
mod: {Chessh.Application, []},
extra_applications: [:logger, :crypto, :syn, :ssh]
extra_applications: [:logger, :crypto, :syn, :ssh, :plug_cowboy, :inets, :ssl]
]
end
@ -34,7 +34,9 @@ defmodule Chessh.MixProject do
{:bcrypt_elixir, "~> 3.0"},
{:hammer, "~> 6.1"},
{:syn, "~> 3.3"},
{:jason, "~> 1.3"}
{:jason, "~> 1.3"},
{:plug_cowboy, "~> 2.2"},
{:joken, "~> 2.5"}
]
end

View File

@ -4,6 +4,9 @@
"chess": {:hex, :chess, "0.4.1", "34c04abed2db81e0c56476c8e74fd85ef4e1bae23a4cd528e0ce8a052ada976f", [:mix], [], "hexpm", "692e0def99dc25af4af2413839a4605a2a0a713c2646f9afcf3a47c76a6de43d"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
@ -13,9 +16,17 @@
"esshd": {:hex, :esshd, "0.2.1", "cded6a329c32bc3b3c15828bcd34203227bbef310db3c39a6f3c55cf5b29cd34", [:mix], [], "hexpm", "b058b56af53aba1c23522d72a3c39ab7f302e509af1c0ba1a748f00d93053c4d"},
"hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"},
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"syn": {:hex, :syn, "3.3.0", "4684a909efdfea35ce75a9662fc523e4a8a4e8169a3df275e4de4fa63f99c486", [:rebar3], [], "hexpm", "e58ee447bc1094bdd21bf0acc102b1fbf99541a508cd48060bf783c245eaf7d6"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"ueberauth": {:hex, :ueberauth, "0.10.3", "4a3bd7ab7b5d93d301d264f0f6858392654ee92171f4437d067d1ae227c051d9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1394f36a6c64e97f2038cf95228e7e52b4cb75417962e30418fbe9902b30e6d3"},
"uef": {:hex, :uef, "2.6.0", "0aa813d125c429e48c1d5abbab89837d8b7bd0499e6ee2f7dc9cc4287a475cfd", [:rebar3], [], "hexpm", "1585cba305dc7c0a3f75aeab15937b324184673d2922148b432edf9beba73d62"},
}

View File

@ -5,11 +5,13 @@ defmodule Chessh.Repo.Migrations.CreatePlayer do
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
create table(:players) do
add(:github_id, :integer, null: false)
add(:username, :citext, null: false)
add(:hashed_password, :string, null: false)
add(:hashed_password, :string, null: true)
timestamps()
end
create(unique_index(:players, [:username]))
create(unique_index(:players, [:github_id]))
end
end

View File

@ -6,7 +6,7 @@ defmodule Chessh.Repo.Migrations.AddKeys do
add(:key, :text, null: false)
add(:name, :string, null: false)
add(:player_id, references(:players))
add(:player_id, references(:players), null: false)
timestamps()
end