From 0a4d54cfd86ade5c6b3be9fbad7955a550c59a26 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Tue, 24 Sep 2024 11:39:09 -0300 Subject: [PATCH] wip: minor fizes needed, compile time issues on injecting macro codes --- .../.env.dev | 8 + .../.formatter.exs | 6 + .../.gitignore | 37 ++ .../README.md | 18 + .../assets/css/app.css | 424 ++++++++++++++++++ .../assets/css/flash.css | 117 +++++ .../assets/js/app.js | 58 +++ .../assets/vendor/topbar.js | 165 +++++++ .../config/config.exs | 50 +++ .../config/dev.exs | 81 ++++ .../config/prod.exs | 14 + .../config/runtime.exs | 99 ++++ .../config/test.exs | 32 ++ .../lib/arcane.ex | 9 + .../lib/arcane/application.ex | 35 ++ .../lib/arcane/profiles.ex | 17 + .../lib/arcane/profiles/profile.ex | 37 ++ .../lib/arcane/repo.ex | 5 + .../lib/arcane/supabase.ex | 3 + .../lib/arcane_web.ex | 111 +++++ .../lib/arcane_web/auth.ex | 17 + .../lib/arcane_web/components.ex | 251 +++++++++++ .../lib/arcane_web/components/layouts.ex | 14 + .../components/layouts/app.html.heex | 5 + .../components/layouts/root.html.heex | 18 + .../lib/arcane_web/controllers/error_html.ex | 24 + .../lib/arcane_web/controllers/error_json.ex | 21 + .../controllers/session_controller.ex | 86 ++++ .../lib/arcane_web/endpoint.ex | 49 ++ .../arcane_web/live/user_management_live.ex | 131 ++++++ .../lib/arcane_web/router.ex | 31 ++ .../lib/arcane_web/telemetry.ex | 92 ++++ .../phoenix_live_view_user_management/mix.exs | 80 ++++ .../mix.lock | 44 ++ .../priv/repo/migrations/.formatter.exs | 4 + .../20240923125336_create_profiles.exs | 18 + ...0240923125853_create_profiles_policies.exs | 41 ++ .../20240923130302_set_up_storage.exs | 27 ++ .../priv/repo/seeds.exs | 14 + .../priv/static/favicon.ico | Bin 0 -> 152 bytes .../priv/static/robots.txt | 5 + .../supabase/.gitignore | 4 + .../supabase/config.toml | 171 +++++++ .../supabase/seed.sql | 0 .../controllers/error_html_test.exs | 14 + .../controllers/error_json_test.exs | 12 + .../controllers/page_controller_test.exs | 8 + .../test/support/conn_case.ex | 38 ++ .../test/support/data_case.ex | 58 +++ .../test/test_helper.exs | 2 + 50 files changed, 2605 insertions(+) create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/.env.dev create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/.gitignore create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/README.md create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/mix.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/mix.lock create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/supabase/seed.sql create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex create mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/.env.dev b/examples/auth/user_managment/phoenix_live_view_user_management/.env.dev new file mode 100644 index 0000000..4ce3c0b --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/.env.dev @@ -0,0 +1,8 @@ +export DATABASE_USER=postgres +export DATABASE_PASS=postgres +export DATABASE_HOST=127.0.0.1 +export DATABASE_NAME=postgres +export DATABASE_PORT=54322 + +export SUPABASE_URL=http://127.0.0.1:54321 +export SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs b/examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs new file mode 100644 index 0000000..ef8840c --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs @@ -0,0 +1,6 @@ +[ + import_deps: [:ecto, :ecto_sql, :phoenix], + subdirectories: ["priv/*/migrations"], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] +] diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/.gitignore b/examples/auth/user_managment/phoenix_live_view_user_management/.gitignore new file mode 100644 index 0000000..fcf7076 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/.gitignore @@ -0,0 +1,37 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +arcane-*.tar + +# Ignore assets that are produced by build tools. +/priv/static/assets/ + +# Ignore digested assets cache. +/priv/static/cache_manifest.json + +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ + diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/README.md b/examples/auth/user_managment/phoenix_live_view_user_management/README.md new file mode 100644 index 0000000..f11fbc0 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/README.md @@ -0,0 +1,18 @@ +# Arcane + +To start your Phoenix server: + + * Run `mix setup` to install and setup dependencies + * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). + +## Learn more + + * Official website: https://www.phoenixframework.org/ + * Guides: https://hexdocs.pm/phoenix/overview.html + * Docs: https://hexdocs.pm/phoenix + * Forum: https://elixirforum.com/c/phoenix-forum + * Source: https://github.com/phoenixframework/phoenix diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css b/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css new file mode 100644 index 0000000..4d45a34 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css @@ -0,0 +1,424 @@ +@import "./flash.css"; + +html, +body { + --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, + Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, + sans-serif; + --custom-bg-color: #101010; + --custom-panel-color: #222; + --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8); + --custom-color: #fff; + --custom-color-brand: #24b47e; + --custom-color-secondary: #666; + --custom-border: 1px solid #333; + --custom-border-radius: 5px; + --custom-spacing: 5px; + + padding: 0; + margin: 0; + font-family: var(--custom-font-family); + background-color: var(--custom-bg-color); +} + +* { + color: var(--custom-color); + font-family: var(--custom-font-family); + box-sizing: border-box; +} + +html, +body, +#__next { + height: 100vh; + width: 100vw; + overflow-x: hidden; +} + +/* Grid */ + +.container { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +.row { + position: relative; + width: 100%; +} + +.row [class^="col"] { + float: left; + margin: 0.5rem 2%; + min-height: 0.125rem; +} + +.col-1, +.col-2, +.col-3, +.col-4, +.col-5, +.col-6, +.col-7, +.col-8, +.col-9, +.col-10, +.col-11, +.col-12 { + width: 96%; +} + +.col-1-sm { + width: 4.33%; +} + +.col-2-sm { + width: 12.66%; +} + +.col-3-sm { + width: 21%; +} + +.col-4-sm { + width: 29.33%; +} + +.col-5-sm { + width: 37.66%; +} + +.col-6-sm { + width: 46%; +} + +.col-7-sm { + width: 54.33%; +} + +.col-8-sm { + width: 62.66%; +} + +.col-9-sm { + width: 71%; +} + +.col-10-sm { + width: 79.33%; +} + +.col-11-sm { + width: 87.66%; +} + +.col-12-sm { + width: 96%; +} + +.row::after { + content: ""; + display: table; + clear: both; +} + +.hidden-sm { + display: none; +} + +@media only screen and (min-width: 33.75em) { + /* 540px */ + .container { + width: 80%; + } +} + +@media only screen and (min-width: 45em) { + /* 720px */ + .col-1 { + width: 4.33%; + } + + .col-2 { + width: 12.66%; + } + + .col-3 { + width: 21%; + } + + .col-4 { + width: 29.33%; + } + + .col-5 { + width: 37.66%; + } + + .col-6 { + width: 46%; + } + + .col-7 { + width: 54.33%; + } + + .col-8 { + width: 62.66%; + } + + .col-9 { + width: 71%; + } + + .col-10 { + width: 79.33%; + } + + .col-11 { + width: 87.66%; + } + + .col-12 { + width: 96%; + } + + .hidden-sm { + display: block; + } +} + +@media only screen and (min-width: 60em) { + /* 960px */ + .container { + width: 75%; + max-width: 60rem; + } +} + +/* Forms */ + +label { + display: block; + margin: 5px 0; + color: var(--custom-color-secondary); + font-size: 0.8rem; + text-transform: uppercase; +} + +input { + width: 100%; + border-radius: 5px; + border: var(--custom-border); + padding: 8px; + font-size: 0.9rem; + background-color: var(--custom-bg-color); + color: var(--custom-color); +} + +input[disabled] { + color: var(--custom-color-secondary); +} + +/* Utils */ + +.block { + display: block; + width: 100%; +} + +.inline-block { + display: inline-block; + width: 100%; +} + +.flex { + display: flex; +} + +.flex.column { + flex-direction: column; +} + +.flex.row { + flex-direction: row; +} + +.flex.flex-1 { + flex: 1 1 0; +} + +.flex-end { + justify-content: flex-end; +} + +.flex-center { + justify-content: center; +} + +.items-center { + align-items: center; +} + +.text-sm { + font-size: 0.8rem; + font-weight: 300; +} + +.text-right { + text-align: right; +} + +.font-light { + font-weight: 300; +} + +.opacity-half { + opacity: 50%; +} + +/* Button */ + +button, +.button { + color: var(--custom-color); + border: var(--custom-border); + background-color: var(--custom-bg-color); + display: inline-block; + text-align: center; + border-radius: var(--custom-border-radius); + padding: 0.5rem 1rem; + cursor: pointer; + text-align: center; + font-size: 0.9rem; + text-transform: uppercase; +} + +button.primary, +.button.primary { + background-color: var(--custom-color-brand); + border: 1px solid var(--custom-color-brand); +} + +/* Widgets */ + +.card { + width: 100%; + display: block; + border: var(--custom-border); + border-radius: var(--custom-border-radius); + padding: var(--custom-spacing); +} + +.avatar { + border-radius: var(--custom-border-radius); + overflow: hidden; + max-width: 100%; +} + +.avatar.image { + object-fit: cover; +} + +.avatar.no-image { + background-color: #333; + border: 1px solid rgb(200, 200, 200); + border-radius: 5px; +} + +.footer { + position: absolute; + max-width: 100%; + bottom: 0; + left: 0; + right: 0; + display: flex; + flex-flow: row; + border-top: var(--custom-border); + background-color: var(--custom-bg-color); +} + +.footer div { + padding: var(--custom-spacing); + display: flex; + align-items: center; + width: 100%; +} + +.footer div > img { + height: 20px; + margin-left: 10px; +} + +.footer > div:first-child { + display: none; +} + +.footer > div:nth-child(2) { + justify-content: left; +} + +@media only screen and (min-width: 60em) { + /* 960px */ + .footer > div:first-child { + display: flex; + } + + .footer > div:nth-child(2) { + justify-content: center; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.mainHeader { + width: 100%; + font-size: 1.3rem; + margin-bottom: 20px; +} + +.avatarPlaceholder { + border: var(--custom-border); + border-radius: var(--custom-border-radius); + width: 35px; + height: 35px; + background-color: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; +} + +.form-widget { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-widget > .button { + display: flex; + align-items: center; + justify-content: center; + border: none; + background-color: #444444; + text-transform: none !important; + transition: all 0.2s ease; +} + +.form-widget .button:hover { + background-color: #2a2a2a; +} + +.form-widget .button > .loader { + width: 17px; + animation: spin 1s linear infinite; + filter: invert(1); +} diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css b/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css new file mode 100644 index 0000000..02a52a0 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css @@ -0,0 +1,117 @@ +/* Flash Container Base Styles */ +.flash-container { + position: fixed; + top: 0.5rem; /* Equivalent to top-2 */ + right: 0.5rem; /* Equivalent to right-2 */ + margin-right: 0.5rem; /* Equivalent to mr-2 */ + width: 20rem; /* Equivalent to w-80 */ + z-index: 50; /* Equivalent to z-50 */ + border-radius: 0.5rem; /* Equivalent to rounded-lg */ + padding: 0.75rem; /* Equivalent to p-3 */ + border: 1px solid; /* Equivalent to ring-1 */ + display: flex; + flex-direction: column; + background-color: var(--flash-background); + color: var(--flash-text); + border-color: var(--flash-border); + fill: var(--flash-fill); +} + +/* Responsive Width for Larger Screens */ +@media (min-width: 640px) { + /* sm:w-96 */ + .flash-container { + width: 24rem; /* Equivalent to sm:w-96 */ + } +} + +/* Flash Variants */ + +/* Info Flash Styles */ +.flash-info { + --flash-background: #f0fff4; /* bg-emerald-50 */ + --flash-text: #065f46; /* text-emerald-800 */ + --flash-border: #10b981; /* ring-emerald-500 */ + --flash-fill: #06b6d4; /* fill-cyan-900 */ +} + +/* Error Flash Styles */ +.flash-error { + --flash-background: #fff1f2; /* bg-rose-50 */ + --flash-text: #991b1b; /* text-rose-900 */ + --flash-border: #f43f5e; /* ring-rose-500 */ + --flash-fill: #991b1b; /* fill-rose-900 */ + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* shadow-md */ +} + +/* Flash Title Styles */ +.flash-title { + display: flex; + align-items: center; + gap: 0.375rem; /* Equivalent to gap-1.5 */ + font-size: 0.875rem; /* Equivalent to text-sm */ + font-weight: 600; /* Equivalent to font-semibold */ + line-height: 1.5rem; /* Equivalent to leading-6 */ +} + +/* Icon Styles */ +.icon-small { + height: 1rem; /* Equivalent to h-4 */ + width: 1rem; /* Equivalent to w-4 */ +} + +.icon-medium { + height: 1.25rem; /* Equivalent to h-5 */ + width: 1.25rem; /* Equivalent to w-5 */ +} + +/* Flash Message Text Styles */ +.flash-message { + margin-top: 0.5rem; /* Equivalent to mt-2 */ + font-size: 0.875rem; /* Equivalent to text-sm */ + line-height: 1.25rem; /* Equivalent to leading-5 */ +} + +/* Close Button Styles */ +.flash-close-button { + position: absolute; + top: 0.25rem; /* Equivalent to top-1 */ + right: 0.25rem; /* Equivalent to right-1 */ + padding: 0.5rem; /* Equivalent to p-2 */ + background: none; + border: none; + cursor: pointer; + opacity: 0.4; /* Equivalent to opacity-40 */ + transition: opacity 0.2s ease-in-out; +} + +.flash-close-button:hover { + opacity: 0.7; /* Equivalent to group-hover:opacity-70 */ +} + +/* Flash Group Container */ +.flash-group-container { + /* Add any specific styles for the flash group container if needed */ +} + +/* Animated Spin for Icons */ +.icon-animated-spin { + /* Apply spin only if user has not requested reduced motion */ +} + +@media (prefers-reduced-motion: no-preference) { + .icon-animated-spin { + animation: spin 1s linear infinite; /* Equivalent to animate-spin */ + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Hidden Utility Class */ +.hidden { + display: none; +} diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js b/examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js new file mode 100644 index 0000000..7047842 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js @@ -0,0 +1,58 @@ +import "../css/app.css"; +// If you want to use Phoenix channels, run `mix help phx.gen.channel` +// to get started and then uncomment the line below. +// import "./user_socket.js" + +// You can include dependencies in two ways. +// +// The simplest option is to put them in assets/vendor and +// import them using relative paths: +// +// import "../vendor/some-package.js" +// +// Alternatively, you can `npm install some-package --prefix assets` and import +// them using a path starting with the package name: +// +// import "some-package" +// + +// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. +import "phoenix_html"; +// Establish Phoenix Socket and LiveView configuration. +import { Socket } from "phoenix"; +import { LiveSocket } from "phoenix_live_view"; +import topbar from "../vendor/topbar"; + +let Hooks = {}; + +Hooks.LivePreview = { + mounted() { + this.handleEvent("consume-blob", ({ blob }) => { + const url = URL.createObjectURL(blob); + this.pushEvent("avatar-blob-url", { url }); + }); + }, +}; + +let csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute("content"); +let liveSocket = new LiveSocket("/live", Socket, { + longPollFallbackMs: 2500, + params: { _csrf_token: csrfToken }, + hooks: Hooks, +}); + +// Show progress bar on live navigation and form submits +topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); +window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); +window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); + +// connect if there are any LiveViews on the page +liveSocket.connect(); + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket; diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js b/examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js new file mode 100644 index 0000000..4195727 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js @@ -0,0 +1,165 @@ +/** + * @license MIT + * topbar 2.0.0, 2023-02-04 + * https://buunguyen.github.io/topbar + * Copyright (c) 2021 Buu Nguyen + */ +(function (window, document) { + "use strict"; + + // https://gist.github.com/paulirish/1579671 + (function () { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = + window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + })(); + + var canvas, + currentProgress, + showing, + progressTimerId = null, + fadeTimerId = null, + delayTimerId = null, + addEvent = function (elem, type, handler) { + if (elem.addEventListener) elem.addEventListener(type, handler, false); + else if (elem.attachEvent) elem.attachEvent("on" + type, handler); + else elem["on" + type] = handler; + }, + options = { + autoRun: true, + barThickness: 3, + barColors: { + 0: "rgba(26, 188, 156, .9)", + ".25": "rgba(52, 152, 219, .9)", + ".50": "rgba(241, 196, 15, .9)", + ".75": "rgba(230, 126, 34, .9)", + "1.0": "rgba(211, 84, 0, .9)", + }, + shadowBlur: 10, + shadowColor: "rgba(0, 0, 0, .6)", + className: null, + }, + repaint = function () { + canvas.width = window.innerWidth; + canvas.height = options.barThickness * 5; // need space for shadow + + var ctx = canvas.getContext("2d"); + ctx.shadowBlur = options.shadowBlur; + ctx.shadowColor = options.shadowColor; + + var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + for (var stop in options.barColors) + lineGradient.addColorStop(stop, options.barColors[stop]); + ctx.lineWidth = options.barThickness; + ctx.beginPath(); + ctx.moveTo(0, options.barThickness / 2); + ctx.lineTo( + Math.ceil(currentProgress * canvas.width), + options.barThickness / 2 + ); + ctx.strokeStyle = lineGradient; + ctx.stroke(); + }, + createCanvas = function () { + canvas = document.createElement("canvas"); + var style = canvas.style; + style.position = "fixed"; + style.top = style.left = style.right = style.margin = style.padding = 0; + style.zIndex = 100001; + style.display = "none"; + if (options.className) canvas.classList.add(options.className); + document.body.appendChild(canvas); + addEvent(window, "resize", repaint); + }, + topbar = { + config: function (opts) { + for (var key in opts) + if (options.hasOwnProperty(key)) options[key] = opts[key]; + }, + show: function (delay) { + if (showing) return; + if (delay) { + if (delayTimerId) return; + delayTimerId = setTimeout(() => topbar.show(), delay); + } else { + showing = true; + if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); + if (!canvas) createCanvas(); + canvas.style.opacity = 1; + canvas.style.display = "block"; + topbar.progress(0); + if (options.autoRun) { + (function loop() { + progressTimerId = window.requestAnimationFrame(loop); + topbar.progress( + "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) + ); + })(); + } + } + }, + progress: function (to) { + if (typeof to === "undefined") return currentProgress; + if (typeof to === "string") { + to = + (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 + ? currentProgress + : 0) + parseFloat(to); + } + currentProgress = to > 1 ? 1 : to; + repaint(); + return currentProgress; + }, + hide: function () { + clearTimeout(delayTimerId); + delayTimerId = null; + if (!showing) return; + showing = false; + if (progressTimerId != null) { + window.cancelAnimationFrame(progressTimerId); + progressTimerId = null; + } + (function loop() { + if (topbar.progress("+.1") >= 1) { + canvas.style.opacity -= 0.05; + if (canvas.style.opacity <= 0.05) { + canvas.style.display = "none"; + fadeTimerId = null; + return; + } + } + fadeTimerId = window.requestAnimationFrame(loop); + })(); + }, + }; + + if (typeof module === "object" && typeof module.exports === "object") { + module.exports = topbar; + } else if (typeof define === "function" && define.amd) { + define(function () { + return topbar; + }); + } else { + this.topbar = topbar; + } +}.call(this, window, document)); diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs b/examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs new file mode 100644 index 0000000..e0f6aac --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs @@ -0,0 +1,50 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +config :arcane, Arcane.Supabase.Client, + base_url: System.get_env("SUPABASE_URL"), + api_key: System.get_env("SUPABASE_KEY"), + db: %{schema: "public"} + +config :arcane, + ecto_repos: [Arcane.Repo], + generators: [timestamp_type: :utc_datetime] + +# Configures the endpoint +config :arcane, ArcaneWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [html: ArcaneWeb.ErrorHTML, json: ArcaneWeb.ErrorJSON], + layout: false + ], + pubsub_server: Arcane.PubSub, + live_view: [signing_salt: "pNdlrcKe"] + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.17.11", + arcane: [ + args: + ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs b/examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs new file mode 100644 index 0000000..1d9fcc4 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs @@ -0,0 +1,81 @@ +import Config + +# Supabase local database config +config :arcane, Arcane.Repo, + username: System.get_env("DATABASE_USER"), + password: System.get_env("DATABASE_PASS"), + hostname: System.get_env("DATABASE_HOST"), + database: System.get_env("DATABASE_NAME"), + port: System.get_env("DATABASE_PORT"), + stacktrace: true, + show_sensitive_data_on_connection_error: true, + pool_size: 10 + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. +config :arcane, ArcaneWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. + http: [ip: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "nODI+64yZkWryU/7nSc+AmNiiaMj1hf4lXch5aFuf0UAoBO6jWZ6HN8zKs2npKD0", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:arcane, ~w(--sourcemap=inline --watch)]} + ] + +# ## SSL Support +# +# In order to use HTTPS in development, a self-signed +# certificate can be generated by running the following +# Mix task: +# +# mix phx.gen.cert +# +# Run `mix help phx.gen.cert` for more information. +# +# The `http:` config above can be replaced with: +# +# https: [ +# port: 4001, +# cipher_suite: :strong, +# keyfile: "priv/cert/selfsigned_key.pem", +# certfile: "priv/cert/selfsigned.pem" +# ], +# +# If desired, both `http:` and `https:` keys can be +# configured to run both http and https servers on +# different ports. + +# Watch static and templates for browser reloading. +config :arcane, ArcaneWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"lib/arcane_web/(controllers|live|components)/.*(ex|heex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :arcane, dev_routes: true + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Set a higher stacktrace during development. Avoid configuring such +# in production as building large stacktraces may be expensive. +config :phoenix, :stacktrace_depth, 20 + +# Initialize plugs at runtime for faster development compilation +config :phoenix, :plug_init_mode, :runtime + +config :phoenix_live_view, + # Include HEEx debug annotations as HTML comments in rendered markup + debug_heex_annotations: true, + # Enable helpful, but potentially expensive runtime checks + enable_expensive_runtime_checks: true diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs b/examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs new file mode 100644 index 0000000..21aa7bd --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs @@ -0,0 +1,14 @@ +import Config + +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix assets.deploy` task, +# which you should run after static files are built and +# before starting your production server. +config :arcane, ArcaneWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs b/examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs new file mode 100644 index 0000000..ffb9c02 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs @@ -0,0 +1,99 @@ +import Config + +# config/runtime.exs is executed for all environments, including +# during releases. It is executed after compilation and before the +# system starts, so it is typically used to load production configuration +# and secrets from environment variables or elsewhere. Do not define +# any compile-time configuration in here, as it won't be applied. +# The block below contains prod specific runtime configuration. + +# ## Using releases +# +# If you use `mix release`, you need to explicitly enable the server +# by passing the PHX_SERVER=true when you start it: +# +# PHX_SERVER=true bin/arcane start +# +# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` +# script that automatically sets the env var above. +if System.get_env("PHX_SERVER") do + config :arcane, ArcaneWeb.Endpoint, server: true +end + +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") in ~w(true 1), do: [:inet6], else: [] + + config :arcane, Arcane.Repo, + # ssl: true, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + socket_options: maybe_ipv6 + + # The secret key base is used to sign/encrypt cookies and other secrets. + # A default value is used in config/dev.exs and config/test.exs but you + # want to use a different value for prod and you most likely don't want + # to check this value into version control, so we use an environment + # variable instead. + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :arcane, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + + config :arcane, ArcaneWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :arcane, ArcaneWeb.Endpoint, + # https: [ + # ..., + # port: 443, + # cipher_suite: :strong, + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") + # ] + # + # The `cipher_suite` is set to `:strong` to support only the + # latest and more secure SSL ciphers. This means old browsers + # and clients may not be supported. You can set it to + # `:compatible` for wider support. + # + # `:keyfile` and `:certfile` expect an absolute path to the key + # and cert in disk or a relative path inside priv, for example + # "priv/ssl/server.key". For all supported SSL configuration + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 + # + # We also recommend setting `force_ssl` in your config/prod.exs, + # ensuring no data is ever sent via http, always redirecting to https: + # + # config :arcane, ArcaneWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs b/examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs new file mode 100644 index 0000000..7e7ea8a --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs @@ -0,0 +1,32 @@ +import Config + +# Configure your database +# +# The MIX_TEST_PARTITION environment variable can be used +# to provide built-in test partitioning in CI environment. +# Run `mix help test` for more information. +config :arcane, Arcane.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "arcane_test#{System.get_env("MIX_TEST_PARTITION")}", + port: 54322, + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: System.schedulers_online() * 2 + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :arcane, ArcaneWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "W0RjQEaGhgYhQRvuaYSomPIeJ9iJLUtDVgMn3yJvwC9By/R9jpThnEdXxngvGQLG", + server: false + +# Print only warnings and errors during test +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime + +# Enable helpful, but potentially expensive runtime checks +config :phoenix_live_view, + enable_expensive_runtime_checks: true diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex new file mode 100644 index 0000000..094de16 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex @@ -0,0 +1,9 @@ +defmodule Arcane do + @moduledoc """ + Arcane keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex new file mode 100644 index 0000000..5d16ede --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex @@ -0,0 +1,35 @@ +defmodule Arcane.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + ArcaneWeb.Telemetry, + Arcane.Repo, + {DNSCluster, query: Application.get_env(:arcane, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: Arcane.PubSub}, + # Start a worker by calling: Arcane.Worker.start_link(arg) + # {Arcane.Worker, arg}, + # Start to serve requests, typically the last entry + ArcaneWeb.Endpoint, + Arcane.Supabase.Client + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Arcane.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + ArcaneWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex new file mode 100644 index 0000000..625042f --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex @@ -0,0 +1,17 @@ +defmodule Arcane.Profiles do + alias Arcane.Profiles.Profile + alias Arcane.Repo + + def get_profile(id: id) do + Repo.get(Profile, id) + end + + def upsert_profile(attrs) do + changeset = Profile.changeset(%Profile{}, attrs) + + Repo.insert(changeset, + on_conflict: {:replace_all_except, [:id]}, + conflict_target: :id + ) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex new file mode 100644 index 0000000..77c0a0b --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex @@ -0,0 +1,37 @@ +defmodule Arcane.Profiles.Profile do + @moduledoc """ + Profiles are the main data structure for users. + """ + + use Ecto.Schema + + import Ecto.Changeset + + @type t :: %__MODULE__{ + id: Ecto.UUID.t(), + username: String.t() | nil, + website: String.t() | nil, + avatar_url: String.t() | nil, + inserted_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t() + } + + @primary_key {:id, :binary_id, autogenerate: false} + schema "profiles" do + field :username, :string + field :website, :string + field :avatar_url, :string + + timestamps() + end + + def changeset(profile \\ %__MODULE__{}, %{} = params) do + profile + |> cast(params, [:id, :username, :website, :avatar_url]) + |> validate_required([:id]) + |> validate_length(:username, min: 3) + |> validate_length(:website, max: 255) + |> unique_constraint(:username) + |> foreign_key_constraint(:id) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex new file mode 100644 index 0000000..ff32a94 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex @@ -0,0 +1,5 @@ +defmodule Arcane.Repo do + use Ecto.Repo, + otp_app: :arcane, + adapter: Ecto.Adapters.Postgres +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex new file mode 100644 index 0000000..230bf25 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex @@ -0,0 +1,3 @@ +defmodule Arcane.Supabase.Client do + use Supabase.Client, otp_app: :arcane +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex new file mode 100644 index 0000000..b5f1643 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex @@ -0,0 +1,111 @@ +defmodule ArcaneWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + + This can be used in your application as: + + use ArcaneWeb, :controller + use ArcaneWeb, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: ArcaneWeb.Layouts] + + import Plug.Conn + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {ArcaneWeb.Layouts, :app} + + unquote(html_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # HTML escaping functionality + import Phoenix.HTML + # UI components + import ArcaneWeb.Components + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: ArcaneWeb.Endpoint, + router: ArcaneWeb.Router, + statics: ArcaneWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/live_view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex new file mode 100644 index 0000000..47a499d --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex @@ -0,0 +1,17 @@ +defmodule ArcaneWeb.Auth do + use Supabase.GoTrue.LiveView, + endpoint: ArcaneWeb.Endpoint, + client: Arcane.Supabase.Client, + signed_in_path: "/", + not_authenticated_path: "/" + + # LiveView cannot write cookies + # or set session, so we need to use Plug + # to handle the session and cookies + # check ArcaneWeb.SessionController + use Supabase.GoTrue.Plug, + endpoint: ArcaneWeb.Endpoint, + client: Arcane.Supabase.Client, + signed_in_path: "/", + not_authenticated_path: "/" +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex new file mode 100644 index 0000000..9c89bb2 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex @@ -0,0 +1,251 @@ +defmodule ArcaneWeb.Components do + @moduledoc """ + This module define function components. + """ + + use ArcaneWeb, :verified_routes + use Phoenix.Component + + alias Phoenix.LiveView.JS + + attr :field, Phoenix.HTML.FormField + attr :src, :string + attr :size, :integer + attr :uploading?, :boolean, default: false + + def avatar(%{size: size} = assigns) do + assigns = + assigns + |> Map.put(:height, "#{size}em") + |> Map.put(:width, "#{size}em") + + ~H""" +
+ Avatar +
+ +
+ + +
+
+ """ + end + + attr :form, Phoenix.HTML.Form, required: true + + def auth(assigns) do + ~H""" + <.form for={@form} action={~p"/session"} class="row flex flex-center"> +
+

Supabase + Phoenix LiveView

+

Sign in via magic link with your email below

+
+ +
+
+ +
+
+ + """ + end + + attr :form, Phoenix.HTML.Form, required: true + attr :avatar, :string + attr :"trigger-signout", :boolean, default: false + + def account(assigns) do + ~H""" + <.form + for={@form} + class="form-widget" + phx-submit="update-profile" + phx-change="upload-profile" + action={~p"/session"} + phx-trigger-action={Map.get(assigns, :"trigger-signout", false)} + method="delete" + > + + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +
+ + """ + end + + @doc """ + Renders flash notices. + + ## Examples + + <.flash kind={:info} flash={@flash} /> + <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back! + """ + attr :id, :string, doc: "the optional id of flash container" + attr :flash, :map, default: %{}, doc: "the map of flash messages to display" + attr :title, :string, default: nil + attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" + attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" + + slot :inner_block, doc: "the optional inner block that renders the flash message" + + def flash(assigns) do + assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end) + + ~H""" +
hide("##{@id}")} + role="alert" + class={[ + "flash-container", + @kind == :info && "flash-info", + @kind == :error && "flash-error" + ]} + {@rest} + > +

+ <%= @title %> +

+

<%= msg %>

+ +
+ """ + end + + @doc """ + Shows the flash group with standard titles and content. + + ## Examples + + <.flash_group flash={@flash} /> + """ + attr :flash, :map, required: true, doc: "the map of flash messages" + attr :id, :string, default: "flash-group", doc: "the optional id of flash container" + + def flash_group(assigns) do + ~H""" +
+ <.flash kind={:info} title="Success!" flash={@flash} /> + <.flash kind={:error} title="Error!" flash={@flash} /> + <.flash + id="client-error" + kind={:error} + title="We can't find the internet!" + phx-disconnected={show(".phx-client-error #client-error")} + phx-connected={hide("#client-error")} + class="hidden" + > + Attempting to reconnect... + + + <.flash + id="server-error" + kind={:error} + title="Something went wrong!" + phx-disconnected={show(".phx-server-error #server-error")} + phx-connected={hide("#server-error")} + class="hidden" + > + Hang in there while we get back on track + +
+ """ + end + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + time: 300, + transition: + {"transition-all ease-out duration-300", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", + "opacity-100 translate-y-0 sm:scale-100"} + ) + end + + def hide(js \\ %JS{}, selector) do + JS.hide(js, + to: selector, + time: 200, + transition: + {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} + ) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex new file mode 100644 index 0000000..3992fb9 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex @@ -0,0 +1,14 @@ +defmodule ArcaneWeb.Layouts do + @moduledoc """ + This module holds different layouts used by your application. + + See the `layouts` directory for all templates available. + The "root" layout is a skeleton rendered as part of the + application router. The "app" layout is set as the default + layout on both `use ArcaneWeb, :controller` and + `use ArcaneWeb, :live_view`. + """ + use ArcaneWeb, :html + + embed_templates "layouts/*" +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex new file mode 100644 index 0000000..3fbb6fc --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex @@ -0,0 +1,5 @@ + +
+ <.flash_group flash={@flash} /> + <%= @inner_content %> +
diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex new file mode 100644 index 0000000..7c0c7c3 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex @@ -0,0 +1,18 @@ + + + + + + + <.live_title> + <%= assigns[:page_title] || "Arcane" %> + + + + + + + <%= @inner_content %> + + diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex new file mode 100644 index 0000000..0e247e5 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex @@ -0,0 +1,24 @@ +defmodule ArcaneWeb.ErrorHTML do + @moduledoc """ + This module is invoked by your endpoint in case of errors on HTML requests. + + See config/config.exs. + """ + use ArcaneWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/arcane_web/controllers/error_html/404.html.heex + # * lib/arcane_web/controllers/error_html/500.html.heex + # + # embed_templates "error_html/*" + + # The default is to render a plain text page based on + # the template name. For example, "404.html" becomes + # "Not Found". + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex new file mode 100644 index 0000000..2c59dc0 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex @@ -0,0 +1,21 @@ +defmodule ArcaneWeb.ErrorJSON do + @moduledoc """ + This module is invoked by your endpoint in case of errors on JSON requests. + + See config/config.exs. + """ + + # If you want to customize a particular status code, + # you may add your own clauses, such as: + # + # def render("500.json", _assigns) do + # %{errors: %{detail: "Internal Server Error"}} + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.json" becomes + # "Not Found". + def render(template, _assigns) do + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex new file mode 100644 index 0000000..32e0860 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex @@ -0,0 +1,86 @@ +defmodule ArcaneWeb.SessionController do + use ArcaneWeb, :controller + + import ArcaneWeb.Auth + import Phoenix.LiveView.Controller + + alias ArcaneWeb.UserManagementLive + alias Supabase.GoTrue + + require Logger + + def create(conn, %{"email" => email}) do + params = %{ + email: email, + options: %{ + should_create_user: true, + email_redirect_to: ~p"/session/confirm" + } + } + + {:ok, client} = Arcane.Supabase.Client.get_client() + + case GoTrue.sign_in_with_otp(client, params) do + :ok -> + message = "Check your email for the login link!" + + conn + |> put_flash(:success, message) + |> live_render(UserManagementLive) + + {:error, error} -> + Logger.error(""" + [#{__MODULE__}] => Failed to login user: + ERROR: #{inspect(error, pretty: true)} + """) + + message = "Failed to send login link!" + + conn + |> put_flash(:error, message) + |> live_render(UserManagementLive) + end + end + + def confirm(conn, %{"token" => token, "type" => "magiclink"}) do + {:ok, client} = Arcane.Supabase.Client.get_client() + + params = %{ + token_hash: token, + type: :magiclink + } + + case GoTrue.verify_otp(client, params) do + {:ok, session} -> + conn + |> put_token_in_session(session.access_token) + |> live_render(UserManagementLive, + session: %{ + "user_token" => session.access_token, + "live_socket_id" => get_session(conn, :live_socket_id) + } + ) + + {:error, error} -> + Logger.error(""" + [#{__MODULE__}] => Failed to verify OTP: + ERROR: #{inspect(error, pretty: true)} + """) + + message = "Failed to verify login link!" + + conn + |> put_flash(:error, message) + |> live_render(UserManagementLive) + end + end + + def signout(conn, _params) do + message = "You have been signed out!" + + conn + |> log_out_user(:local) + |> put_flash(:info, message) + |> live_render(UserManagementLive) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex new file mode 100644 index 0000000..c36476d --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex @@ -0,0 +1,49 @@ +defmodule ArcaneWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :arcane + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + @session_options [ + store: :cookie, + key: "_arcane_key", + signing_salt: "il6sVVfc", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]], + longpoll: [connect_info: [session: @session_options]] + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :arcane, + gzip: false, + only: ArcaneWeb.static_paths() + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :arcane + end + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug ArcaneWeb.Router +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex new file mode 100644 index 0000000..8e77b5f --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex @@ -0,0 +1,131 @@ +defmodule ArcaneWeb.UserManagementLive do + use ArcaneWeb, :live_view + + import ArcaneWeb.Components + + alias Arcane.Profiles + alias Phoenix.LiveView.AsyncResult + alias Supabase.Storage + alias Supabase.Storage.Bucket + + require Logger + + on_mount {ArcaneWeb.Auth, :mount_current_user} + + def mount(_params, _session, socket) do + current_user = socket.assigns.current_user + profile = current_user && Profiles.get_profile(id: current_user.id) + + # `assigns` on render expect that the + # `@` is defined on `socket.assigns` + # so we need to define it here if there isn't + # any current user + {:ok, + socket + |> assign(:page_title, "User Management") + |> assign(:auth_form, to_form(%{"email" => nil})) + |> assign( + :account_form, + to_form(%{ + "id" => profile && profile.id, + "username" => profile && profile.username, + "website" => profile && profile.website, + "email" => current_user && current_user.email, + "avatar" => nil + }) + ) + |> assign(:profile, profile) + |> assign_new(:avatar, fn -> nil end) + |> assign(:avatar_blob, AsyncResult.loading()) + |> start_async(:download_avatar_blob, fn -> maybe_download_avatar(profile) end)} + end + + def render(assigns) do + ~H""" +
+ <.account :if={@current_user} form={@account_form} /> + <.auth :if={is_nil(@current_user)} form={@auth_form} /> +
+ """ + end + + def handle_event("update-profile", params, socket) do + IO.inspect(params) + + case Profiles.upsert_profile(params) do + {:ok, profile} -> + Logger.info(""" + [#{__MODULE__}] => Profile updated: #{inspect(profile)} + """) + changeset = Profiles.Profile.changeset(profile, %{}) + + {:noreply, assign(socket, :account_form, to_form(changeset))} + + {:error, changeset} -> + Logger.error(""" + [#{__MODULE__}] => Error updating profile: #{inspect(changeset.errors)} + """) + + {:noreply, put_flash(socket, :error, "Error updating profile")} + end + end + + def handle_event("upload-profile", _params, socket) do + {:noreply, socket} + end + + def handle_event("avatar-blob-url", %{"url" => url}, socket) do + {:noreply, assign(socket, avatar: url)} + end + + def handle_event("signout", _params, socket) do + {:noreply, assign(socket, :"trigger-signout", true)} + end + + # fallback to avoid crashing the LiveView process + # although this isn't a problem for Phoenix + # as Elixir is fault tolerant, but it helps with observability + def handle_event(event, params, socket) do + Logger.info(""" + [#{__MODULE__}] => Unhandled event: #{event} + PARAMS: #{inspect(params, pretty: true)} + """) + + {:noreply, socket} + end + + def handle_async(:download_avatar_blob, {:ok, nil}, socket) do + avatar_blob = socket.assigns.avatar_blob + ok = AsyncResult.ok(avatar_blob, nil) + {:noreply, assign(socket, avatar_blob: ok)} + end + + def handle_async(:download_avatar_blob, {:ok, blob}, socket) do + avatar_blob = socket.assigns.avatar_blob + + {:noreply, + socket + |> assign(avatar_blob: AsyncResult.ok(avatar_blob, blob)) + |> push_event("consume-blob", %{blob: blob})} + end + + def handle_async(:download_avatar_blob, {:error, error}, socket) do + Logger.error(""" + [#{__MODULE__}] => Error downloading avatar blob: #{inspect(error)} + """) + + avatar_blob = socket.assigns.avatar_blob + failed = AsyncResult.failed(avatar_blob, {:error, error}) + {:noreply, assign(socket, avatar_blob: failed)} + end + + defp maybe_download_avatar(nil), do: nil + defp maybe_download_avatar(%Profiles.Profile{avatar_url: nil}), do: nil + + defp maybe_download_avatar(%Profiles.Profile{} = profile) do + client = Arcane.Supabase.Client.get_client() + bucket = %Bucket{name: "avatars"} + + Storage.download_object(client, bucket, profile.avatar_url) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex new file mode 100644 index 0000000..928c864 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex @@ -0,0 +1,31 @@ +defmodule ArcaneWeb.Router do + use ArcaneWeb, :router + + import ArcaneWeb.Auth + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {ArcaneWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + plug :fetch_current_user + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", ArcaneWeb do + pipe_through :browser + + live "/", UserManagementLive + + scope "/session" do + delete "/", SessionController, :signout + post "/", SessionController, :create + get "/confirm", SessionController, :confirm + end + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex new file mode 100644 index 0000000..a846b7a --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex @@ -0,0 +1,92 @@ +defmodule ArcaneWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.start.system_time", + unit: {:native, :millisecond} + ), + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.start.system_time", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.exception.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.socket_connected.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_joined.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("arcane.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("arcane.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("arcane.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("arcane.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("arcane.repo.query.idle_time", + unit: {:native, :millisecond}, + description: + "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {ArcaneWeb, :count_users, []} + ] + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/mix.exs b/examples/auth/user_managment/phoenix_live_view_user_management/mix.exs new file mode 100644 index 0000000..6250bf6 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/mix.exs @@ -0,0 +1,80 @@ +defmodule Arcane.MixProject do + use Mix.Project + + def project do + [ + app: :arcane, + version: "0.1.0", + elixir: "~> 1.14", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {Arcane.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + # supabase + {:supabase_potion, "~> 0.5"}, + # last fix released 24/09/2024 + {:supabase_gotrue, "~> 0.3.10"}, + {:supabase_storage, "~> 0.3"}, + + # phoenix base + {:phoenix, "~> 1.7.14"}, + {:phoenix_ecto, "~> 4.5"}, + {:ecto_sql, "~> 3.10"}, + {:postgrex, ">= 0.0.0"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, + {:phoenix_live_view, "~> 1.0.0-rc.1", override: true}, + {:floki, ">= 0.30.0", only: :test}, + {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:jason, "~> 1.2"}, + {:dns_cluster, "~> 0.1.1"}, + {:bandit, "~> 1.5"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to install project dependencies and perform other setup tasks, run: + # + # $ mix setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], + "assets.setup": ["esbuild.install --if-missing"], + "assets.build": ["esbuild arcane"], + "assets.deploy": [ + "esbuild arcane --minify", + "phx.digest" + ] + ] + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/mix.lock b/examples/auth/user_managment/phoenix_live_view_user_management/mix.lock new file mode 100644 index 0000000..579f4b4 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/mix.lock @@ -0,0 +1,44 @@ +%{ + "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 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", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, + "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, + "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{: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", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, + "supabase_gotrue": {:hex, :supabase_gotrue, "0.3.10", "acc7ba45199bfbe1b1283730d880d27eb1d14d9aee8f9bdec65db3fab9e695ca", [:mix], [{:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.4", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "3700e6fb859a06f6417dab76060f9046debd2c6bbcb6d57661de190183f2b05d"}, + "supabase_potion": {:hex, :supabase_potion, "0.5.1", "3f604c875edc8895010552f6b36ba03fe5f281813234e337adb930dd2f7df178", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c26a9e99fd61fc546694c7a5ae48c4c8ab36295230eb28de04818e1b59610c23"}, + "supabase_storage": {:hex, :supabase_storage, "0.3.4", "e6dd3f560cd330a5c0af372a629a592b1850a4ad4245f086fcdfa03364ea54b8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.4", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "884db370fcce62dcf3d128b20c27f3dcf3ea649df5aa531ec5347b3180ca3a56"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, +} diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs new file mode 100644 index 0000000..49f9151 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs new file mode 100644 index 0000000..4bd5582 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs @@ -0,0 +1,18 @@ +defmodule Arcane.Repo.Migrations.CreateProfiles do + use Ecto.Migration + + def change do + create table(:profiles, primary_key: false) do + add :id, references(:users, prefix: "auth", type: :binary_id), primary_key: true + add :username, :text + add :avatar_url, :text + add :website, :text + + # inserted_at and updated_at + timestamps() + end + + create unique_index(:profiles, :username) + create constraint(:profiles, :username, check: "char_length(username) >= 3") + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs new file mode 100644 index 0000000..2be359e --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs @@ -0,0 +1,41 @@ +defmodule Arcane.Repo.Migrations.CreateProfilesPolicies do + use Ecto.Migration + + def up do + execute("alter table profiles enable row level security;") + + execute(""" + create policy "Public profiles are viewable by everyone." + on profiles for select + using ( true ); + """) + + execute(""" + create policy "Users can insert their own profile." + on profiles for insert + with check ( (select auth.uid()) = id ); + """) + + execute(""" + create policy "Users can update own profile." + on profiles for update + using ( (select auth.uid()) = id ); + """) + end + + def down do + execute("alter table profiles disable row level security;") + + execute(""" + drop policy "Public profiles are viewable by everyone." on profiles; + """) + + execute(""" + drop policy "Users can insert their own profile." on profiles; + """) + + execute(""" + drop policy "Users can update own profile." on profiles; + """) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs new file mode 100644 index 0000000..64beda7 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs @@ -0,0 +1,27 @@ +defmodule Arcane.Repo.Migrations.SetUpStorage do + use Ecto.Migration + + def up do + execute(""" + create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + """) + + execute(""" + create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); + """) + end + + def down do + execute(""" + drop policy "Avatar images are publicly accessible." on storage.objects; + """) + + execute(""" + drop policy "Anyone can upload an avatar." on storage.objects; + """) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs new file mode 100644 index 0000000..95c2555 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs @@ -0,0 +1,14 @@ +# Script for populating the database. You can run it as: +# +# mix run priv/repo/seeds.exs +# +# Inside the script, you can read and write to any of your +# repositories directly: +# +# Arcane.Repo.insert!(%Arcane.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. +Arcane.Repo.query!(""" +insert into storage.buckets (id, name) values ('avatars', 'avatars'); +""") diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico b/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f372bfc21cdd8cb47585339d5fa4d9dd424402f GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=@t!V@Ar*{oFEH`~d50E!_s``s q?{G*w(7?#d#v@^nKnY_HKaYb01EZMZjMqTJ89ZJ6T-G@yGywoKK_h|y literal 0 HcmV?d00001 diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt b/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore b/examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore new file mode 100644 index 0000000..a3ad880 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml b/examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml new file mode 100644 index 0000000..e072216 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml @@ -0,0 +1,171 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "arcane" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` is always included. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. `public` is always included. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +[storage.image_transformation] +enabled = true + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = true +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }} ." +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false + +[analytics] +enabled = false +port = 54327 +vector_port = 54328 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/seed.sql b/examples/auth/user_managment/phoenix_live_view_user_management/supabase/seed.sql new file mode 100644 index 0000000..e69de29 diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs new file mode 100644 index 0000000..56883a8 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs @@ -0,0 +1,14 @@ +defmodule ArcaneWeb.ErrorHTMLTest do + use ArcaneWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template + + test "renders 404.html" do + assert render_to_string(ArcaneWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(ArcaneWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs new file mode 100644 index 0000000..55a99ed --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs @@ -0,0 +1,12 @@ +defmodule ArcaneWeb.ErrorJSONTest do + use ArcaneWeb.ConnCase, async: true + + test "renders 404" do + assert ArcaneWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert ArcaneWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs new file mode 100644 index 0000000..3a184f5 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule ArcaneWeb.PageControllerTest do + use ArcaneWeb.ConnCase + + test "GET /", %{conn: conn} do + conn = get(conn, ~p"/") + assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex b/examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex new file mode 100644 index 0000000..72b939a --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex @@ -0,0 +1,38 @@ +defmodule ArcaneWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use ArcaneWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # The default endpoint for testing + @endpoint ArcaneWeb.Endpoint + + use ArcaneWeb, :verified_routes + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import ArcaneWeb.ConnCase + end + end + + setup tags do + Arcane.DataCase.setup_sandbox(tags) + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex b/examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex new file mode 100644 index 0000000..cb96496 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex @@ -0,0 +1,58 @@ +defmodule Arcane.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use Arcane.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + alias Arcane.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import Arcane.DataCase + end + end + + setup tags do + Arcane.DataCase.setup_sandbox(tags) + :ok + end + + @doc """ + Sets up the sandbox based on the test tags. + """ + def setup_sandbox(tags) do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Arcane.Repo, shared: not tags[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end +end diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs b/examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs new file mode 100644 index 0000000..65d4d27 --- /dev/null +++ b/examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(Arcane.Repo, :manual)