diff --git a/assets/js/app.js b/assets/js/app.js
index b876f92..e245ae5 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -47,20 +47,39 @@ window.liveSocket = liveSocket
const reactRoot = document.querySelector('#react-root')
if (reactRoot) {
- const root = createRoot(reactRoot);
- root.render();
+ const root = createRoot(reactRoot);
+ root.render();
}
document
- .querySelector("#dark-mode-toggle")
- .addEventListener("click", (e) => {
- e.preventDefault()
- const data = document.documentElement.dataset
- if (data["mode"] && data["mode"] == "dark") {
- delete data["mode"]
- window.localStorage.removeItem("theme")
- } else {
- data["mode"] = "dark"
- window.localStorage.setItem("theme", "dark")
- }
- })
+ .querySelector("#dark-mode-toggle")
+ .addEventListener("click", (e) => {
+ e.preventDefault()
+ const data = document.documentElement.dataset
+ if (data["mode"] && data["mode"] == "dark") {
+ delete data["mode"]
+ window.localStorage.removeItem("theme")
+ } else {
+ data["mode"] = "dark"
+ window.localStorage.setItem("theme", "dark")
+ }
+ })
+
+document
+ .querySelectorAll('textarea')
+ .forEach(e => e.addEventListener('keydown', function(e) {
+ if (e.key == 'Tab') {
+ e.preventDefault();
+ var start = this.selectionStart;
+ var end = this.selectionEnd;
+
+ // set textarea value to: text before caret + tab + text after caret
+ this.value = this.value.substring(0, start) +
+ "\t" + this.value.substring(end);
+
+ // put caret at right position again
+ this.selectionStart =
+ this.selectionEnd = start + 1;
+ }
+ }))
+
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/components/layouts/root_public.html.heex b/lib/chiya_web/components/layouts/root_public.html.heex
index b297531..da9361e 100644
--- a/lib/chiya_web/components/layouts/root_public.html.heex
+++ b/lib/chiya_web/components/layouts/root_public.html.heex
@@ -76,6 +76,7 @@
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