diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c6b553b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+TELEGRAM_BOT_TOKEN=
+TELEGRAM_BOT_USERNAME=
+DEV_USER_ID=
diff --git a/.gitignore b/.gitignore
index b9cd48a..0a74500 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,4 @@ devenv.local.yaml
# pre-commit
.pre-commit-config.yaml
Session.vim
+.env
diff --git a/assets/js/app.js b/assets/js/app.js
index 8894288..8d0b8b8 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -29,6 +29,7 @@ import MilkdownEditor from "./hooks/milkdown.js";
import { LayoutStickyToolbar } from "./hooks/layout-sticky-toolbar.js";
import Wikimap from "./hooks/wikimap.js";
import Wikitree from "./hooks/wikitree.js";
+import { TelegramLoginHook } from "./hooks/telegram_login.js"
const csrfToken = document
.querySelector("meta[name='csrf-token']")
@@ -43,6 +44,7 @@ const liveSocket = new LiveSocket("/live", Socket, {
LayoutStickyToolbar,
Wikimap,
Wikitree,
+ TelegramLogin: TelegramLoginHook,
},
});
diff --git a/assets/js/hooks/telegram_login.js b/assets/js/hooks/telegram_login.js
new file mode 100644
index 0000000..c27149b
--- /dev/null
+++ b/assets/js/hooks/telegram_login.js
@@ -0,0 +1,24 @@
+export const TelegramLoginHook = {
+ mounted() {
+ if (this.el.dataset.inited === "1") return
+ this.el.dataset.inited = "1"
+
+ const bot = this.el.dataset.botUsername
+ const req = this.el.dataset.requestAccess || "write"
+ const size = this.el.dataset.size || "large"
+
+ const { pathname, search } = window.location
+ const returnTo = `${pathname}${search || ""}` // <- path-only
+ const authUrl = `/auth/telegram/callback?return_to=${encodeURIComponent(returnTo)}`
+
+ const s = document.createElement("script")
+ s.async = true
+ s.src = "https://telegram.org/js/telegram-widget.js?22"
+ s.dataset.telegramLogin = bot
+ s.dataset.authUrl = authUrl
+ s.dataset.requestAccess = req
+ s.dataset.size = size
+
+ this.el.appendChild(s)
+ },
+}
diff --git a/lib/wik/accounts/user.ex b/lib/wik/accounts/user.ex
index 698c962..084c9e3 100644
--- a/lib/wik/accounts/user.ex
+++ b/lib/wik/accounts/user.ex
@@ -8,8 +8,7 @@ defmodule Wik.Accounts.User do
defimpl String.Chars do
def to_string(user) do
- [username, _] = Kernel.to_string(user.email) |> String.split("@", parts: 2)
- username
+ Kernel.to_string(user.tg_username)
end
end
@@ -62,7 +61,7 @@ defmodule Wik.Accounts.User do
create :create do
primary? true
- accept [:email]
+ accept [:email, :tg_id, :tg_first_name, :tg_last_name, :tg_username, :tg_photo_url]
end
create :sign_in_with_magic_link do
@@ -87,7 +86,7 @@ defmodule Wik.Accounts.User do
action :request_magic_link do
argument :email, :ci_string do
- allow_nil? false
+ allow_nil? true
end
run AshAuthentication.Strategy.MagicLink.Request
@@ -111,13 +110,49 @@ defmodule Wik.Accounts.User do
)
)
end
+
+ policy action_type(:create) do
+ authorize_if always()
+ end
end
attributes do
uuid_primary_key :id
attribute :email, :ci_string do
- allow_nil? false
+ allow_nil? true
+ public? true
+ end
+
+ attribute :confirmed_at, :utc_datetime_usec
+
+ attribute :role, :atom do
+ public? true
+ constraints one_of: [:user, :admin, :moderator]
+ default :user
+ end
+
+ attribute :tz, :string do
+ public? true
+ end
+
+ attribute :tg_id, :string do
+ public? true
+ end
+
+ attribute :tg_first_name, :string do
+ public? true
+ end
+
+ attribute :tg_last_name, :string do
+ public? true
+ end
+
+ attribute :tg_username, :string do
+ public? true
+ end
+
+ attribute :tg_photo_url, :string do
public? true
end
end
@@ -136,5 +171,6 @@ defmodule Wik.Accounts.User do
identities do
identity :unique_email, [:email]
+ identity :unique_tg_id, [:tg_id]
end
end
diff --git a/lib/wik_web/components/telegram/widgets.ex b/lib/wik_web/components/telegram/widgets.ex
new file mode 100644
index 0000000..3805608
--- /dev/null
+++ b/lib/wik_web/components/telegram/widgets.ex
@@ -0,0 +1,30 @@
+defmodule WikWeb.Components.Telegram.Widgets do
+ use Phoenix.Component
+
+ # Optional attrs if you want to override defaults
+ attr :class, :string, default: nil
+ attr :request_access, :string, default: "write"
+ attr :size, :string, default: "large", values: ~w(small medium large)
+
+ def login(assigns) do
+ bot_username =
+ System.get_env("TELEGRAM_BOT_USERNAME") ||
+ raise "TELEGRAM_BOT_USERNAME not set"
+
+ assigns =
+ assigns
+ |> assign(:bot_username, bot_username)
+
+ ~H"""
+
+ """
+ end
+end
diff --git a/lib/wik_web/controllers/auth_controller/dev.ex b/lib/wik_web/controllers/auth_controller/dev.ex
new file mode 100644
index 0000000..b0a6335
--- /dev/null
+++ b/lib/wik_web/controllers/auth_controller/dev.ex
@@ -0,0 +1,36 @@
+defmodule WikWeb.AuthController.Dev do
+ use WikWeb, :controller
+ require Ash.Query
+ require Logger
+ alias AshAuthentication.Jwt
+ alias AshAuthentication.Plug.Helpers, as: AuthHelpers
+
+ def login(conn, _params) do
+ return_to = "/"
+
+ id = System.get_env("DEV_USER_ID", "000000000")
+
+ img =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcBAMAAACAI8KnAAABg2lDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSKVDlYQcQhSneyioo61CkWoEGqFVh1Mrp/QpCFJcXEUXAsOfixWHVycdXVwFQTBDxB3wUnRRUr8X1JoEePBcT/e3XvcvQOERoWpZlcMUDXLSCXiYia7KgZeISCEAYxgRmamPidJSXiOr3v4+HoX5Vne5/4cfbm8yQCfSBxjumERbxBPb1o6533iMCvJOeJz4nGDLkj8yHXF5TfORYcFnhk20ql54jCxWOxgpYNZyVCJp4gjOVWjfCHjco7zFme1UmOte/IXBvPayjLXaQ4jgUUsQYIIBTWUUYGFKK0aKSZStB/38A85folcCrnKYORYQBUqZMcP/ge/uzULkxNuUjAOdL/Y9scoENgFmnXb/j627eYJ4H8GrrS2v9oAZj9Jr7e1yBEQ2gYurtuasgdc7gCDT7psyI7kpykUCsD7GX1TFui/BXrX3N5a+zh9ANLUVfIGODgExoqUve7x7p7O3v490+rvB903ctGnToVWAAAAIVBMVEVKHBxKHD5vKl68TqDKcrS8oE5OvLyFvE6eynK62Zv36fNZSEXTAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+kCDxUqHL67UiAAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAZklEQVQY02OYiQDT0mYy4OROSw1NQ+e6IIBbmgsBrrGxCULAGJ3LYGzitWoVkOm1aolLBzbuQkEg11GKMLcDL7cc2d5yIrjOxiBg4l6OyS0vBjKVjJWUjc2xcSECxiAeMVwEQOMCAM+GiLYSTcf8AAAAAElFTkSuQmCC"
+
+ attrs = %{
+ tg_id: id,
+ tg_first_name: "Testuser",
+ tg_last_name: "Testuser",
+ tg_username: "Testuser",
+ tg_photo_url: img
+ }
+
+ db_user =
+ Wik.Accounts.User
+ |> Ash.Changeset.for_create(:create, attrs)
+ |> Ash.create!(upsert?: true, upsert_identity: :unique_tg_id)
+
+ {:ok, token, _claims} = Jwt.token_for_user(db_user)
+ db_user = Ash.Resource.set_metadata(db_user, %{token: token})
+
+ conn
+ |> AuthHelpers.store_in_session(db_user)
+ |> redirect(to: return_to)
+ end
+end
diff --git a/lib/wik_web/controllers/auth_controller/telegram.ex b/lib/wik_web/controllers/auth_controller/telegram.ex
new file mode 100644
index 0000000..0ea9459
--- /dev/null
+++ b/lib/wik_web/controllers/auth_controller/telegram.ex
@@ -0,0 +1,97 @@
+defmodule WikWeb.AuthController.Telegram do
+ use WikWeb, :controller
+ require Ash.Query
+ require Logger
+ alias AshAuthentication.Jwt
+ alias AshAuthentication.Plug.Helpers, as: AuthHelpers
+ alias Assent.Strategy.Telegram
+
+ defp config do
+ [
+ bot_token: System.get_env("TELEGRAM_BOT_TOKEN"),
+ # `, may be one of `:login_widget` or `:web_mini_app`
+ authorization_channel: :login_widget,
+ origin: ""
+ # return_to: "/"
+ # client_id: "REPLACE_WITH_CLIENT_ID",
+ # client_secret: "REPLACE_WITH_CLIENT_SECRET",
+ # redirect_uri: "http://localhost:4000/auth/telegram /callback"
+ ]
+ end
+
+ def request(conn) do
+ config()
+ |> Telegram.authorize_url()
+ |> case do
+ {:ok, %{url: url, session_params: session_params}} ->
+ conn = put_session(conn, :session_params, session_params)
+
+ # Redirect end-user to Telegram to authorize access to their account
+ conn
+ |> put_resp_header("location", url)
+ |> send_resp(302, "")
+
+ {:error, error} ->
+ Logger.error("""
+ Error in AuthController.Telegram.authorize_url()
+ #{inspect(error)}
+ """)
+
+ nil
+ end
+ end
+
+ def callback(conn, params) do
+ return_to = params["return_to"]
+ params = params |> Map.delete("return_to")
+ session_params = get_session(conn, :session_params)
+
+ config()
+ # Session params should be added to the config so the strategy can use them
+ |> Keyword.put(:session_params, session_params)
+ |> Telegram.callback(params)
+ |> case do
+ {:ok, %{user: user_from_telegram}} ->
+ attrs = %{
+ tg_id: user_from_telegram["sub"] |> to_string(),
+ tg_first_name: user_from_telegram["given_name"],
+ tg_last_name: user_from_telegram["family_name"],
+ tg_username: user_from_telegram["preferred_username"],
+ tg_photo_url: user_from_telegram["picture"]
+ }
+
+ db_user =
+ Wik.Accounts.User
+ |> Ash.Changeset.for_create(:create, attrs)
+ |> Ash.create!(upsert?: true, upsert_identity: :unique_tg_id)
+
+ {:ok, token, _claims} = Jwt.token_for_user(db_user)
+ db_user = Ash.Resource.set_metadata(db_user, %{token: token})
+
+ # current_user = %{
+ # id: db_user.id,
+ # first_name: db_user.telegram_first_name,
+ # last_name: db_user.telegram_last_name,
+ # username: db_user.telegram_username,
+ # photo_url: db_user.telegram_photo_url,
+ # role: db_user.role
+ # }
+
+ conn
+ |> AuthHelpers.store_in_session(db_user)
+ |> redirect(to: return_to)
+
+ {:error, error} ->
+ error_id = :rand.uniform(10_000)
+
+ Logger.error("""
+ Error #{error_id} in AuthController.Telegram.callback()
+ #{inspect(error)}
+ """)
+
+ conn
+ |> put_flash(:error, "Authentication failed. Error id: #{error_id}")
+ |> redirect(to: return_to)
+ end
+ end
+end
diff --git a/lib/wik_web/live/sign_in_live.ex b/lib/wik_web/live/sign_in_live.ex
new file mode 100644
index 0000000..d96affb
--- /dev/null
+++ b/lib/wik_web/live/sign_in_live.ex
@@ -0,0 +1,27 @@
+defmodule WikWeb.SignInLive do
+ @moduledoc """
+ """
+
+ use WikWeb, :live_view
+ alias WikWeb.Components.RealtimeToast
+ alias WikWeb.Components
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <.link class="btn btn-primary" navigate="/dev/login">dev login
+
+
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ dbg("🔴")
+ dbg(socket.assigns)
+ {:ok, socket}
+ end
+end
diff --git a/lib/wik_web/router.ex b/lib/wik_web/router.ex
index 97c54bf..9cd1837 100644
--- a/lib/wik_web/router.ex
+++ b/lib/wik_web/router.ex
@@ -34,18 +34,19 @@ defmodule WikWeb.Router do
scope "/", WikWeb do
pipe_through :browser
+ get "/auth/telegram/callback", AuthController.Telegram, :callback
auth_routes AuthController, Wik.Accounts.User, path: "/auth"
sign_out_route AuthController
- # Remove these if you'd like to use your own authentication views
- sign_in_route register_path: "/register",
- reset_path: "/reset",
- auth_routes_prefix: "/auth",
- on_mount: [{WikWeb.LiveUserAuth, :live_no_user}],
- overrides: [
- WikWeb.AuthOverrides,
- Elixir.AshAuthentication.Phoenix.Overrides.DaisyUI
- ]
+ # # Remove these if you'd like to use your own authentication views
+ # sign_in_route register_path: "/register",
+ # reset_path: "/reset",
+ # auth_routes_prefix: "/auth",
+ # on_mount: [{WikWeb.LiveUserAuth, :live_no_user}],
+ # overrides: [
+ # WikWeb.AuthOverrides,
+ # Elixir.AshAuthentication.Phoenix.Overrides.DaisyUI
+ # ]
# Remove this if you do not want to use the reset password feature
reset_route auth_routes_prefix: "/auth",
@@ -64,6 +65,8 @@ defmodule WikWeb.Router do
auth_routes_prefix: "/auth",
overrides: [WikWeb.AuthOverrides, Elixir.AshAuthentication.Phoenix.Overrides.DaisyUI]
)
+
+ live "/sign-in", SignInLive, :show
end
scope "/", WikWeb do
@@ -125,6 +128,8 @@ defmodule WikWeb.Router do
scope "/dev" do
pipe_through :browser
+ get "/login", WikWeb.AuthController.Dev, :login
+ live "/telegram", WikWeb.TelegramLive.BotUpdates, :index
live_dashboard "/dashboard", metrics: WikWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
diff --git a/priv/repo/migrations/20260207004623_migrate_resources7_dev.exs b/priv/repo/migrations/20260207004623_migrate_resources7_dev.exs
new file mode 100644
index 0000000..83b3716
--- /dev/null
+++ b/priv/repo/migrations/20260207004623_migrate_resources7_dev.exs
@@ -0,0 +1,39 @@
+defmodule Wik.Repo.Migrations.MigrateResources7 do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:users) do
+ add :confirmed_at, :utc_datetime_usec
+ add :role, :text, default: "user"
+ add :tz, :text
+ add :tg_id, :text
+ add :tg_first_name, :text
+ add :tg_last_name, :text
+ add :tg_username, :text
+ add :tg_photo_url, :text
+ end
+
+ create unique_index(:users, [:tg_id], name: "users_unique_tg_id_index")
+ end
+
+ def down do
+ drop_if_exists unique_index(:users, [:tg_id], name: "users_unique_tg_id_index")
+
+ alter table(:users) do
+ remove :tg_photo_url
+ remove :tg_username
+ remove :tg_last_name
+ remove :tg_first_name
+ remove :tg_id
+ remove :tz
+ remove :role
+ remove :confirmed_at
+ end
+ end
+end
diff --git a/priv/repo/migrations/20260207004946_migrate_resources8_dev.exs b/priv/repo/migrations/20260207004946_migrate_resources8_dev.exs
new file mode 100644
index 0000000..ecfcb75
--- /dev/null
+++ b/priv/repo/migrations/20260207004946_migrate_resources8_dev.exs
@@ -0,0 +1,21 @@
+defmodule Wik.Repo.Migrations.MigrateResources8 do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:users) do
+ modify :email, :citext, null: true
+ end
+ end
+
+ def down do
+ alter table(:users) do
+ modify :email, :citext, null: false
+ end
+ end
+end
diff --git a/priv/resource_snapshots/repo/users/20260207004623_dev.json b/priv/resource_snapshots/repo/users/20260207004623_dev.json
new file mode 100644
index 0000000..059aec4
--- /dev/null
+++ b/priv/resource_snapshots/repo/users/20260207004623_dev.json
@@ -0,0 +1,168 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "citext"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "confirmed_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": true,
+ "default": "\"user\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "role",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tz",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_id",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_first_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_last_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_username",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_photo_url",
+ "type": "text"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "2086A57017F4D2E182152346DB33E6D24B2CB3967EB4ABEB7759C6D54A8F7E21",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_tg_id_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "tg_id"
+ }
+ ],
+ "name": "unique_tg_id",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Wik.Repo",
+ "schema": null,
+ "table": "users"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/users/20260207004946_dev.json b/priv/resource_snapshots/repo/users/20260207004946_dev.json
new file mode 100644
index 0000000..6b188f4
--- /dev/null
+++ b/priv/resource_snapshots/repo/users/20260207004946_dev.json
@@ -0,0 +1,168 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "citext"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "confirmed_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": true,
+ "default": "\"user\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "role",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tz",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_id",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_first_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_last_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_username",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "tg_photo_url",
+ "type": "text"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "A737867823207BFA1CAEE2E7F3CBFA4F2344AE2E4AE09CF3F890A2337266A5BE",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_tg_id_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "tg_id"
+ }
+ ],
+ "name": "unique_tg_id",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Wik.Repo",
+ "schema": null,
+ "table": "users"
+}
\ No newline at end of file