diff --git a/lib/chiya/accounts.ex b/lib/chiya/accounts.ex index 88959e1..5d20f3c 100644 --- a/lib/chiya/accounts.ex +++ b/lib/chiya/accounts.ex @@ -8,6 +8,10 @@ defmodule Chiya.Accounts do alias Chiya.Accounts.{User, UserToken, UserNotifier} + defp preload_user(user) do + Repo.preload(user, [:tokens]) + end + def has_user?() do Repo.exists?(User) end @@ -240,6 +244,21 @@ defmodule Chiya.Accounts do Repo.update(changeset) end + ## App Tokens + + @doc """ + Generates a application token. + """ + def generate_app_token(user, app_name, context) do + attrs = UserToken.build_app_token(user, app_name, context) + changeset = UserToken.app_token_changeset(%UserToken{}, attrs) + Repo.insert(changeset) + end + + def delete_app_token(id) do + Repo.delete(Repo.get(UserToken, id)) + end + ## Session @doc """ @@ -256,7 +275,7 @@ defmodule Chiya.Accounts do """ def get_user_by_session_token(token) do {:ok, query} = UserToken.verify_session_token_query(token) - Repo.one(query) + Repo.one(query) |> preload_user() end @doc """ diff --git a/lib/chiya/accounts/user.ex b/lib/chiya/accounts/user.ex index ab49440..f25b8d3 100644 --- a/lib/chiya/accounts/user.ex +++ b/lib/chiya/accounts/user.ex @@ -11,6 +11,8 @@ defmodule Chiya.Accounts.User do field :user_image, ChiyaWeb.Uploaders.UserImage.Type + has_many :tokens, Chiya.Accounts.UserToken + timestamps() end diff --git a/lib/chiya/accounts/user_token.ex b/lib/chiya/accounts/user_token.ex index 0babba8..8018302 100644 --- a/lib/chiya/accounts/user_token.ex +++ b/lib/chiya/accounts/user_token.ex @@ -1,6 +1,7 @@ defmodule Chiya.Accounts.UserToken do use Ecto.Schema import Ecto.Query + import Ecto.Changeset alias Chiya.Accounts.UserToken @hash_algorithm :sha256 @@ -22,6 +23,23 @@ defmodule Chiya.Accounts.UserToken do timestamps(updated_at: false) end + def build_app_token(user, app_name, context) do + token = :crypto.strong_rand_bytes(@rand_size) + + %{ + token: token, + context: context, + user_id: user.id, + sent_to: app_name + } + end + + def app_token_changeset(token, attrs) do + token + |> cast(attrs, [:context, :token, :sent_to, :user_id]) + |> validate_required([:context, :token, :sent_to, :user_id]) + end + @doc """ Generates a token that will be stored in a signed place, such as session or cookie. As they are signed, those diff --git a/lib/chiya_web/components/layouts/app.html.heex b/lib/chiya_web/components/layouts/app.html.heex index 60d32fe..352e7c6 100644 --- a/lib/chiya_web/components/layouts/app.html.heex +++ b/lib/chiya_web/components/layouts/app.html.heex @@ -21,6 +21,11 @@ icon: "hero-user-solid", name: "Identities" }, + %{ + path: ~p"/admin/tokens", + icon: "hero-key-solid", + name: "Tokens" + }, %{ path: ~p"/admin/settings", icon: "hero-wrench-screwdriver-solid", diff --git a/lib/chiya_web/controllers/token_controller.ex b/lib/chiya_web/controllers/token_controller.ex new file mode 100644 index 0000000..75152a5 --- /dev/null +++ b/lib/chiya_web/controllers/token_controller.ex @@ -0,0 +1,47 @@ +defmodule ChiyaWeb.TokenController do + use ChiyaWeb, :controller + + alias Chiya.Accounts.UserToken + + def index(conn, _params) do + tokens = conn.assigns.current_user.tokens + render(conn, :index, tokens: tokens) + end + + def show(conn, %{"id" => id}) do + token_id = String.to_integer(id) + tokens = conn.assigns.current_user.tokens + token = Enum.find(tokens, fn t -> t.id == token_id end) + render(conn, :show, token: token) + end + + def new(conn, _params) do + changeset = + UserToken.app_token_changeset(%UserToken{}, %{ + user_id: conn.assigns.current_user.id, + context: "app" + }) + + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"user_token" => %{"sent_to" => app_name, "context" => context}}) do + case Chiya.Accounts.generate_app_token(conn.assigns.current_user, app_name, context) do + {:ok, token} -> + conn + |> put_flash(:info, "token created successfully.") + |> redirect(to: ~p"/admin/tokens/#{token}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + {:ok, _token} = Chiya.Accounts.delete_app_token(id) + + conn + |> put_flash(:info, "Token deleted successfully.") + |> redirect(to: ~p"/admin/tokens") + end +end diff --git a/lib/chiya_web/controllers/token_html.ex b/lib/chiya_web/controllers/token_html.ex new file mode 100644 index 0000000..f23bca3 --- /dev/null +++ b/lib/chiya_web/controllers/token_html.ex @@ -0,0 +1,13 @@ +defmodule ChiyaWeb.TokenHTML do + use ChiyaWeb, :html + + embed_templates "token_html/*" + + @doc """ + Renders a token form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def token_form(assigns) +end diff --git a/lib/chiya_web/controllers/token_html/edit.html.heex b/lib/chiya_web/controllers/token_html/edit.html.heex new file mode 100644 index 0000000..96831c8 --- /dev/null +++ b/lib/chiya_web/controllers/token_html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit Channel <%= @channel.id %> + <:subtitle>Use this form to manage channel records in your database. + + +<.token_form changeset={@changeset} action={~p"/admin/channels/#{@channel}"} /> + +<.back navigate={~p"/admin/channels"}>Back to channels diff --git a/lib/chiya_web/controllers/token_html/index.html.heex b/lib/chiya_web/controllers/token_html/index.html.heex new file mode 100644 index 0000000..86a2523 --- /dev/null +++ b/lib/chiya_web/controllers/token_html/index.html.heex @@ -0,0 +1,21 @@ +<.header> + <.icon name="hero-key" /> Tokens + <:subtitle>Tokens are like keys. + <:actions> + <.link href={~p"/admin/tokens/new"}> + <.button>New Token + + + + +<.table id="tokens" rows={@tokens} row_click={&JS.navigate(~p"/admin/tokens/#{&1}")}> + <:col :let={token} label="Id"><%= token.id %> + <:col :let={token} label="Context"><%= token.context %> + <:col :let={token} label="App Name"><%= token.sent_to %> + <:col :let={token} label="Created"><%= pretty_date(token.inserted_at) %> + <:action :let={token}> + <.link href={~p"/admin/tokens/#{token}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/lib/chiya_web/controllers/token_html/new.html.heex b/lib/chiya_web/controllers/token_html/new.html.heex new file mode 100644 index 0000000..76264f4 --- /dev/null +++ b/lib/chiya_web/controllers/token_html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New Token + <:subtitle>Use this form to create new token records in your database. + + +<.token_form changeset={@changeset} action={~p"/admin/tokens"} /> + +<.back navigate={~p"/admin/tokens"}>Back to channels diff --git a/lib/chiya_web/controllers/token_html/show.html.heex b/lib/chiya_web/controllers/token_html/show.html.heex new file mode 100644 index 0000000..94caffc --- /dev/null +++ b/lib/chiya_web/controllers/token_html/show.html.heex @@ -0,0 +1,17 @@ +<.header> + Token <%= @token.id %> + <:subtitle>This is a token. + <:actions> + + +<.list> + <:item title="Id"><%= @token.id %> + <:item title="App Name"><%= @token.sent_to %> + <:item title="Context"><%= @token.context %> + <:item title="Created"><%= @token.inserted_at %> + <:item title="Value"> +

<%= :crypto.bytes_to_integer(@token.token) %>

+ + + +<.back navigate={~p"/admin/tokens"}>Back to channels diff --git a/lib/chiya_web/controllers/token_html/token_form.html.heex b/lib/chiya_web/controllers/token_html/token_form.html.heex new file mode 100644 index 0000000..db6d234 --- /dev/null +++ b/lib/chiya_web/controllers/token_html/token_form.html.heex @@ -0,0 +1,11 @@ +<.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:context]} type="text" label="Context" /> + <.input field={f[:sent_to]} type="text" label="Application" /> + <.input field={f[:user_id]} type="hidden" /> + <:actions> + <.button>Save Token + + diff --git a/lib/chiya_web/router.ex b/lib/chiya_web/router.ex index 3db8c3e..5ba03f9 100644 --- a/lib/chiya_web/router.ex +++ b/lib/chiya_web/router.ex @@ -73,6 +73,7 @@ defmodule ChiyaWeb.Router do resources "/settings", SettingController, singleton: true resources "/identities", IdentityController resources "/comments", CommentController, only: [:index, :show] + resources "/tokens", TokenController, only: [:index, :show, :new, :create, :delete] get "/notes/import", NoteController, :import_prepare post "/notes/import", NoteController, :import_run