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 01/27] 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) From c017d339c73d0ab7bfd1924824d32e19b5c68cc7 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Tue, 24 Sep 2024 12:30:23 -0300 Subject: [PATCH 02/27] wip: quickstarts --- .../README.md | 18 --- .../.env.dev | 0 .../.formatter.exs | 0 .../.gitignore | 0 .../README.md | 75 ++++++++++ .../assets/css/app.css | 0 .../assets/css/flash.css | 0 .../assets/js/app.js | 0 .../assets/vendor/topbar.js | 0 .../config/config.exs | 0 .../config/dev.exs | 0 .../config/prod.exs | 0 .../config/runtime.exs | 0 .../config/test.exs | 0 .../guides/quickstart.md | 128 ++++++++++++++++++ .../lib/arcane.ex | 0 .../lib/arcane/application.ex | 0 .../lib/arcane/profiles.ex | 0 .../lib/arcane/profiles/profile.ex | 0 .../lib/arcane/repo.ex | 0 .../lib/arcane/supabase.ex | 0 .../lib/arcane_web.ex | 0 .../lib/arcane_web/auth.ex | 0 .../lib/arcane_web/components.ex | 0 .../lib/arcane_web/components/layouts.ex | 0 .../components/layouts/app.html.heex | 0 .../components/layouts/root.html.heex | 0 .../lib/arcane_web/controllers/error_html.ex | 0 .../lib/arcane_web/controllers/error_json.ex | 0 .../controllers/session_controller.ex | 0 .../lib/arcane_web/endpoint.ex | 0 .../arcane_web/live/user_management_live.ex | 0 .../lib/arcane_web/router.ex | 0 .../lib/arcane_web/telemetry.ex | 0 .../phoenix_live_view_user_management/mix.exs | 0 .../mix.lock | 0 .../priv/repo/migrations/.formatter.exs | 0 .../20240923125336_create_profiles.exs | 0 ...0240923125853_create_profiles_policies.exs | 0 .../20240923130302_set_up_storage.exs | 0 .../priv/repo/seeds.exs | 0 .../priv/static/favicon.ico | Bin .../priv/static/robots.txt | 0 .../supabase/.gitignore | 0 .../supabase/config.toml | 0 .../supabase/seed.sql | 0 .../controllers/error_html_test.exs | 0 .../controllers/error_json_test.exs | 0 .../controllers/page_controller_test.exs | 0 .../test/support/conn_case.ex | 0 .../test/support/data_case.ex | 0 .../test/test_helper.exs | 0 guides/quickstarts/phoenix_liveview.md | 87 ++++++++++++ lib/supabase/client/behaviour.ex | 2 +- 54 files changed, 291 insertions(+), 19 deletions(-) delete mode 100644 examples/auth/user_managment/phoenix_live_view_user_management/README.md rename examples/{auth => }/user_managment/phoenix_live_view_user_management/.env.dev (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/.formatter.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/.gitignore (100%) create mode 100644 examples/user_managment/phoenix_live_view_user_management/README.md rename examples/{auth => }/user_managment/phoenix_live_view_user_management/assets/css/app.css (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/assets/css/flash.css (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/assets/js/app.js (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/config/config.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/config/dev.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/config/prod.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/config/runtime.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/config/test.exs (100%) create mode 100644 examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/mix.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/mix.lock (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/priv/static/robots.txt (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/supabase/.gitignore (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/supabase/config.toml (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/supabase/seed.sql (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/support/data_case.ex (100%) rename examples/{auth => }/user_managment/phoenix_live_view_user_management/test/test_helper.exs (100%) create mode 100644 guides/quickstarts/phoenix_liveview.md 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 deleted file mode 100644 index f11fbc0..0000000 --- a/examples/auth/user_managment/phoenix_live_view_user_management/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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/.env.dev b/examples/user_managment/phoenix_live_view_user_management/.env.dev similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/.env.dev rename to examples/user_managment/phoenix_live_view_user_management/.env.dev diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs b/examples/user_managment/phoenix_live_view_user_management/.formatter.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/.formatter.exs rename to examples/user_managment/phoenix_live_view_user_management/.formatter.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/.gitignore b/examples/user_managment/phoenix_live_view_user_management/.gitignore similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/.gitignore rename to examples/user_managment/phoenix_live_view_user_management/.gitignore diff --git a/examples/user_managment/phoenix_live_view_user_management/README.md b/examples/user_managment/phoenix_live_view_user_management/README.md new file mode 100644 index 0000000..dcaa531 --- /dev/null +++ b/examples/user_managment/phoenix_live_view_user_management/README.md @@ -0,0 +1,75 @@ +# Supabase Phoenix LiveView User Management + +This repo is a quick sample of how you can get started building apps using [Phoenix LiveView](https://phoenixframework.org) and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Phoenix LiveView guide](./guides/quickstart.md). + +This repo will demonstrate how to: + +- sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link) +- store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database) +- store image files in [Supabase storage](https://supabase.io/docs/guides/storage) + +## Getting Started + +Before running this app, you need to create a Supabase project and copy [your credentials](./guides/quickstart.md#get-the-api-keys) to `.env` or you can safely use [supabase-cli](https://supabase.com/docs/guides/cli/getting-started) and use the already defined `.env.dev`. + +Run the following command to launch it on `localhost:4000` + +```bash +mix dev +``` + +> Note that this command `mix dev` is a custom alias defind on `mix.exs`. + +## Database Schema + +```sql +-- Create a table for public "profiles" +create table profiles ( + id uuid references auth.users not null, + updated_at timestamp with time zone, + username text unique, + avatar_url text, + website text, + + primary key (id), + unique(username), + constraint username_length check (char_length(username) >= 3) +); + +alter table profiles enable row level security; + +create policy "Public profiles are viewable by everyone." + on profiles for select + using ( true ); + +create policy "Users can insert their own profile." + on profiles for insert + with check ( (select auth.uid()) = id ); + +create policy "Users can update own profile." + on profiles for update + using ( (select auth.uid()) = id ); + +-- Set up Realtime! +begin; + drop publication if exists supabase_realtime; + create publication supabase_realtime; +commit; +alter publication supabase_realtime add table profiles; + +-- Set up Storage! +insert into storage.buckets (id, name) +values ('avatars', 'avatars'); + +create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + +create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); +``` + +> [!INFO] +> You can find the SQL schema in the [migrations](./priv/repo/migrations) folder. +> Generally you would prefer to use direct connection between your app (with ecto) and the Supabase postgres intances instead of using PostgREST diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/assets/css/app.css rename to examples/user_managment/phoenix_live_view_user_management/assets/css/app.css diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/assets/css/flash.css rename to examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js b/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/assets/js/app.js rename to examples/user_managment/phoenix_live_view_user_management/assets/js/app.js diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js b/examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js rename to examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs b/examples/user_managment/phoenix_live_view_user_management/config/config.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/config/config.exs rename to examples/user_managment/phoenix_live_view_user_management/config/config.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs b/examples/user_managment/phoenix_live_view_user_management/config/dev.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/config/dev.exs rename to examples/user_managment/phoenix_live_view_user_management/config/dev.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs b/examples/user_managment/phoenix_live_view_user_management/config/prod.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/config/prod.exs rename to examples/user_managment/phoenix_live_view_user_management/config/prod.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs b/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/config/runtime.exs rename to examples/user_managment/phoenix_live_view_user_management/config/runtime.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs b/examples/user_managment/phoenix_live_view_user_management/config/test.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/config/test.exs rename to examples/user_managment/phoenix_live_view_user_management/config/test.exs diff --git a/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md b/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md new file mode 100644 index 0000000..f2cb4c3 --- /dev/null +++ b/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md @@ -0,0 +1,128 @@ +# Build a User Management App with Phoenix LiveView + +Learn how to use Supabase in your Phoenix LiveView App. + +This tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses: + +- [Supabase Database](https://supabase.com/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](https://supabase.com/docs/guides/auth#row-level-security) so data is protected and users can only access their own information. +- [Supabase Auth](https://supabase.com/docs/guides/auth) - allow users to sign up and log in. +- [Supabase Storage](https://supabase.com/docs/guides/storage) - users can upload a profile photo. + +![Supabase User Management example](https://supabase.com/docs/img/user-management-demo.png) + +> [!INFO] +> If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/zoedsoupe/supabase-ex/tree/main/examples/user_management/phoenix_live_view_user_management). + +## Project Setup + +Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database. + +### Create a Project + +1. [Create a new project](https://supabase.com/dashboard) in the Supabase Dashboard. +2. Enter your project details. +3. Wait for the new database to launch. + +### Set up the database schema + +I'll be doing that using the [Ecto migrations](https://hexdocs.pm/ecto_sql), but you can also do that manually in the Supabase Dashboard. + +### Get the API Keys + +Now that you've created some database tables, you are ready to insert data using the auto-generated API. We just need to get the Project URL and `anon` key from the API settings. + +1. Go to the [API Settings](https://supabase.com/dashboard/project/_/settings/api) page in the Dashboard. +2. Find your Project `URL`, `anon`, and `service_role` keys on this page. + +## Building the app + +Let's start building the Phoenix LiveView app from scratch. + +### Initialize a Phoenix LiveView app + +We can use [`mix phx.new`](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html) to create an app called `arcane`: + +> Before issuing this command, ensure you have [elixir](https://elixir-lang.org) installed +> Also ensure that you have the [phoenix installer](https://hexdocs.pm/phoenix/installation.html) in your machine + +```bash +mix phx.new --adapter bandit --no-tailwind --app arcane phoenix_live_view_user_management + +cd phoenix_live_view_user_management +``` + +Then let's install the needed dependencies to integrate with supabase: [Supabase Potion](https://hexdocs.pm/supabase_potion). We only need to add these lines to your `deps` in `mix.exs`: + +```elixir +defp deps do + [ + {:supabase_potion, "~> 0.5"}, + {:supabase_gotrue, "~> 0.3"}, + {:supabase_storage, "~> 0.3"}, + # other dependencies + ] +end +``` + +Then install them with: + +```sh +mix deps.get +``` + +And finally we want to save the environment variables in a `.env`. +All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). + +```bash .env +export SUPABASE_URL="YOUR_SUPABASE_URL" +export SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY" +``` + +These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. +Amazing thing about [NuxtSupabase](https://supabase.nuxtjs.org/) is that setting environment variables is all we need to do in order to start using Supabase. +No need to initialize Supabase. The library will take care of it automatically. + +### App styling (optional) + +An optional step is to update the CSS file `assets/main.css` to make the app look nice. +You can find the full contents of this file [here](https://github.com/zoedsoupe/supabase-ex/blob/main/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css). + +### Set up Auth component + +TODO + +### User state + +TODO + +### Account component + +TODO + +### Launch! + +TODO + +Once that's done, run this in a terminal window: + +```bash +iex -S mix phx.server +``` + +And then open the browser to [localhost:4000](http://localhost:4000) and you should see the completed app. + +![Supabase Phoenix LiveView](https://supabase.com/docs/img/supabase-vue-3-demo.png) + +## Bonus: Profile photos + +Every Supabase project is configured with [Storage](https://supabase.com//docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +TODO + +### Add the new widget + +TODO + +That is it! You should now be able to upload a profile photo to Supabase Storage and you have a fully functional application. diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex rename to examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/mix.exs b/examples/user_managment/phoenix_live_view_user_management/mix.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/mix.exs rename to examples/user_managment/phoenix_live_view_user_management/mix.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/mix.lock b/examples/user_managment/phoenix_live_view_user_management/mix.lock similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/mix.lock rename to examples/user_managment/phoenix_live_view_user_management/mix.lock diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs rename to examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs rename to examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs rename to examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs rename to examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs rename to examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico b/examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico rename to examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt b/examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/priv/static/robots.txt rename to examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore b/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/supabase/.gitignore rename to examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/supabase/config.toml rename to examples/user_managment/phoenix_live_view_user_management/supabase/config.toml diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/supabase/seed.sql b/examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/supabase/seed.sql rename to examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs rename to examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs rename to examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs rename to examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex b/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex rename to examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex b/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/support/data_case.ex rename to examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex diff --git a/examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs b/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs similarity index 100% rename from examples/auth/user_managment/phoenix_live_view_user_management/test/test_helper.exs rename to examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs diff --git a/guides/quickstarts/phoenix_liveview.md b/guides/quickstarts/phoenix_liveview.md new file mode 100644 index 0000000..876b9a9 --- /dev/null +++ b/guides/quickstarts/phoenix_liveview.md @@ -0,0 +1,87 @@ +# Use Supabase with Phoenix LiveView + +Learn how to create a LiveView project and connect it to your Supabase Postgres database. + +## 1. Create a Phoenix LiveView Project + +Make sure your Elixir and Phoenix installer versions are up to date, then use `mix phx.new` to scaffold a new LiveView project. Postgresql is the default database for Phoenix apps. + +Go to the [Phoenix docs](https://phoenixframework.org) for more details. + +```sh +mix phx.new blog +``` + +## 2. Set up the Postgres connection details + +Go to [database.new](https://database.new/) and create a new Supabase project. Save your database password securely. + +When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password. + +> [!INFO] +> If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI. + +For the production environment, you can set up this env var on your `config/runtime.exs` +```sh +export DATABASE_URL=ecto://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres +``` + +For your local dev environment your can modify the `config/dev.exs` file to look like this (replacing placeholders with your `supabase-cli` config): + +```elixir +# config/dev.exs + +import Config + +config :blog, Blog.Repo, + hostname: "localhost", + port: 54322, # default supabase-cli postgres port + username: "postgres", + password: "postgres", + database: "postgres" + +# other configs +``` + +## 3. Create and run a database migration + +Phoenix LiveView includes [Ecto](https://hexdocs.pm/ecto) as the data mapping and database schema magement tool (aka ORM in other stacks) as well as database migration tooling which generates the SQL migration files for you. + +Create an example `Article` model and generate the migration files. + +```sh +mix phx.gen.schema Posts.Article articles title:string views:integer +mix ecto.migrate +``` + +The first argument is the schema module followed by its plural name (used as the table name). + +The generated schema above will contain: +- a schema file in `lib/blog/posts/article.ex`, with a articles table +- a migration file for the repository + +More information on the [mix phx.new.schema task documentation](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html) + +## 4. Use the Model to interact with the database + +You can use the included Phoenix console to interact with the database. For example, you can create new entries or list all entries in a Model's table. + +```sh +iex -S mix +``` + +```iex +article = %Blog.Posts.Article{title: "Hello Phoenix", body: "I am on Phoenix!"} +Blog.Repo.insert!(article) # Saves the entry to the database +Blog.Repo.all(Blog.Posts.Article) # Lists all the entries of a model in the database +``` + +## 5. Start the app + +Run the development server. Go to [http://127.0.0.1:4000](http://127.0.0.1:4000) in a browser to see your application running. + +```sh +iex -S mix phx.server +``` + +> This command also starts an iex session (REPL) while staring the web server diff --git a/lib/supabase/client/behaviour.ex b/lib/supabase/client/behaviour.ex index ecf9dba..02975c4 100644 --- a/lib/supabase/client/behaviour.ex +++ b/lib/supabase/client/behaviour.ex @@ -1,5 +1,5 @@ defmodule Supabase.Client.Behaviour do - @doc """ + @moduledoc """ The behaviour for the Supabase Client. This behaviour is used to define the API for a Supabase Client. If you're implementing a [Self Managed Client](https://github.com/zoedsoupe/supabase-ex?tab=readme-ov-file#self-managed-clients) as the [Supabase.Client](https://hexdocs.pm/supabase_potion/Supabase.Client.html), this behaviour is already implemented for you. From 787395b244ba55f9ebac7a8d3b26cae2bdae4841 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Sat, 26 Oct 2024 18:45:20 -0300 Subject: [PATCH 03/27] some fixes --- .../lib/arcane/profiles.ex | 27 ++- .../lib/arcane/profiles/profile.ex | 7 + .../lib/arcane_web/components.ex | 175 +++--------------- .../components/layouts/app.html.heex | 1 - .../controllers/session_controller.ex | 70 ++++--- .../arcane_web/live/user_management_live.ex | 89 ++++++--- .../supabase/config.toml | 12 +- .../supabase/templates/magic_link.html | 7 + flake.nix | 2 +- 9 files changed, 171 insertions(+), 219 deletions(-) create mode 100644 examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex index 625042f..c72c126 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex @@ -1,4 +1,6 @@ defmodule Arcane.Profiles do + import Ecto.Query + alias Arcane.Profiles.Profile alias Arcane.Repo @@ -6,12 +8,25 @@ defmodule Arcane.Profiles do Repo.get(Profile, id) end - def upsert_profile(attrs) do - changeset = Profile.changeset(%Profile{}, attrs) + def create_profile(user_id: user_id) do + changeset = Profile.changeset(%Profile{}, %{id: user_id}) + Repo.insert(changeset, on_conflict: :nothing, conflict_target: [:id]) + end + + def update_profile(%{"id" => profile_id} = attrs) do + changeset = Profile.update_changeset(attrs) + + if changeset.valid? do + updated_at = NaiveDateTime.utc_now() + changes = [{:updated_at, updated_at} | Map.to_list(changeset.changes)] + q = from p in Profile, where: p.id == ^profile_id, select: p - Repo.insert(changeset, - on_conflict: {:replace_all_except, [:id]}, - conflict_target: :id - ) + case Repo.update_all(q, set: changes) do + {1, [profile]} -> {:ok, profile} + _ -> {:error, :failed_to_update_profile} + end + else + {:error, changeset} + end end end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex index 77c0a0b..071eb52 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex @@ -34,4 +34,11 @@ defmodule Arcane.Profiles.Profile do |> unique_constraint(:username) |> foreign_key_constraint(:id) end + + def update_changeset(%{} = params) do + %__MODULE__{} + |> cast(params, [:username, :website, :avatar_url]) + |> validate_length(:username, min: 3) + |> validate_length(:website, max: 255) + end end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex index 9c89bb2..5067d52 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex @@ -6,45 +6,32 @@ defmodule ArcaneWeb.Components do use ArcaneWeb, :verified_routes use Phoenix.Component - alias Phoenix.LiveView.JS - - attr :field, Phoenix.HTML.FormField - attr :src, :string + attr :upload, Phoenix.LiveView.UploadConfig, required: true 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") + size_str = "height: #{size}em; width: #{size}em;" + assigns = assign(assigns, size: size_str) ~H"""
- Avatar -
+
-
-
@@ -80,27 +67,16 @@ defmodule ArcaneWeb.Components do end attr :form, Phoenix.HTML.Form, required: true - attr :avatar, :string - attr :"trigger-signout", :boolean, default: false + @doc """ + We actually need 2 different forms as the first one will keep track of + the profile update data and emit LiveView events and the second one will submit an HTTP request + `DELETE /session` to log out the current user (aka delete session cookies) + """ 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" - > - - + <.form for={@form} class="form-widget" phx-submit="update-profile" phx-change="upload-profile"> +
- + + <.form for={%{}} action={~p"/session"} 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/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex index 3fbb6fc..f81f065 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex @@ -1,5 +1,4 @@
- <.flash_group flash={@flash} /> <%= @inner_content %>
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex index 32e0860..58fd526 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex @@ -4,11 +4,19 @@ defmodule ArcaneWeb.SessionController do import ArcaneWeb.Auth import Phoenix.LiveView.Controller + alias Arcane.Profiles alias ArcaneWeb.UserManagementLive alias Supabase.GoTrue require Logger + @doc """ + THis function is responsible to process the log in request and send tbe + magic link via Supabase/GoTrue + + Note that we do `live_render` since there's no state to mantain between + controller and the live view itself (that will do authentication checks). + """ def create(conn, %{"email" => email}) do params = %{ email: email, @@ -22,11 +30,7 @@ defmodule ArcaneWeb.SessionController do 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) + live_render(conn, UserManagementLive) {:error, error} -> Logger.error(""" @@ -34,53 +38,59 @@ defmodule ArcaneWeb.SessionController do ERROR: #{inspect(error, pretty: true)} """) - message = "Failed to send login link!" - - conn - |> put_flash(:error, message) - |> live_render(UserManagementLive) + live_render(conn, UserManagementLive) end end - def confirm(conn, %{"token" => token, "type" => "magiclink"}) do + @doc """ + Once the user clicks the email link that they'll receive, the link will redirect + to the `/session/confirm` route defined on `ArcaneWeb.Router` and will trigger + this function. + + So we create an empty Profile for this user, so the `UserManagementLive` can + correctly show informations about the profile. + + Note also that we put the token into the session, as configured in the `ArcaneWeb.Endpoint` + it will set up session cookies to store authentication information locally. + + Finally, we redirect back the user to the root page, that will redenr `UserManagementLive` + live view. We could use `live_render`, but it would need to pass all the state and session + mannually to the live view, which is unecessary here since it will happen automatically on + `mount` of the live view. + """ + def confirm(conn, %{"token_hash" => token_hash, "type" => "magiclink"}) do {:ok, client} = Arcane.Supabase.Client.get_client() params = %{ - token_hash: token, + token_hash: token_hash, 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) - } - ) + with {:ok, session} <- GoTrue.verify_otp(client, params), + {:ok, user} <- GoTrue.get_user(client, session) do + Profiles.create_profile(user_id: user.id) + conn + |> put_token_in_session(session.access_token) + |> redirect(to: ~p"/") + else {: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) + redirect(conn, to: ~p"/") end end + @doc """ + This function clears the local session, which includes the session cookie, so the user + will need to authenticate again on the application. + """ 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/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex index 8e77b5f..80c697c 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex +++ b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex @@ -12,9 +12,12 @@ defmodule ArcaneWeb.UserManagementLive do on_mount {ArcaneWeb.Auth, :mount_current_user} + @bucket_name "avatars" + def mount(_params, _session, socket) do current_user = socket.assigns.current_user profile = current_user && Profiles.get_profile(id: current_user.id) + account_form = make_account_form(profile, current_user) # `assigns` on render expect that the # `@` is defined on `socket.assigns` @@ -24,18 +27,13 @@ defmodule ArcaneWeb.UserManagementLive do 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(:account_form, account_form) |> assign(:profile, profile) - |> assign_new(:avatar, fn -> nil end) + |> allow_upload(:avatar, + auto_upload: true, + accept: ["image/*"], + progress: &handle_progress/3 + ) |> assign(:avatar_blob, AsyncResult.loading()) |> start_async(:download_avatar_blob, fn -> maybe_download_avatar(profile) end)} end @@ -43,6 +41,7 @@ defmodule ArcaneWeb.UserManagementLive do def render(assigns) do ~H"""
+ <.avatar :if={@current_user} upload={@uploads.avatar} size={10} /> <.account :if={@current_user} form={@account_form} /> <.auth :if={is_nil(@current_user)} form={@auth_form} />
@@ -50,36 +49,34 @@ defmodule ArcaneWeb.UserManagementLive do end def handle_event("update-profile", params, socket) do - IO.inspect(params) + current_user = socket.assigns.current_user + params = Map.merge(params, %{"id" => current_user.id}) - case Profiles.upsert_profile(params) do + case Profiles.update_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))} + account_form = make_account_form(profile, current_user) + {:noreply, assign(socket, :account_form, account_form)} - {:error, changeset} -> + {:error, error} -> Logger.error(""" - [#{__MODULE__}] => Error updating profile: #{inspect(changeset.errors)} + [#{__MODULE__}] => Error updating profile: #{inspect(error)} """) {: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)} + def handle_event("sign-out", _params, socket) do + ArcaneWeb.Auth.log_out_user(socket, :local) + {:noreply, socket} end # fallback to avoid crashing the LiveView process @@ -123,9 +120,51 @@ defmodule ArcaneWeb.UserManagementLive do 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"} + {:ok, client} = Arcane.Supabase.Client.get_client() + bucket = %Bucket{name: @bucket_name} Storage.download_object(client, bucket, profile.avatar_url) end + + defp make_account_form(profile, current_user) do + to_form(%{ + "id" => profile && profile.id, + "username" => profile && profile.username, + "website" => profile && profile.website, + "email" => current_user && current_user.email, + "avatar" => nil + }) + end + + defp handle_progress(:avatar, entry, socket) when entry.done? do + current_user = socket.assigns.current_user + profile = socket.assigns.profile + params = %{profile: profile, user: current_user} + consume_uploaded_entry(socket, entry, &handle_avatar_upload(&1, params)) + + {:noreply, socket} + end + + defp handle_progress(:avatar, entry, socket) do + Logger.info("[#{__MODULE__}] => Avatar with #{entry.progress} progress") + {:noreply, socket} + end + + defp handle_avatar_upload(%{path: path}, %{user: current_user, profile: profile}) do + bucket = %Bucket{name: @bucket_name} + basename = Path.basename(path) + remote_path = Path.join([bucket.name, current_user.id, basename]) + expires = :timer.hours(24) * 365 + + with {:ok, client} = Arcane.Supabase.Client.get_client(), + {:ok, obj} <- Supabase.Storage.upload_object(client, bucket, remote_path, path), + {:ok, url} <- Supabase.Storage.create_signed_url(client, bucket, remote_path, expires), + {:ok, _} <- Profiles.update_profile(%{id: profile.id, avatar_url: url}) do + {:ok, obj.path} + else + err -> + Logger.error("[#{__MODULE__}] => Failed to upload avatar with #{inspect(err)}") + err + end + end end diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml index e072216..d8bce59 100644 --- a/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml +++ b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml @@ -74,9 +74,9 @@ enabled = true 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" +site_url = "http://127.0.0.1:4000/session/confirm" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] +additional_redirect_urls = ["https://127.0.0.1:4000"] # 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. @@ -103,9 +103,11 @@ enable_confirmations = false max_frequency = "1s" # Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" +# We need that for Phoenix since we're using Server Side Rendering +# So we will be doing authentication on the Server +[auth.email.template.magic_link] +subject = "Your Magic Link" +content_path = "./supabase/templates/magic_link.html" [auth.sms] # Allow/disallow new user signups via SMS to your project. diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html b/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html new file mode 100644 index 0000000..f17ac62 --- /dev/null +++ b/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html @@ -0,0 +1,7 @@ +Magic Link + +Follow this link: + + + Log In + \ No newline at end of file diff --git a/flake.nix b/flake.nix index 98f6c92..aed0294 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ mkShell { name = "supabase-ex"; packages = with pkgs; - [beam.elixir_1_17] + [beam.elixir_1_17 postgresql] ++ lib.optional stdenv.isLinux [inotify-tools] ++ lib.optional stdenv.isDarwin [ darwin.apple_sdk.frameworks.CoreServices From c59b7cd9cf1d5eef5c12f6bdede143605a014237 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Tue, 14 Jan 2025 15:59:23 -0300 Subject: [PATCH 04/27] fix: ex_doc linkings --- lib/supabase/error.ex | 2 +- lib/supabase/fetcher.ex | 2 +- lib/supabase/fetcher/request.ex | 2 +- priv/local/client.ex | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/supabase/error.ex b/lib/supabase/error.ex index c96a034..51b8962 100644 --- a/lib/supabase/error.ex +++ b/lib/supabase/error.ex @@ -109,7 +109,7 @@ end defmodule Supabase.HTTPErrorParser do @moduledoc """ - The default error parser in case no one is provided via `Supabase.Fetcher.with_error_parser/2`. + The default error parser in case no one is provided via `Supabase.Fetcher.Request.with_error_parser/2`. Error parsers should be implement firstly by adjacent services libraries, to handle service-specific error like for authentication or storage, although diff --git a/lib/supabase/fetcher.ex b/lib/supabase/fetcher.ex index 08ebada..f1ada84 100644 --- a/lib/supabase/fetcher.ex +++ b/lib/supabase/fetcher.ex @@ -165,7 +165,7 @@ defmodule Supabase.Fetcher do @doc """ Executes the current request builder, asynchronously and returns the response. Note that this function does not **stream** the request, although it can use - a stream to consume chunks, as you can see an example in `Supabase.Fetcher.Adapter.Finch.request_async/2`. + a stream to consume chunks, as you can see an example in `Supabase.Fetcher.Adapter.Finch`. What happens here is that the request is done on a separate process, and the response is sent via message passing, in chunks, so no all HTTP client support async requests. diff --git a/lib/supabase/fetcher/request.ex b/lib/supabase/fetcher/request.ex index 48c1db9..669b78a 100644 --- a/lib/supabase/fetcher/request.ex +++ b/lib/supabase/fetcher/request.ex @@ -133,7 +133,7 @@ defmodule Supabase.Fetcher.Request do @doc """ Attaches a custom body decoder to be called after a successfull response. The body decoder should implement the `Supabase.Fetcher.BodyDecoder` behaviour, and it default - to the `Supabase.Fetcher.JSONDecoder`, or it can be a 2-arity function that will follow the `Supabase.Fetcher.BodyDecoder.decode/1` callback interface. + to the `Supabase.Fetcher.JSONDecoder`, or it can be a 2-arity function that will follow the `Supabase.Fetcher.BodyDecoder.decode` callback interface. You can pass `nil` as the decoder to avoid body decoding, if you need the raw body. """ diff --git a/priv/local/client.ex b/priv/local/client.ex index 1a7c52f..46c1ac4 100644 --- a/priv/local/client.ex +++ b/priv/local/client.ex @@ -1,5 +1,6 @@ # local client to be used in tests and also # dev env defmodule SupabasePotion.Client do + @moduledoc false use Supabase.Client, otp_app: :supabase_potion end From 2262f9d0ce57954e67c520fcf89a09a32e27ae52 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Wed, 5 Mar 2025 08:52:07 -0300 Subject: [PATCH 05/27] improved dev env --- README.md | 19 ++++++------- supabase/config.toml | 37 +++++++++++++++++--------- supabase/templates/confirm_signup.html | 23 ++++++++++++++++ supabase/templates/invite_user.html | 21 +++++++++++++++ supabase/templates/magic_link_otp.html | 24 +++++++++++++++++ supabase/templates/reset_password.html | 23 ++++++++++++++++ 6 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 supabase/templates/confirm_signup.html create mode 100644 supabase/templates/invite_user.html create mode 100644 supabase/templates/magic_link_otp.html create mode 100644 supabase/templates/reset_password.html diff --git a/README.md b/README.md index c3bbdd1..a4f09ed 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,6 @@ Where the magic starts! -> [!WARNING] -> This project is still in high development, expect breaking changes and unexpected behaviour. - ## Getting Started ### Examples @@ -37,7 +34,7 @@ To install the base SDK: ```elixir def deps do [ - {:supabase_potion, "~> 0.5"} + {:supabase_potion, "~> 0.6"} ] end ``` @@ -50,16 +47,20 @@ Available client services are: - [PostgREST](https://github.com/supabase-community/postgres-ex) - [Storage](https://github.com/supabase-community/storage-ex) - [Auth/GoTrue](https://github.com/supabase-community/auth-ex) +- [Edge Functions](https://github.com/supabase-community/functions-ex) +- [Realtime](https://github.com/supabase-community/realtime-ex) -So if you wanna use the Storage and Auth/GoTrue services, your `mix.exs` should look like that: +So if you wanna use the all the above services, your `mix.exs` should look like that: ```elixir def deps do [ - {:supabase_potion, "~> 0.5"}, # base SDK - {:supabase_storage, "~> 0.3"}, # storage integration - {:supabase_gotrue, "~> 0.3"}, # auth integration - {:supabase_postgrest, "~> 0.2"}, # postgrest integration + {:supabase_potion, "~> 0.6"}, # base SDK + {:supabase_storage, "~> 0.4"}, # storage integration + {:supabase_gotrue, "~> 0.4"}, # auth integration + {:supabase_postgrest, "~> 1.0"}, # postgrest integration + {:supabase_functions, "~> 0.1"}, # edge functions integration + {:supabase_realtime, "~> 0.1"}, # realtime integration ] end ``` diff --git a/supabase/config.toml b/supabase/config.toml index 9d33f28..7fb0382 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -50,7 +50,7 @@ enabled = true sql_paths = ['./seed.sql'] [realtime] -enabled = false +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) @@ -68,7 +68,7 @@ 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 = false +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. @@ -110,7 +110,7 @@ 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 +enable_anonymous_sign_ins = true # Allow/disallow testing manual linking of accounts enable_manual_linking = false # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. @@ -121,14 +121,14 @@ password_requirements = "" [auth.email] # Allow/disallow new user signups via email to your project. -enable_signup = false +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 = false # If enabled, users need to confirm their email address before signing in. -enable_confirmations = false +enable_confirmations = true # If enabled, users will need to reauthenticate or have logged in recently to change their password. -secure_password_change = false +secure_password_change = true # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. max_frequency = "1s" # Number of characters used in the email OTP. @@ -146,10 +146,21 @@ otp_expiry = 3600 # admin_email = "admin@email.com" # sender_name = "Admin" -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" +[auth.email.template.invite] +subject = "You have been invited" +content_path = "./supabase/templates/invite_user.html" + +[auth.email.template.recovery] +subject = "Password recovery" +content_path = "./supabase/templates/reset_password.html" + +[auth.email.template.confirmation] +subject = "Signup confirmation" +content_path = "./supabase/templates/confirm_signup.html" + +[auth.email.template.magicLink] +subject = "Your magic Link" +content_path = "./supabase/templates/magic_link_otp.html" [auth.sms] # Allow/disallow new user signups via SMS to your project. @@ -212,10 +223,10 @@ max_frequency = "5s" # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, # `twitter`, `slack`, `spotify`, `workos`, `zoom`. [auth.external.apple] -enabled = false -client_id = "" +enabled = true +client_id = "client-123" # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +secret = "secret-123" # Overrides the default auth redirectUrl. redirect_uri = "" # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, diff --git a/supabase/templates/confirm_signup.html b/supabase/templates/confirm_signup.html new file mode 100644 index 0000000..ee6c64c --- /dev/null +++ b/supabase/templates/confirm_signup.html @@ -0,0 +1,23 @@ + + + + + + + Recuperação de Senha + + + +
+ +
+ + + \ No newline at end of file diff --git a/supabase/templates/invite_user.html b/supabase/templates/invite_user.html new file mode 100644 index 0000000..1cfbb90 --- /dev/null +++ b/supabase/templates/invite_user.html @@ -0,0 +1,21 @@ + + + + + + + Recuperação de Senha + + + +
+
+

Olá!

+

Acesse o link abaixo:

+ {{ .ConfirmationURL }} +

Se você não faz parte da instituição, ignore este email.

+
+
+ + + \ No newline at end of file diff --git a/supabase/templates/magic_link_otp.html b/supabase/templates/magic_link_otp.html new file mode 100644 index 0000000..8f15d1b --- /dev/null +++ b/supabase/templates/magic_link_otp.html @@ -0,0 +1,24 @@ + + + + + + + Link mágico + + + +
+ +
+ + + \ No newline at end of file diff --git a/supabase/templates/reset_password.html b/supabase/templates/reset_password.html new file mode 100644 index 0000000..6230bfe --- /dev/null +++ b/supabase/templates/reset_password.html @@ -0,0 +1,23 @@ + + + + + + + Recuperação de Senha + + + +
+
+

Olá!

+

Acesse o link abaixo:

+ + {{ .SiteURL }}/confirmar?token_hash={{ .TokenHash }}&type=recovery + +

Se você não solicitou a recuperação, ignore este email.

+
+
+ + + \ No newline at end of file From 8e84c85a6a258afb590dcb0e680271acb6ddaf6a Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Wed, 5 Mar 2025 09:01:17 -0300 Subject: [PATCH 06/27] issue 57 --- README.md | 11 +++++++++++ lib/supabase/fetcher.ex | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4f09ed..bb2af1e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,17 @@ def deps do end ``` +### Debug & Logging + +`supabase-ex` follows the [Logger](https://hexdocs.pm/logger) conventions for logging. You can configure the log level and the log backend in your `config.exs`: + +```elixir +config :logger, backends: [{Logger.Backends.Console, :console}] +config :logger, level: :info +``` + +And `supabase-ex` will respect the log level you set. + ### Clients A `Supabase.Client` holds general information about Supabase, that can be used to intereact with any of the children integrations, for example: `Supabase.Storage` or `Supabase.UI`. diff --git a/lib/supabase/fetcher.ex b/lib/supabase/fetcher.ex index 5c8f28c..171d9e8 100644 --- a/lib/supabase/fetcher.ex +++ b/lib/supabase/fetcher.ex @@ -137,6 +137,8 @@ defmodule Supabase.Fetcher do alias Supabase.Fetcher.Response alias Supabase.Fetcher.ResponseAdapter + require Logger + @behaviour Supabase.Fetcher.Behaviour @typedoc "Generic typespec to define possible response values, adapt to each client" @@ -267,7 +269,14 @@ defmodule Supabase.Fetcher do defp handle_response({:error, %Error{} = err}, %Request{} = builder) do metadata = Error.make_default_http_metadata(builder) metadata = Map.merge(metadata, err.metadata) - {:error, %{err | metadata: metadata}} + + {:error, + %{err | metadata: metadata} + |> tap( + &Logger.debug(""" + [#{__MODULE__}]: Error response while processing request. #{inspect(&1)} + """) + )} end defp handle_response({:error, err}, %Request{} = builder) do @@ -278,6 +287,11 @@ defmodule Supabase.Fetcher do code: :unexpected, service: builder.service, metadata: Map.put(metadata, :raw_error, err) + ) + |> tap( + &Logger.debug(""" + [#{__MODULE__}]: Unexpected error while processing request. #{inspect(&1)} + """) )} end @@ -292,6 +306,11 @@ defmodule Supabase.Fetcher do message: message, service: builder.service, metadata: %{stacktrace: stacktrace, exception: exception} + ) + |> tap( + &Logger.error(""" + [#{__MODULE__}]: Exception raised while processing request. #{inspect(&1)} + """) )} end From e231d65b5155ef4bd86e5452898a55626ec58b47 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 6 May 2025 15:16:20 -0300 Subject: [PATCH 07/27] fix: missing api key header on requests --- lib/supabase/fetcher/request.ex | 1 + mix.exs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/supabase/fetcher/request.ex b/lib/supabase/fetcher/request.ex index 48c1db9..d3532bb 100644 --- a/lib/supabase/fetcher/request.ex +++ b/lib/supabase/fetcher/request.ex @@ -108,6 +108,7 @@ defmodule Supabase.Fetcher.Request do headers = global.headers |> Map.put("authorization", "Bearer " <> client.access_token) + |> Map.put("apikey", client.api_key) |> Map.to_list() %__MODULE__{client: client, headers: headers} diff --git a/mix.exs b/mix.exs index 5853f74..ea722c7 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Supabase.MixProject do use Mix.Project - @version "0.6.1" + @version "0.6.2" @source_url "https://github.com/supabase-community/supabase-ex" def project do From 979a0e73ce6a397831a580c644528feb5466a2f8 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sat, 17 May 2025 19:05:14 -0300 Subject: [PATCH 08/27] fix: supaabse start was failing --- supabase/config.toml | 4 ++-- supabase/seed.sql | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/supabase/config.toml b/supabase/config.toml index 9d33f28..046f86a 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -110,7 +110,7 @@ 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 +enable_anonymous_sign_ins = true # Allow/disallow testing manual linking of accounts enable_manual_linking = false # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. @@ -121,7 +121,7 @@ password_requirements = "" [auth.email] # Allow/disallow new user signups via email to your project. -enable_signup = false +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 = false diff --git a/supabase/seed.sql b/supabase/seed.sql index 8c82216..0618c1d 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS films ( +CREATE TABLE IF NOT EXISTS public.films ( id serial PRIMARY KEY, code char(5), title varchar(40) NOT NULL, @@ -7,16 +7,20 @@ CREATE TABLE IF NOT EXISTS films ( kind varchar(10), len interval hour to minute ); +-- Commit after creation to ensure table exists before insertion +COMMIT; -CREATE TABLE IF NOT EXISTS distributors ( +CREATE TABLE IF NOT EXISTS public.distributors ( did integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, name varchar(40) NOT NULL CHECK (name <> ''), film_id integer, FOREIGN KEY (film_id) REFERENCES films (id) ); +-- Commit after creation to ensure table exists before insertion +COMMIT; -- Insert sample data into the films table -INSERT INTO films (code, title, did, date_prod, kind, len) +INSERT INTO public.films (code, title, did, date_prod, kind, len) VALUES ('F001', 'The Shawshank Redemption', 1, '1994-09-23', 'Drama', '2 hours 22 minutes'), ('F002', 'The Godfather', 2, '1972-03-24', 'Crime', '2 hours 55 minutes'), @@ -25,7 +29,7 @@ VALUES ('F005', 'Schindler''s List', 5, '1993-12-15', 'Biography', '3 hours 15 minutes'); -- Insert sample data into the distributors table -INSERT INTO distributors (name, film_id) +INSERT INTO public.distributors (name, film_id) VALUES ('Warner Bros.', 1), ('Paramount Pictures', 2), From 9547790de3a876d74c856d9ac4c2b64f81a7bc55 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Mon, 19 May 2025 18:20:47 -0300 Subject: [PATCH 09/27] capture logs on tests --- test/supabase/fetcher/adapter/finch_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/supabase/fetcher/adapter/finch_test.exs b/test/supabase/fetcher/adapter/finch_test.exs index 1fab4f6..8d2de08 100644 --- a/test/supabase/fetcher/adapter/finch_test.exs +++ b/test/supabase/fetcher/adapter/finch_test.exs @@ -8,6 +8,8 @@ defmodule Supabase.Fetcher.Adapter.FinchTest do alias Supabase.Fetcher.Request alias Supabase.Fetcher.Response + @moduletag capture_log: true + @mock Supabase.TestHTTPAdapter setup do From 52a5c0410a9f458f512c40b5cfce8046cd943fb5 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 21:51:39 -0300 Subject: [PATCH 10/27] reset example app initial scaffold --- .../.formatter.exs | 0 .../.gitignore | 2 +- examples/user_managment/README.md | 18 + examples/user_managment/assets/css/app.css | 5 + .../assets/js/app.js | 40 +- .../user_managment/assets/tailwind.config.js | 74 ++ .../assets/vendor/topbar.js | 0 .../config/config.exs | 31 +- .../config/dev.exs | 26 +- .../config/prod.exs | 3 +- .../config/runtime.exs | 14 +- .../config/test.exs | 9 +- .../lib/arcane.ex => lib/user_management.ex} | 4 +- .../user_management}/application.ex | 21 +- .../lib/user_management/release.ex | 28 + .../lib/user_management/repo.ex | 5 + .../user_management_web.ex} | 20 +- .../components/core_components.ex | 671 ++++++++++++++++++ .../components/layouts.ex | 8 +- .../components/layouts/app.html.heex | 32 + .../components/layouts/root.html.heex | 9 +- .../controllers/error_html.ex | 8 +- .../controllers/error_json.ex | 2 +- .../user_management_web}/endpoint.ex | 16 +- .../lib/user_management_web/router.ex | 21 + .../user_management_web}/telemetry.ex | 15 +- .../mix.exs | 33 +- examples/user_managment/mix.lock | 33 + .../.env.dev | 8 - .../README.md | 75 -- .../assets/css/app.css | 424 ----------- .../assets/css/flash.css | 117 --- .../guides/quickstart.md | 128 ---- .../lib/arcane/profiles.ex | 32 - .../lib/arcane/profiles/profile.ex | 44 -- .../lib/arcane/repo.ex | 5 - .../lib/arcane/supabase.ex | 3 - .../lib/arcane_web/auth.ex | 17 - .../lib/arcane_web/components.ex | 124 ---- .../components/layouts/app.html.heex | 4 - .../controllers/session_controller.ex | 96 --- .../arcane_web/live/user_management_live.ex | 170 ----- .../lib/arcane_web/router.ex | 31 - .../mix.lock | 44 -- .../20240923125336_create_profiles.exs | 18 - ...0240923125853_create_profiles_policies.exs | 41 -- .../20240923130302_set_up_storage.exs | 27 - .../supabase/.gitignore | 4 - .../supabase/config.toml | 173 ----- .../supabase/seed.sql | 0 .../supabase/templates/magic_link.html | 7 - .../controllers/error_html_test.exs | 14 - .../controllers/error_json_test.exs | 12 - .../test/test_helper.exs | 2 - .../priv/repo/migrations/.formatter.exs | 0 .../priv/repo/seeds.exs | 5 +- .../priv/static/favicon.ico | Bin .../priv/static/images/logo.svg | 6 + .../priv/static/robots.txt | 0 .../user_managment/rel/overlays/bin/migrate | 5 + .../rel/overlays/bin/migrate.bat | 1 + .../user_managment/rel/overlays/bin/server | 5 + .../rel/overlays/bin/server.bat | 2 + .../test/support/conn_case.ex | 12 +- .../test/support/data_case.ex | 12 +- examples/user_managment/test/test_helper.exs | 2 + .../controllers/error_html_test.exs | 15 + .../controllers/error_json_test.exs | 14 + .../controllers/page_controller_test.exs | 4 +- 69 files changed, 1079 insertions(+), 1772 deletions(-) rename examples/user_managment/{phoenix_live_view_user_management => }/.formatter.exs (100%) rename examples/user_managment/{phoenix_live_view_user_management => }/.gitignore (97%) create mode 100644 examples/user_managment/README.md create mode 100644 examples/user_managment/assets/css/app.css rename examples/user_managment/{phoenix_live_view_user_management => }/assets/js/app.js (56%) create mode 100644 examples/user_managment/assets/tailwind.config.js rename examples/user_managment/{phoenix_live_view_user_management => }/assets/vendor/topbar.js (100%) rename examples/user_managment/{phoenix_live_view_user_management => }/config/config.exs (66%) rename examples/user_managment/{phoenix_live_view_user_management => }/config/dev.exs (75%) rename examples/user_managment/{phoenix_live_view_user_management => }/config/prod.exs (80%) rename examples/user_managment/{phoenix_live_view_user_management => }/config/runtime.exs (88%) rename examples/user_managment/{phoenix_live_view_user_management => }/config/test.exs (75%) rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane.ex => lib/user_management.ex} (66%) rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane => lib/user_management}/application.ex (52%) create mode 100644 examples/user_managment/lib/user_management/release.ex create mode 100644 examples/user_managment/lib/user_management/repo.ex rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web.ex => lib/user_management_web.ex} (83%) create mode 100644 examples/user_managment/lib/user_management_web/components/core_components.ex rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/components/layouts.ex (64%) create mode 100644 examples/user_managment/lib/user_management_web/components/layouts/app.html.heex rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/components/layouts/root.html.heex (73%) rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/controllers/error_html.ex (72%) rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/controllers/error_json.ex (93%) rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/endpoint.ex (79%) create mode 100644 examples/user_managment/lib/user_management_web/router.ex rename examples/user_managment/{phoenix_live_view_user_management/lib/arcane_web => lib/user_management_web}/telemetry.ex (86%) rename examples/user_managment/{phoenix_live_view_user_management => }/mix.exs (72%) create mode 100644 examples/user_managment/mix.lock delete mode 100644 examples/user_managment/phoenix_live_view_user_management/.env.dev delete mode 100644 examples/user_managment/phoenix_live_view_user_management/README.md delete mode 100644 examples/user_managment/phoenix_live_view_user_management/assets/css/app.css delete mode 100644 examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css delete mode 100644 examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex delete mode 100644 examples/user_managment/phoenix_live_view_user_management/mix.lock delete mode 100644 examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs delete mode 100644 examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs delete mode 100644 examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs delete mode 100644 examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore delete mode 100644 examples/user_managment/phoenix_live_view_user_management/supabase/config.toml delete mode 100644 examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql delete mode 100644 examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html delete mode 100644 examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs delete mode 100644 examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs delete mode 100644 examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs rename examples/user_managment/{phoenix_live_view_user_management => }/priv/repo/migrations/.formatter.exs (100%) rename examples/user_managment/{phoenix_live_view_user_management => }/priv/repo/seeds.exs (67%) rename examples/user_managment/{phoenix_live_view_user_management => }/priv/static/favicon.ico (100%) create mode 100644 examples/user_managment/priv/static/images/logo.svg rename examples/user_managment/{phoenix_live_view_user_management => }/priv/static/robots.txt (100%) create mode 100755 examples/user_managment/rel/overlays/bin/migrate create mode 100755 examples/user_managment/rel/overlays/bin/migrate.bat create mode 100755 examples/user_managment/rel/overlays/bin/server create mode 100755 examples/user_managment/rel/overlays/bin/server.bat rename examples/user_managment/{phoenix_live_view_user_management => }/test/support/conn_case.ex (75%) rename examples/user_managment/{phoenix_live_view_user_management => }/test/support/data_case.ex (81%) create mode 100644 examples/user_managment/test/test_helper.exs create mode 100644 examples/user_managment/test/user_management_web/controllers/error_html_test.exs create mode 100644 examples/user_managment/test/user_management_web/controllers/error_json_test.exs rename examples/user_managment/{phoenix_live_view_user_management/test/arcane_web => test/user_management_web}/controllers/page_controller_test.exs (65%) diff --git a/examples/user_managment/phoenix_live_view_user_management/.formatter.exs b/examples/user_managment/.formatter.exs similarity index 100% rename from examples/user_managment/phoenix_live_view_user_management/.formatter.exs rename to examples/user_managment/.formatter.exs diff --git a/examples/user_managment/phoenix_live_view_user_management/.gitignore b/examples/user_managment/.gitignore similarity index 97% rename from examples/user_managment/phoenix_live_view_user_management/.gitignore rename to examples/user_managment/.gitignore index fcf7076..72b8a8b 100644 --- a/examples/user_managment/phoenix_live_view_user_management/.gitignore +++ b/examples/user_managment/.gitignore @@ -23,7 +23,7 @@ erl_crash.dump /tmp/ # Ignore package tarball (built via "mix hex.build"). -arcane-*.tar +user_management-*.tar # Ignore assets that are produced by build tools. /priv/static/assets/ diff --git a/examples/user_managment/README.md b/examples/user_managment/README.md new file mode 100644 index 0000000..5fcb50b --- /dev/null +++ b/examples/user_managment/README.md @@ -0,0 +1,18 @@ +# UserManagement + +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/user_managment/assets/css/app.css b/examples/user_managment/assets/css/app.css new file mode 100644 index 0000000..378c8f9 --- /dev/null +++ b/examples/user_managment/assets/css/app.css @@ -0,0 +1,5 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +/* This file is for your main application CSS */ diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js b/examples/user_managment/assets/js/app.js similarity index 56% rename from examples/user_managment/phoenix_live_view_user_management/assets/js/app.js rename to examples/user_managment/assets/js/app.js index 7047842..d5e278a 100644 --- a/examples/user_managment/phoenix_live_view_user_management/assets/js/app.js +++ b/examples/user_managment/assets/js/app.js @@ -1,4 +1,3 @@ -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" @@ -17,42 +16,29 @@ import "../css/app.css"; // // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html"; +import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. -import { Socket } from "phoenix"; -import { LiveSocket } from "phoenix_live_view"; -import topbar from "../vendor/topbar"; +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 csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, - params: { _csrf_token: csrfToken }, - hooks: Hooks, -}); + params: {_csrf_token: csrfToken} +}) // 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()); +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(); +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; +window.liveSocket = liveSocket + diff --git a/examples/user_managment/assets/tailwind.config.js b/examples/user_managment/assets/tailwind.config.js new file mode 100644 index 0000000..15a5ef0 --- /dev/null +++ b/examples/user_managment/assets/tailwind.config.js @@ -0,0 +1,74 @@ +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration + +const plugin = require("tailwindcss/plugin") +const fs = require("fs") +const path = require("path") + +module.exports = { + content: [ + "./js/**/*.js", + "../lib/user_management_web.ex", + "../lib/user_management_web/**/*.*ex" + ], + theme: { + extend: { + colors: { + brand: "#FD4F00", + } + }, + }, + plugins: [ + require("@tailwindcss/forms"), + // Allows prefixing tailwind classes with LiveView classes to add rules + // only when LiveView classes are applied, for example: + // + //
+ // + plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), + plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), + plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), + + // Embeds Heroicons (https://heroicons.com) into your app.css bundle + // See your `CoreComponents.icon/1` for more information. + // + plugin(function({matchComponents, theme}) { + let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") + let values = {} + let icons = [ + ["", "/24/outline"], + ["-solid", "/24/solid"], + ["-mini", "/20/solid"], + ["-micro", "/16/solid"] + ] + icons.forEach(([suffix, dir]) => { + fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { + let name = path.basename(file, ".svg") + suffix + values[name] = {name, fullPath: path.join(iconsDir, dir, file)} + }) + }) + matchComponents({ + "hero": ({name, fullPath}) => { + let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") + let size = theme("spacing.6") + if (name.endsWith("-mini")) { + size = theme("spacing.5") + } else if (name.endsWith("-micro")) { + size = theme("spacing.4") + } + return { + [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + "-webkit-mask": `var(--hero-${name})`, + "mask": `var(--hero-${name})`, + "mask-repeat": "no-repeat", + "background-color": "currentColor", + "vertical-align": "middle", + "display": "inline-block", + "width": size, + "height": size + } + } + }, {values}) + }) + ] +} diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js b/examples/user_managment/assets/vendor/topbar.js similarity index 100% rename from examples/user_managment/phoenix_live_view_user_management/assets/vendor/topbar.js rename to examples/user_managment/assets/vendor/topbar.js diff --git a/examples/user_managment/phoenix_live_view_user_management/config/config.exs b/examples/user_managment/config/config.exs similarity index 66% rename from examples/user_managment/phoenix_live_view_user_management/config/config.exs rename to examples/user_managment/config/config.exs index e0f6aac..43a76c6 100644 --- a/examples/user_managment/phoenix_live_view_user_management/config/config.exs +++ b/examples/user_managment/config/config.exs @@ -7,36 +7,43 @@ # 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], +config :user_management, + ecto_repos: [UserManagement.Repo], generators: [timestamp_type: :utc_datetime] # Configures the endpoint -config :arcane, ArcaneWeb.Endpoint, +config :user_management, UserManagementWeb.Endpoint, url: [host: "localhost"], adapter: Bandit.PhoenixAdapter, render_errors: [ - formats: [html: ArcaneWeb.ErrorHTML, json: ArcaneWeb.ErrorJSON], + formats: [html: UserManagementWeb.ErrorHTML, json: UserManagementWeb.ErrorJSON], layout: false ], - pubsub_server: Arcane.PubSub, - live_view: [signing_salt: "pNdlrcKe"] + pubsub_server: UserManagement.PubSub, + live_view: [signing_salt: "I/jBzfjZ"] # Configure esbuild (the version is required) config :esbuild, version: "0.17.11", - arcane: [ + user_management: [ 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__)} ] +# Configure tailwind (the version is required) +config :tailwind, + version: "3.4.3", + user_management: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] + # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", diff --git a/examples/user_managment/phoenix_live_view_user_management/config/dev.exs b/examples/user_managment/config/dev.exs similarity index 75% rename from examples/user_managment/phoenix_live_view_user_management/config/dev.exs rename to examples/user_managment/config/dev.exs index 1d9fcc4..04c4d13 100644 --- a/examples/user_managment/phoenix_live_view_user_management/config/dev.exs +++ b/examples/user_managment/config/dev.exs @@ -1,12 +1,11 @@ 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"), +# Configure your database +config :user_management, UserManagement.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "user_management_dev", stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10 @@ -17,16 +16,17 @@ config :arcane, Arcane.Repo, # 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, +config :user_management, UserManagementWeb.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", + secret_key_base: "qPuV8P5jtLUGTkjfVLst2MXrzCUI9r+8dBrkdngvVM64mVzuPnVLSphbFIlpNaR/", watchers: [ - esbuild: {Esbuild, :install_and_run, [:arcane, ~w(--sourcemap=inline --watch)]} + esbuild: {Esbuild, :install_and_run, [:user_management, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:user_management, ~w(--watch)]} ] # ## SSL Support @@ -53,16 +53,16 @@ config :arcane, ArcaneWeb.Endpoint, # different ports. # Watch static and templates for browser reloading. -config :arcane, ArcaneWeb.Endpoint, +config :user_management, UserManagementWeb.Endpoint, live_reload: [ patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", - ~r"lib/arcane_web/(controllers|live|components)/.*(ex|heex)$" + ~r"lib/user_management_web/(controllers|live|components)/.*(ex|heex)$" ] ] # Enable dev routes for dashboard and mailbox -config :arcane, dev_routes: true +config :user_management, dev_routes: true # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/examples/user_managment/phoenix_live_view_user_management/config/prod.exs b/examples/user_managment/config/prod.exs similarity index 80% rename from examples/user_managment/phoenix_live_view_user_management/config/prod.exs rename to examples/user_managment/config/prod.exs index 21aa7bd..3e06c24 100644 --- a/examples/user_managment/phoenix_live_view_user_management/config/prod.exs +++ b/examples/user_managment/config/prod.exs @@ -5,7 +5,8 @@ import Config # 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" +config :user_management, UserManagementWeb.Endpoint, + cache_static_manifest: "priv/static/cache_manifest.json" # Do not print debug messages in production config :logger, level: :info diff --git a/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs b/examples/user_managment/config/runtime.exs similarity index 88% rename from examples/user_managment/phoenix_live_view_user_management/config/runtime.exs rename to examples/user_managment/config/runtime.exs index ffb9c02..7d0cb8d 100644 --- a/examples/user_managment/phoenix_live_view_user_management/config/runtime.exs +++ b/examples/user_managment/config/runtime.exs @@ -12,12 +12,12 @@ import Config # 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 +# PHX_SERVER=true bin/user_management 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 + config :user_management, UserManagementWeb.Endpoint, server: true end if config_env() == :prod do @@ -30,7 +30,7 @@ if config_env() == :prod do maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] - config :arcane, Arcane.Repo, + config :user_management, UserManagement.Repo, # ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), @@ -51,9 +51,9 @@ if config_env() == :prod do 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 :user_management, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - config :arcane, ArcaneWeb.Endpoint, + config :user_management, UserManagementWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ # Enable IPv6 and bind on all interfaces. @@ -70,7 +70,7 @@ if config_env() == :prod do # To get SSL working, you will need to add the `https` key # to your endpoint configuration: # - # config :arcane, ArcaneWeb.Endpoint, + # config :user_management, UserManagementWeb.Endpoint, # https: [ # ..., # port: 443, @@ -92,7 +92,7 @@ if config_env() == :prod do # 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, + # config :user_management, UserManagementWeb.Endpoint, # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. diff --git a/examples/user_managment/phoenix_live_view_user_management/config/test.exs b/examples/user_managment/config/test.exs similarity index 75% rename from examples/user_managment/phoenix_live_view_user_management/config/test.exs rename to examples/user_managment/config/test.exs index 7e7ea8a..612bc24 100644 --- a/examples/user_managment/phoenix_live_view_user_management/config/test.exs +++ b/examples/user_managment/config/test.exs @@ -5,20 +5,19 @@ import Config # 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, +config :user_management, UserManagement.Repo, username: "postgres", password: "postgres", hostname: "localhost", - database: "arcane_test#{System.get_env("MIX_TEST_PARTITION")}", - port: 54322, + database: "user_management_test#{System.get_env("MIX_TEST_PARTITION")}", 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, +config :user_management, UserManagementWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], - secret_key_base: "W0RjQEaGhgYhQRvuaYSomPIeJ9iJLUtDVgMn3yJvwC9By/R9jpThnEdXxngvGQLG", + secret_key_base: "+Lq4wRKQgId05Yr03l9MGAzuIB4j/kE7WMhK2JNnP3uyUet0L6AQpClju4FJOn3k", server: false # Print only warnings and errors during test diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex b/examples/user_managment/lib/user_management.ex similarity index 66% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex rename to examples/user_managment/lib/user_management.ex index 094de16..6b00bd3 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane.ex +++ b/examples/user_managment/lib/user_management.ex @@ -1,6 +1,6 @@ -defmodule Arcane do +defmodule UserManagement do @moduledoc """ - Arcane keeps the contexts that define your domain + UserManagement keeps the contexts that define your domain and business logic. Contexts are also responsible for managing your data, regardless diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex b/examples/user_managment/lib/user_management/application.ex similarity index 52% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex rename to examples/user_managment/lib/user_management/application.ex index 5d16ede..8011142 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/application.ex +++ b/examples/user_managment/lib/user_management/application.ex @@ -1,4 +1,4 @@ -defmodule Arcane.Application do +defmodule UserManagement.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false @@ -8,20 +8,19 @@ defmodule Arcane.Application do @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}, + UserManagementWeb.Telemetry, + UserManagement.Repo, + {DNSCluster, query: Application.get_env(:user_management, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: UserManagement.PubSub}, + # Start a worker by calling: UserManagement.Worker.start_link(arg) + # {UserManagement.Worker, arg}, # Start to serve requests, typically the last entry - ArcaneWeb.Endpoint, - Arcane.Supabase.Client + UserManagementWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options - opts = [strategy: :one_for_one, name: Arcane.Supervisor] + opts = [strategy: :one_for_one, name: UserManagement.Supervisor] Supervisor.start_link(children, opts) end @@ -29,7 +28,7 @@ defmodule Arcane.Application do # whenever the application is updated. @impl true def config_change(changed, _new, removed) do - ArcaneWeb.Endpoint.config_change(changed, removed) + UserManagementWeb.Endpoint.config_change(changed, removed) :ok end end diff --git a/examples/user_managment/lib/user_management/release.ex b/examples/user_managment/lib/user_management/release.ex new file mode 100644 index 0000000..38c7040 --- /dev/null +++ b/examples/user_managment/lib/user_management/release.ex @@ -0,0 +1,28 @@ +defmodule UserManagement.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :user_management + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/examples/user_managment/lib/user_management/repo.ex b/examples/user_managment/lib/user_management/repo.ex new file mode 100644 index 0000000..7ef0ea8 --- /dev/null +++ b/examples/user_managment/lib/user_management/repo.ex @@ -0,0 +1,5 @@ +defmodule UserManagement.Repo do + use Ecto.Repo, + otp_app: :user_management, + adapter: Ecto.Adapters.Postgres +end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex b/examples/user_managment/lib/user_management_web.ex similarity index 83% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex rename to examples/user_managment/lib/user_management_web.ex index b5f1643..8eb20dc 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web.ex +++ b/examples/user_managment/lib/user_management_web.ex @@ -1,12 +1,12 @@ -defmodule ArcaneWeb do +defmodule UserManagementWeb 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 + use UserManagementWeb, :controller + use UserManagementWeb, :html The definitions below will be executed for every controller, component, etc, so keep them short and clean, focused @@ -40,7 +40,7 @@ defmodule ArcaneWeb do quote do use Phoenix.Controller, formats: [:html, :json], - layouts: [html: ArcaneWeb.Layouts] + layouts: [html: UserManagementWeb.Layouts] import Plug.Conn @@ -51,7 +51,7 @@ defmodule ArcaneWeb do def live_view do quote do use Phoenix.LiveView, - layout: {ArcaneWeb.Layouts, :app} + layout: {UserManagementWeb.Layouts, :app} unquote(html_helpers()) end @@ -82,8 +82,8 @@ defmodule ArcaneWeb do quote do # HTML escaping functionality import Phoenix.HTML - # UI components - import ArcaneWeb.Components + # Core UI components + import UserManagementWeb.CoreComponents # Shortcut for generating JS commands alias Phoenix.LiveView.JS @@ -96,9 +96,9 @@ defmodule ArcaneWeb do def verified_routes do quote do use Phoenix.VerifiedRoutes, - endpoint: ArcaneWeb.Endpoint, - router: ArcaneWeb.Router, - statics: ArcaneWeb.static_paths() + endpoint: UserManagementWeb.Endpoint, + router: UserManagementWeb.Router, + statics: UserManagementWeb.static_paths() end end diff --git a/examples/user_managment/lib/user_management_web/components/core_components.ex b/examples/user_managment/lib/user_management_web/components/core_components.ex new file mode 100644 index 0000000..d730478 --- /dev/null +++ b/examples/user_managment/lib/user_management_web/components/core_components.ex @@ -0,0 +1,671 @@ +defmodule UserManagementWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At first glance, this module may seem daunting, but its goal is to provide + core building blocks for your application, such as modals, tables, and + forms. The components consist mostly of markup and are well-documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + + The default components use Tailwind CSS, a utility-first CSS framework. + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn + how to customize them or feel free to swap in another framework altogether. + + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + + @doc """ + Renders a modal. + + ## Examples + + <.modal id="confirm-modal"> + This is a modal. + + + JS commands may be passed to the `:on_cancel` to configure + the closing/cancel event, for example: + + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> + This is another modal. + + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + slot :inner_block, required: true + + def modal(assigns) do + ~H""" + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ <.label for={@id}>{@label} + + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ <.label for={@id}>{@label} + + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ <.label for={@id}>{@label} + + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + @doc """ + Renders a label. + """ + attr :for, :string, default: nil + slot :inner_block, required: true + + def label(assigns) do + ~H""" + + """ + end + + @doc """ + Generates a generic error message. + """ + slot :inner_block, required: true + + def error(assigns) do + ~H""" +

+ <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> + {render_slot(@inner_block)} +

+ """ + end + + @doc """ + Renders a header with title. + """ + attr :class, :string, default: nil + + slot :inner_block, required: true + slot :subtitle + slot :actions + + def header(assigns) do + ~H""" +
+
+

+ {render_slot(@inner_block)} +

+

+ {render_slot(@subtitle)} +

+
+
{render_slot(@actions)}
+
+ """ + end + + @doc ~S""" + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id">{user.id} + <:col :let={user} label="username">{user.username} + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" +
+ + + + + + + + + + + + + +
{col[:label]} + Actions +
+
+ + + {render_slot(col, @row_item.(row))} + +
+
+
+ + + {render_slot(action, @row_item.(row))} + +
+
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title">{@post.title} + <:item title="Views">{@post.views} + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
+
+
+
{item.title}
+
{render_slot(item)}
+
+
+
+ """ + end + + @doc """ + Renders a back navigation link. + + ## Examples + + <.back navigate={~p"/posts"}>Back to posts + """ + attr :navigate, :any, required: true + slot :inner_block, required: true + + def back(assigns) do + ~H""" +
+ <.link + navigate={@navigate} + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" + > + <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> + {render_slot(@inner_block)} + +
+ """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + width, height, and background color classes. + + Icons are extracted from the `deps/heroicons` directory and bundled within + your compiled app.css by the plugin in your `assets/tailwind.config.js`. + + ## Examples + + <.icon name="hero-x-mark-solid" /> + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :string, default: nil + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + time: 300, + transition: + {"transition-all transform 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 transform 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 + + def show_modal(js \\ %JS{}, id) when is_binary(id) do + js + |> JS.show(to: "##{id}") + |> JS.show( + to: "##{id}-bg", + time: 300, + transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} + ) + |> show("##{id}-container") + |> JS.add_class("overflow-hidden", to: "body") + |> JS.focus_first(to: "##{id}-content") + end + + def hide_modal(js \\ %JS{}, id) do + js + |> JS.hide( + to: "##{id}-bg", + transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} + ) + |> hide("##{id}-container") + |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) + |> JS.remove_class("overflow-hidden", to: "body") + |> JS.pop_focus() + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # You can make use of gettext to translate error messages by + # uncommenting and adjusting the following code: + + # if count = opts[:count] do + # Gettext.dngettext(UserManagementWeb.Gettext, "errors", msg, msg, count, opts) + # else + # Gettext.dgettext(UserManagementWeb.Gettext, "errors", msg, opts) + # end + + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + end) + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex b/examples/user_managment/lib/user_management_web/components/layouts.ex similarity index 64% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex rename to examples/user_managment/lib/user_management_web/components/layouts.ex index 3992fb9..a85b075 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts.ex +++ b/examples/user_managment/lib/user_management_web/components/layouts.ex @@ -1,14 +1,14 @@ -defmodule ArcaneWeb.Layouts do +defmodule UserManagementWeb.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`. + layout on both `use UserManagementWeb, :controller` and + `use UserManagementWeb, :live_view`. """ - use ArcaneWeb, :html + use UserManagementWeb, :html embed_templates "layouts/*" end diff --git a/examples/user_managment/lib/user_management_web/components/layouts/app.html.heex b/examples/user_managment/lib/user_management_web/components/layouts/app.html.heex new file mode 100644 index 0000000..3b3b607 --- /dev/null +++ b/examples/user_managment/lib/user_management_web/components/layouts/app.html.heex @@ -0,0 +1,32 @@ +
+
+
+ + + +

+ v{Application.spec(:phoenix, :vsn)} +

+
+ +
+
+
+
+ <.flash_group flash={@flash} /> + {@inner_content} +
+
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex b/examples/user_managment/lib/user_management_web/components/layouts/root.html.heex similarity index 73% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex rename to examples/user_managment/lib/user_management_web/components/layouts/root.html.heex index 7c0c7c3..edddc68 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/root.html.heex +++ b/examples/user_managment/lib/user_management_web/components/layouts/root.html.heex @@ -4,15 +4,14 @@ - <.live_title> - <%= assigns[:page_title] || "Arcane" %> + <.live_title default="UserManagement" suffix=" · Phoenix Framework"> + {assigns[:page_title]} - - - <%= @inner_content %> + + {@inner_content} diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex b/examples/user_managment/lib/user_management_web/controllers/error_html.ex similarity index 72% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex rename to examples/user_managment/lib/user_management_web/controllers/error_html.ex index 0e247e5..51f3228 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_html.ex +++ b/examples/user_managment/lib/user_management_web/controllers/error_html.ex @@ -1,17 +1,17 @@ -defmodule ArcaneWeb.ErrorHTML do +defmodule UserManagementWeb.ErrorHTML do @moduledoc """ This module is invoked by your endpoint in case of errors on HTML requests. See config/config.exs. """ - use ArcaneWeb, :html + use UserManagementWeb, :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 + # * lib/user_management_web/controllers/error_html/404.html.heex + # * lib/user_management_web/controllers/error_html/500.html.heex # # embed_templates "error_html/*" diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex b/examples/user_managment/lib/user_management_web/controllers/error_json.ex similarity index 93% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex rename to examples/user_managment/lib/user_management_web/controllers/error_json.ex index 2c59dc0..ac7674e 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/error_json.ex +++ b/examples/user_managment/lib/user_management_web/controllers/error_json.ex @@ -1,4 +1,4 @@ -defmodule ArcaneWeb.ErrorJSON do +defmodule UserManagementWeb.ErrorJSON do @moduledoc """ This module is invoked by your endpoint in case of errors on JSON requests. diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex b/examples/user_managment/lib/user_management_web/endpoint.ex similarity index 79% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex rename to examples/user_managment/lib/user_management_web/endpoint.ex index c36476d..4ff2c89 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/endpoint.ex +++ b/examples/user_managment/lib/user_management_web/endpoint.ex @@ -1,13 +1,13 @@ -defmodule ArcaneWeb.Endpoint do - use Phoenix.Endpoint, otp_app: :arcane +defmodule UserManagementWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :user_management # 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", + key: "_user_management_key", + signing_salt: "oig7J15C", same_site: "Lax" ] @@ -21,9 +21,9 @@ defmodule ArcaneWeb.Endpoint do # when deploying your static files in production. plug Plug.Static, at: "/", - from: :arcane, + from: :user_management, gzip: false, - only: ArcaneWeb.static_paths() + only: UserManagementWeb.static_paths() # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. @@ -31,7 +31,7 @@ defmodule ArcaneWeb.Endpoint do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader plug Phoenix.CodeReloader - plug Phoenix.Ecto.CheckRepoStatus, otp_app: :arcane + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :user_management end plug Plug.RequestId @@ -45,5 +45,5 @@ defmodule ArcaneWeb.Endpoint do plug Plug.MethodOverride plug Plug.Head plug Plug.Session, @session_options - plug ArcaneWeb.Router + plug UserManagementWeb.Router end diff --git a/examples/user_managment/lib/user_management_web/router.ex b/examples/user_managment/lib/user_management_web/router.ex new file mode 100644 index 0000000..afb2d57 --- /dev/null +++ b/examples/user_managment/lib/user_management_web/router.ex @@ -0,0 +1,21 @@ +defmodule UserManagementWeb.Router do + use UserManagementWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {UserManagementWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + # Other scopes may use custom stacks. + # scope "/api", UserManagementWeb do + # pipe_through :api + # end +end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex b/examples/user_managment/lib/user_management_web/telemetry.ex similarity index 86% rename from examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex rename to examples/user_managment/lib/user_management_web/telemetry.ex index a846b7a..30ba77c 100644 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/telemetry.ex +++ b/examples/user_managment/lib/user_management_web/telemetry.ex @@ -1,4 +1,4 @@ -defmodule ArcaneWeb.Telemetry do +defmodule UserManagementWeb.Telemetry do use Supervisor import Telemetry.Metrics @@ -43,6 +43,7 @@ defmodule ArcaneWeb.Telemetry do summary("phoenix.socket_connected.duration", unit: {:native, :millisecond} ), + sum("phoenix.socket_drain.count"), summary("phoenix.channel_joined.duration", unit: {:native, :millisecond} ), @@ -52,23 +53,23 @@ defmodule ArcaneWeb.Telemetry do ), # Database Metrics - summary("arcane.repo.query.total_time", + summary("user_management.repo.query.total_time", unit: {:native, :millisecond}, description: "The sum of the other measurements" ), - summary("arcane.repo.query.decode_time", + summary("user_management.repo.query.decode_time", unit: {:native, :millisecond}, description: "The time spent decoding the data received from the database" ), - summary("arcane.repo.query.query_time", + summary("user_management.repo.query.query_time", unit: {:native, :millisecond}, description: "The time spent executing the query" ), - summary("arcane.repo.query.queue_time", + summary("user_management.repo.query.queue_time", unit: {:native, :millisecond}, description: "The time spent waiting for a database connection" ), - summary("arcane.repo.query.idle_time", + summary("user_management.repo.query.idle_time", unit: {:native, :millisecond}, description: "The time the connection spent waiting before being checked out for the query" @@ -86,7 +87,7 @@ defmodule ArcaneWeb.Telemetry 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, []} + # {UserManagementWeb, :count_users, []} ] end end diff --git a/examples/user_managment/phoenix_live_view_user_management/mix.exs b/examples/user_managment/mix.exs similarity index 72% rename from examples/user_managment/phoenix_live_view_user_management/mix.exs rename to examples/user_managment/mix.exs index 6250bf6..d160d9c 100644 --- a/examples/user_managment/phoenix_live_view_user_management/mix.exs +++ b/examples/user_managment/mix.exs @@ -1,9 +1,9 @@ -defmodule Arcane.MixProject do +defmodule UserManagement.MixProject do use Mix.Project def project do [ - app: :arcane, + app: :user_management, version: "0.1.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), @@ -18,7 +18,7 @@ defmodule Arcane.MixProject do # Type `mix help compile.app` for more information. def application do [ - mod: {Arcane.Application, []}, + mod: {UserManagement.Application, []}, extra_applications: [:logger, :runtime_tools] ] end @@ -32,23 +32,23 @@ defmodule Arcane.MixProject do # 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, "~> 1.7.21"}, {: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}, + {:phoenix_live_view, "~> 1.0"}, {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, + {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev}, + {:heroicons, + github: "tailwindlabs/heroicons", + tag: "v2.1.1", + sparse: "optimized", + app: false, + compile: false, + depth: 1}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, {:jason, "~> 1.2"}, @@ -69,10 +69,11 @@ defmodule Arcane.MixProject do "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.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["tailwind user_management", "esbuild user_management"], "assets.deploy": [ - "esbuild arcane --minify", + "tailwind user_management --minify", + "esbuild user_management --minify", "phx.digest" ] ] diff --git a/examples/user_managment/mix.lock b/examples/user_managment/mix.lock new file mode 100644 index 0000000..9784c12 --- /dev/null +++ b/examples/user_managment/mix.lock @@ -0,0 +1,33 @@ +%{ + "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.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", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"}, + "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, + "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.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [: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", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [: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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [: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", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [: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", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, + "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.12", "a37134b9bb3602efbfa5a7a8cb51d50e796f7acff7075af9d9796f30de04c66a", [: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 or ~> 1.8.0-rc", [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", "058e06e59fd38f1feeca59bbf167bec5d44aacd9b745e4363e2ac342ca32e546"}, + "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.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [: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", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, + "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, +} diff --git a/examples/user_managment/phoenix_live_view_user_management/.env.dev b/examples/user_managment/phoenix_live_view_user_management/.env.dev deleted file mode 100644 index 4ce3c0b..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/.env.dev +++ /dev/null @@ -1,8 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/README.md b/examples/user_managment/phoenix_live_view_user_management/README.md deleted file mode 100644 index dcaa531..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Supabase Phoenix LiveView User Management - -This repo is a quick sample of how you can get started building apps using [Phoenix LiveView](https://phoenixframework.org) and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Phoenix LiveView guide](./guides/quickstart.md). - -This repo will demonstrate how to: - -- sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link) -- store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database) -- store image files in [Supabase storage](https://supabase.io/docs/guides/storage) - -## Getting Started - -Before running this app, you need to create a Supabase project and copy [your credentials](./guides/quickstart.md#get-the-api-keys) to `.env` or you can safely use [supabase-cli](https://supabase.com/docs/guides/cli/getting-started) and use the already defined `.env.dev`. - -Run the following command to launch it on `localhost:4000` - -```bash -mix dev -``` - -> Note that this command `mix dev` is a custom alias defind on `mix.exs`. - -## Database Schema - -```sql --- Create a table for public "profiles" -create table profiles ( - id uuid references auth.users not null, - updated_at timestamp with time zone, - username text unique, - avatar_url text, - website text, - - primary key (id), - unique(username), - constraint username_length check (char_length(username) >= 3) -); - -alter table profiles enable row level security; - -create policy "Public profiles are viewable by everyone." - on profiles for select - using ( true ); - -create policy "Users can insert their own profile." - on profiles for insert - with check ( (select auth.uid()) = id ); - -create policy "Users can update own profile." - on profiles for update - using ( (select auth.uid()) = id ); - --- Set up Realtime! -begin; - drop publication if exists supabase_realtime; - create publication supabase_realtime; -commit; -alter publication supabase_realtime add table profiles; - --- Set up Storage! -insert into storage.buckets (id, name) -values ('avatars', 'avatars'); - -create policy "Avatar images are publicly accessible." - on storage.objects for select - using ( bucket_id = 'avatars' ); - -create policy "Anyone can upload an avatar." - on storage.objects for insert - with check ( bucket_id = 'avatars' ); -``` - -> [!INFO] -> You can find the SQL schema in the [migrations](./priv/repo/migrations) folder. -> Generally you would prefer to use direct connection between your app (with ecto) and the Supabase postgres intances instead of using PostgREST diff --git a/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css deleted file mode 100644 index 4d45a34..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css +++ /dev/null @@ -1,424 +0,0 @@ -@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/user_managment/phoenix_live_view_user_management/assets/css/flash.css b/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css deleted file mode 100644 index 02a52a0..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/assets/css/flash.css +++ /dev/null @@ -1,117 +0,0 @@ -/* 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/user_managment/phoenix_live_view_user_management/guides/quickstart.md b/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md deleted file mode 100644 index f2cb4c3..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/guides/quickstart.md +++ /dev/null @@ -1,128 +0,0 @@ -# Build a User Management App with Phoenix LiveView - -Learn how to use Supabase in your Phoenix LiveView App. - -This tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses: - -- [Supabase Database](https://supabase.com/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](https://supabase.com/docs/guides/auth#row-level-security) so data is protected and users can only access their own information. -- [Supabase Auth](https://supabase.com/docs/guides/auth) - allow users to sign up and log in. -- [Supabase Storage](https://supabase.com/docs/guides/storage) - users can upload a profile photo. - -![Supabase User Management example](https://supabase.com/docs/img/user-management-demo.png) - -> [!INFO] -> If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/zoedsoupe/supabase-ex/tree/main/examples/user_management/phoenix_live_view_user_management). - -## Project Setup - -Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database. - -### Create a Project - -1. [Create a new project](https://supabase.com/dashboard) in the Supabase Dashboard. -2. Enter your project details. -3. Wait for the new database to launch. - -### Set up the database schema - -I'll be doing that using the [Ecto migrations](https://hexdocs.pm/ecto_sql), but you can also do that manually in the Supabase Dashboard. - -### Get the API Keys - -Now that you've created some database tables, you are ready to insert data using the auto-generated API. We just need to get the Project URL and `anon` key from the API settings. - -1. Go to the [API Settings](https://supabase.com/dashboard/project/_/settings/api) page in the Dashboard. -2. Find your Project `URL`, `anon`, and `service_role` keys on this page. - -## Building the app - -Let's start building the Phoenix LiveView app from scratch. - -### Initialize a Phoenix LiveView app - -We can use [`mix phx.new`](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html) to create an app called `arcane`: - -> Before issuing this command, ensure you have [elixir](https://elixir-lang.org) installed -> Also ensure that you have the [phoenix installer](https://hexdocs.pm/phoenix/installation.html) in your machine - -```bash -mix phx.new --adapter bandit --no-tailwind --app arcane phoenix_live_view_user_management - -cd phoenix_live_view_user_management -``` - -Then let's install the needed dependencies to integrate with supabase: [Supabase Potion](https://hexdocs.pm/supabase_potion). We only need to add these lines to your `deps` in `mix.exs`: - -```elixir -defp deps do - [ - {:supabase_potion, "~> 0.5"}, - {:supabase_gotrue, "~> 0.3"}, - {:supabase_storage, "~> 0.3"}, - # other dependencies - ] -end -``` - -Then install them with: - -```sh -mix deps.get -``` - -And finally we want to save the environment variables in a `.env`. -All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). - -```bash .env -export SUPABASE_URL="YOUR_SUPABASE_URL" -export SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY" -``` - -These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -Amazing thing about [NuxtSupabase](https://supabase.nuxtjs.org/) is that setting environment variables is all we need to do in order to start using Supabase. -No need to initialize Supabase. The library will take care of it automatically. - -### App styling (optional) - -An optional step is to update the CSS file `assets/main.css` to make the app look nice. -You can find the full contents of this file [here](https://github.com/zoedsoupe/supabase-ex/blob/main/examples/user_managment/phoenix_live_view_user_management/assets/css/app.css). - -### Set up Auth component - -TODO - -### User state - -TODO - -### Account component - -TODO - -### Launch! - -TODO - -Once that's done, run this in a terminal window: - -```bash -iex -S mix phx.server -``` - -And then open the browser to [localhost:4000](http://localhost:4000) and you should see the completed app. - -![Supabase Phoenix LiveView](https://supabase.com/docs/img/supabase-vue-3-demo.png) - -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](https://supabase.com//docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -TODO - -### Add the new widget - -TODO - -That is it! You should now be able to upload a profile photo to Supabase Storage and you have a fully functional application. diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex deleted file mode 100644 index c72c126..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Arcane.Profiles do - import Ecto.Query - - alias Arcane.Profiles.Profile - alias Arcane.Repo - - def get_profile(id: id) do - Repo.get(Profile, id) - end - - def create_profile(user_id: user_id) do - changeset = Profile.changeset(%Profile{}, %{id: user_id}) - Repo.insert(changeset, on_conflict: :nothing, conflict_target: [:id]) - end - - def update_profile(%{"id" => profile_id} = attrs) do - changeset = Profile.update_changeset(attrs) - - if changeset.valid? do - updated_at = NaiveDateTime.utc_now() - changes = [{:updated_at, updated_at} | Map.to_list(changeset.changes)] - q = from p in Profile, where: p.id == ^profile_id, select: p - - case Repo.update_all(q, set: changes) do - {1, [profile]} -> {:ok, profile} - _ -> {:error, :failed_to_update_profile} - end - else - {:error, changeset} - end - end -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex deleted file mode 100644 index 071eb52..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/profiles/profile.ex +++ /dev/null @@ -1,44 +0,0 @@ -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 - - def update_changeset(%{} = params) do - %__MODULE__{} - |> cast(params, [:username, :website, :avatar_url]) - |> validate_length(:username, min: 3) - |> validate_length(:website, max: 255) - end -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex deleted file mode 100644 index ff32a94..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/repo.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Arcane.Repo do - use Ecto.Repo, - otp_app: :arcane, - adapter: Ecto.Adapters.Postgres -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex deleted file mode 100644 index 230bf25..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane/supabase.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule Arcane.Supabase.Client do - use Supabase.Client, otp_app: :arcane -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex deleted file mode 100644 index 47a499d..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/auth.ex +++ /dev/null @@ -1,17 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex deleted file mode 100644 index 5067d52..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components.ex +++ /dev/null @@ -1,124 +0,0 @@ -defmodule ArcaneWeb.Components do - @moduledoc """ - This module define function components. - """ - - use ArcaneWeb, :verified_routes - use Phoenix.Component - - attr :upload, Phoenix.LiveView.UploadConfig, required: true - attr :size, :integer - - def avatar(%{size: size} = assigns) do - size_str = "height: #{size}em; width: #{size}em;" - assigns = assign(assigns, size: size_str) - - ~H""" -
- <.live_img_preview - :for={entry <- @upload.entries} - entry={entry} - alt="Avatar" - class="avatar-image" - style={@size} - /> -
- -
- - <.live_file_input - upload={@upload} - id="single" - style="position: absolute; visibility: hidden;" - /> -
-
- """ - 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 - - @doc """ - We actually need 2 different forms as the first one will keep track of - the profile update data and emit LiveView events and the second one will submit an HTTP request - `DELETE /session` to log out the current user (aka delete session cookies) - """ - def account(assigns) do - ~H""" - <.form for={@form} class="form-widget" phx-submit="update-profile" phx-change="upload-profile"> - -
- - -
-
- - -
-
- - -
- -
- -
- - <.form for={%{}} action={~p"/session"} method="delete"> -
- -
- - """ - end -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex deleted file mode 100644 index f81f065..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/components/layouts/app.html.heex +++ /dev/null @@ -1,4 +0,0 @@ - -
- <%= @inner_content %> -
diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex deleted file mode 100644 index 58fd526..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/controllers/session_controller.ex +++ /dev/null @@ -1,96 +0,0 @@ -defmodule ArcaneWeb.SessionController do - use ArcaneWeb, :controller - - import ArcaneWeb.Auth - import Phoenix.LiveView.Controller - - alias Arcane.Profiles - alias ArcaneWeb.UserManagementLive - alias Supabase.GoTrue - - require Logger - - @doc """ - THis function is responsible to process the log in request and send tbe - magic link via Supabase/GoTrue - - Note that we do `live_render` since there's no state to mantain between - controller and the live view itself (that will do authentication checks). - """ - 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 -> - live_render(conn, UserManagementLive) - - {:error, error} -> - Logger.error(""" - [#{__MODULE__}] => Failed to login user: - ERROR: #{inspect(error, pretty: true)} - """) - - live_render(conn, UserManagementLive) - end - end - - @doc """ - Once the user clicks the email link that they'll receive, the link will redirect - to the `/session/confirm` route defined on `ArcaneWeb.Router` and will trigger - this function. - - So we create an empty Profile for this user, so the `UserManagementLive` can - correctly show informations about the profile. - - Note also that we put the token into the session, as configured in the `ArcaneWeb.Endpoint` - it will set up session cookies to store authentication information locally. - - Finally, we redirect back the user to the root page, that will redenr `UserManagementLive` - live view. We could use `live_render`, but it would need to pass all the state and session - mannually to the live view, which is unecessary here since it will happen automatically on - `mount` of the live view. - """ - def confirm(conn, %{"token_hash" => token_hash, "type" => "magiclink"}) do - {:ok, client} = Arcane.Supabase.Client.get_client() - - params = %{ - token_hash: token_hash, - type: :magiclink - } - - with {:ok, session} <- GoTrue.verify_otp(client, params), - {:ok, user} <- GoTrue.get_user(client, session) do - Profiles.create_profile(user_id: user.id) - - conn - |> put_token_in_session(session.access_token) - |> redirect(to: ~p"/") - else - {:error, error} -> - Logger.error(""" - [#{__MODULE__}] => Failed to verify OTP: - ERROR: #{inspect(error, pretty: true)} - """) - - redirect(conn, to: ~p"/") - end - end - - @doc """ - This function clears the local session, which includes the session cookie, so the user - will need to authenticate again on the application. - """ - def signout(conn, _params) do - conn - |> log_out_user(:local) - |> live_render(UserManagementLive) - end -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex deleted file mode 100644 index 80c697c..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/live/user_management_live.ex +++ /dev/null @@ -1,170 +0,0 @@ -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} - - @bucket_name "avatars" - - def mount(_params, _session, socket) do - current_user = socket.assigns.current_user - profile = current_user && Profiles.get_profile(id: current_user.id) - account_form = make_account_form(profile, current_user) - - # `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, account_form) - |> assign(:profile, profile) - |> allow_upload(:avatar, - auto_upload: true, - accept: ["image/*"], - progress: &handle_progress/3 - ) - |> assign(:avatar_blob, AsyncResult.loading()) - |> start_async(:download_avatar_blob, fn -> maybe_download_avatar(profile) end)} - end - - def render(assigns) do - ~H""" -
- <.avatar :if={@current_user} upload={@uploads.avatar} size={10} /> - <.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 - current_user = socket.assigns.current_user - params = Map.merge(params, %{"id" => current_user.id}) - - case Profiles.update_profile(params) do - {:ok, profile} -> - Logger.info(""" - [#{__MODULE__}] => Profile updated: #{inspect(profile)} - """) - - account_form = make_account_form(profile, current_user) - {:noreply, assign(socket, :account_form, account_form)} - - {:error, error} -> - Logger.error(""" - [#{__MODULE__}] => Error updating profile: #{inspect(error)} - """) - - {:noreply, put_flash(socket, :error, "Error updating profile")} - end - end - - def handle_event("avatar-blob-url", %{"url" => url}, socket) do - {:noreply, assign(socket, avatar: url)} - end - - def handle_event("sign-out", _params, socket) do - ArcaneWeb.Auth.log_out_user(socket, :local) - {:noreply, socket} - 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 - {:ok, client} = Arcane.Supabase.Client.get_client() - bucket = %Bucket{name: @bucket_name} - - Storage.download_object(client, bucket, profile.avatar_url) - end - - defp make_account_form(profile, current_user) do - to_form(%{ - "id" => profile && profile.id, - "username" => profile && profile.username, - "website" => profile && profile.website, - "email" => current_user && current_user.email, - "avatar" => nil - }) - end - - defp handle_progress(:avatar, entry, socket) when entry.done? do - current_user = socket.assigns.current_user - profile = socket.assigns.profile - params = %{profile: profile, user: current_user} - consume_uploaded_entry(socket, entry, &handle_avatar_upload(&1, params)) - - {:noreply, socket} - end - - defp handle_progress(:avatar, entry, socket) do - Logger.info("[#{__MODULE__}] => Avatar with #{entry.progress} progress") - {:noreply, socket} - end - - defp handle_avatar_upload(%{path: path}, %{user: current_user, profile: profile}) do - bucket = %Bucket{name: @bucket_name} - basename = Path.basename(path) - remote_path = Path.join([bucket.name, current_user.id, basename]) - expires = :timer.hours(24) * 365 - - with {:ok, client} = Arcane.Supabase.Client.get_client(), - {:ok, obj} <- Supabase.Storage.upload_object(client, bucket, remote_path, path), - {:ok, url} <- Supabase.Storage.create_signed_url(client, bucket, remote_path, expires), - {:ok, _} <- Profiles.update_profile(%{id: profile.id, avatar_url: url}) do - {:ok, obj.path} - else - err -> - Logger.error("[#{__MODULE__}] => Failed to upload avatar with #{inspect(err)}") - err - end - end -end diff --git a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex b/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex deleted file mode 100644 index 928c864..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/lib/arcane_web/router.ex +++ /dev/null @@ -1,31 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/mix.lock b/examples/user_managment/phoenix_live_view_user_management/mix.lock deleted file mode 100644 index 579f4b4..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/mix.lock +++ /dev/null @@ -1,44 +0,0 @@ -%{ - "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/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs deleted file mode 100644 index 4bd5582..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125336_create_profiles.exs +++ /dev/null @@ -1,18 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs deleted file mode 100644 index 2be359e..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923125853_create_profiles_policies.exs +++ /dev/null @@ -1,41 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs b/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs deleted file mode 100644 index 64beda7..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/20240923130302_set_up_storage.exs +++ /dev/null @@ -1,27 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/supabase/.gitignore b/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore deleted file mode 100644 index a3ad880..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/supabase/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Supabase -.branches -.temp -.env diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml b/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml deleted file mode 100644 index d8bce59..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/supabase/config.toml +++ /dev/null @@ -1,173 +0,0 @@ -# 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:4000/session/confirm" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:4000"] -# 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 -# We need that for Phoenix since we're using Server Side Rendering -# So we will be doing authentication on the Server -[auth.email.template.magic_link] -subject = "Your Magic Link" -content_path = "./supabase/templates/magic_link.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/user_managment/phoenix_live_view_user_management/supabase/seed.sql b/examples/user_managment/phoenix_live_view_user_management/supabase/seed.sql deleted file mode 100644 index e69de29..0000000 diff --git a/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html b/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html deleted file mode 100644 index f17ac62..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/supabase/templates/magic_link.html +++ /dev/null @@ -1,7 +0,0 @@ -Magic Link - -Follow this link: - - - Log In - \ No newline at end of file diff --git a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs deleted file mode 100644 index 56883a8..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_html_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs b/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs deleted file mode 100644 index 55a99ed..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/error_json_test.exs +++ /dev/null @@ -1,12 +0,0 @@ -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/user_managment/phoenix_live_view_user_management/test/test_helper.exs b/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs deleted file mode 100644 index 65d4d27..0000000 --- a/examples/user_managment/phoenix_live_view_user_management/test/test_helper.exs +++ /dev/null @@ -1,2 +0,0 @@ -ExUnit.start() -Ecto.Adapters.SQL.Sandbox.mode(Arcane.Repo, :manual) diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs b/examples/user_managment/priv/repo/migrations/.formatter.exs similarity index 100% rename from examples/user_managment/phoenix_live_view_user_management/priv/repo/migrations/.formatter.exs rename to examples/user_managment/priv/repo/migrations/.formatter.exs diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs b/examples/user_managment/priv/repo/seeds.exs similarity index 67% rename from examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs rename to examples/user_managment/priv/repo/seeds.exs index 95c2555..240fa10 100644 --- a/examples/user_managment/phoenix_live_view_user_management/priv/repo/seeds.exs +++ b/examples/user_managment/priv/repo/seeds.exs @@ -5,10 +5,7 @@ # Inside the script, you can read and write to any of your # repositories directly: # -# Arcane.Repo.insert!(%Arcane.SomeSchema{}) +# UserManagement.Repo.insert!(%UserManagement.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/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico b/examples/user_managment/priv/static/favicon.ico similarity index 100% rename from examples/user_managment/phoenix_live_view_user_management/priv/static/favicon.ico rename to examples/user_managment/priv/static/favicon.ico diff --git a/examples/user_managment/priv/static/images/logo.svg b/examples/user_managment/priv/static/images/logo.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/examples/user_managment/priv/static/images/logo.svg @@ -0,0 +1,6 @@ + diff --git a/examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt b/examples/user_managment/priv/static/robots.txt similarity index 100% rename from examples/user_managment/phoenix_live_view_user_management/priv/static/robots.txt rename to examples/user_managment/priv/static/robots.txt diff --git a/examples/user_managment/rel/overlays/bin/migrate b/examples/user_managment/rel/overlays/bin/migrate new file mode 100755 index 0000000..d903377 --- /dev/null +++ b/examples/user_managment/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./user_management eval UserManagement.Release.migrate diff --git a/examples/user_managment/rel/overlays/bin/migrate.bat b/examples/user_managment/rel/overlays/bin/migrate.bat new file mode 100755 index 0000000..141f091 --- /dev/null +++ b/examples/user_managment/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\user_management" eval UserManagement.Release.migrate diff --git a/examples/user_managment/rel/overlays/bin/server b/examples/user_managment/rel/overlays/bin/server new file mode 100755 index 0000000..97729ea --- /dev/null +++ b/examples/user_managment/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./user_management start diff --git a/examples/user_managment/rel/overlays/bin/server.bat b/examples/user_managment/rel/overlays/bin/server.bat new file mode 100755 index 0000000..3aedea7 --- /dev/null +++ b/examples/user_managment/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\user_management" start diff --git a/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex b/examples/user_managment/test/support/conn_case.ex similarity index 75% rename from examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex rename to examples/user_managment/test/support/conn_case.ex index 72b939a..325b8ba 100644 --- a/examples/user_managment/phoenix_live_view_user_management/test/support/conn_case.ex +++ b/examples/user_managment/test/support/conn_case.ex @@ -1,4 +1,4 @@ -defmodule ArcaneWeb.ConnCase do +defmodule UserManagementWeb.ConnCase do @moduledoc """ This module defines the test case to be used by tests that require setting up a connection. @@ -11,7 +11,7 @@ defmodule ArcaneWeb.ConnCase do 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 + by setting `use UserManagementWeb.ConnCase, async: true`, although this option is not recommended for other databases. """ @@ -20,19 +20,19 @@ defmodule ArcaneWeb.ConnCase do using do quote do # The default endpoint for testing - @endpoint ArcaneWeb.Endpoint + @endpoint UserManagementWeb.Endpoint - use ArcaneWeb, :verified_routes + use UserManagementWeb, :verified_routes # Import conveniences for testing with connections import Plug.Conn import Phoenix.ConnTest - import ArcaneWeb.ConnCase + import UserManagementWeb.ConnCase end end setup tags do - Arcane.DataCase.setup_sandbox(tags) + UserManagement.DataCase.setup_sandbox(tags) {:ok, conn: Phoenix.ConnTest.build_conn()} end end diff --git a/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex b/examples/user_managment/test/support/data_case.ex similarity index 81% rename from examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex rename to examples/user_managment/test/support/data_case.ex index cb96496..eb8fc17 100644 --- a/examples/user_managment/phoenix_live_view_user_management/test/support/data_case.ex +++ b/examples/user_managment/test/support/data_case.ex @@ -1,4 +1,4 @@ -defmodule Arcane.DataCase do +defmodule UserManagement.DataCase do @moduledoc """ This module defines the setup for tests requiring access to the application's data layer. @@ -10,7 +10,7 @@ defmodule Arcane.DataCase do 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 + by setting `use UserManagement.DataCase, async: true`, although this option is not recommended for other databases. """ @@ -18,17 +18,17 @@ defmodule Arcane.DataCase do using do quote do - alias Arcane.Repo + alias UserManagement.Repo import Ecto import Ecto.Changeset import Ecto.Query - import Arcane.DataCase + import UserManagement.DataCase end end setup tags do - Arcane.DataCase.setup_sandbox(tags) + UserManagement.DataCase.setup_sandbox(tags) :ok end @@ -36,7 +36,7 @@ defmodule Arcane.DataCase do 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]) + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(UserManagement.Repo, shared: not tags[:async]) on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) end diff --git a/examples/user_managment/test/test_helper.exs b/examples/user_managment/test/test_helper.exs new file mode 100644 index 0000000..8b99c88 --- /dev/null +++ b/examples/user_managment/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(UserManagement.Repo, :manual) diff --git a/examples/user_managment/test/user_management_web/controllers/error_html_test.exs b/examples/user_managment/test/user_management_web/controllers/error_html_test.exs new file mode 100644 index 0000000..3fccfb8 --- /dev/null +++ b/examples/user_managment/test/user_management_web/controllers/error_html_test.exs @@ -0,0 +1,15 @@ +defmodule UserManagementWeb.ErrorHTMLTest do + use UserManagementWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template + + test "renders 404.html" do + assert render_to_string(UserManagementWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(UserManagementWeb.ErrorHTML, "500", "html", []) == + "Internal Server Error" + end +end diff --git a/examples/user_managment/test/user_management_web/controllers/error_json_test.exs b/examples/user_managment/test/user_management_web/controllers/error_json_test.exs new file mode 100644 index 0000000..01f4ff9 --- /dev/null +++ b/examples/user_managment/test/user_management_web/controllers/error_json_test.exs @@ -0,0 +1,14 @@ +defmodule UserManagementWeb.ErrorJSONTest do + use UserManagementWeb.ConnCase, async: true + + test "renders 404" do + assert UserManagementWeb.ErrorJSON.render("404.json", %{}) == %{ + errors: %{detail: "Not Found"} + } + end + + test "renders 500" do + assert UserManagementWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs b/examples/user_managment/test/user_management_web/controllers/page_controller_test.exs similarity index 65% rename from examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs rename to examples/user_managment/test/user_management_web/controllers/page_controller_test.exs index 3a184f5..473c950 100644 --- a/examples/user_managment/phoenix_live_view_user_management/test/arcane_web/controllers/page_controller_test.exs +++ b/examples/user_managment/test/user_management_web/controllers/page_controller_test.exs @@ -1,5 +1,5 @@ -defmodule ArcaneWeb.PageControllerTest do - use ArcaneWeb.ConnCase +defmodule UserManagementWeb.PageControllerTest do + use UserManagementWeb.ConnCase test "GET /", %{conn: conn} do conn = get(conn, ~p"/") From e19061e0bbca6441396ec10b54e96edb4de10ae7 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 21:59:37 -0300 Subject: [PATCH 11/27] basic supabase-ex setup --- examples/user_managment/config/runtime.exs | 4 ++++ .../user_managment/lib/user_management/application.ex | 3 ++- examples/user_managment/lib/user_management/supabase.ex | 3 +++ examples/user_managment/mix.exs | 3 +++ examples/user_managment/mix.lock | 8 ++++++++ 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 examples/user_managment/lib/user_management/supabase.ex diff --git a/examples/user_managment/config/runtime.exs b/examples/user_managment/config/runtime.exs index 7d0cb8d..ff5763d 100644 --- a/examples/user_managment/config/runtime.exs +++ b/examples/user_managment/config/runtime.exs @@ -20,6 +20,10 @@ if System.get_env("PHX_SERVER") do config :user_management, UserManagementWeb.Endpoint, server: true end +config :user_management, UserManagement.Supabase, + base_url: System.fetch_env!("SUPABASE_URL"), + api_key: System.fetch_env!("SUPABASE_KEY") + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || diff --git a/examples/user_managment/lib/user_management/application.ex b/examples/user_managment/lib/user_management/application.ex index 8011142..e79653d 100644 --- a/examples/user_managment/lib/user_management/application.ex +++ b/examples/user_managment/lib/user_management/application.ex @@ -15,7 +15,8 @@ defmodule UserManagement.Application do # Start a worker by calling: UserManagement.Worker.start_link(arg) # {UserManagement.Worker, arg}, # Start to serve requests, typically the last entry - UserManagementWeb.Endpoint + UserManagementWeb.Endpoint, + UserManagement.Supabase ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/examples/user_managment/lib/user_management/supabase.ex b/examples/user_managment/lib/user_management/supabase.ex new file mode 100644 index 0000000..7a262db --- /dev/null +++ b/examples/user_managment/lib/user_management/supabase.ex @@ -0,0 +1,3 @@ +defmodule UserManagement.Supabase do + use Supabase.Client, otp_app: :user_management +end diff --git a/examples/user_managment/mix.exs b/examples/user_managment/mix.exs index d160d9c..5c5868b 100644 --- a/examples/user_managment/mix.exs +++ b/examples/user_managment/mix.exs @@ -42,6 +42,9 @@ defmodule UserManagement.MixProject do {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev}, + {:supabase_potion, "~> 0.6"}, + {:supabase_gotrue, "~> 0.5"}, + {:supabase_storage, "~> 0.4"}, {:heroicons, github: "tailwindlabs/heroicons", tag: "v2.1.1", diff --git a/examples/user_managment/mix.lock b/examples/user_managment/mix.lock index 9784c12..bfb20e9 100644 --- a/examples/user_managment/mix.lock +++ b/examples/user_managment/mix.lock @@ -8,11 +8,16 @@ "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [: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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "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.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [: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", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "peri": {:hex, :peri, "0.3.2", "79a2a91d6947d8bb7ed2d28911904e1dc20c0fe3257c3a86b010c833bb35acda", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "900b8d7a9cf4d32be98e8f84e2b720c40133d0477e78e83e41d81e0fe00d9b18"}, "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [: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", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [: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", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, @@ -23,6 +28,9 @@ "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [: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", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.0", "1e553994dbb1d3bbb0f16003f1a4a51ff3c82c800ae82a1043ce4b04ea7a4eea", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "11cd4cb26e6bf2837250fd1b0ec5982c333791693b432523a908cbb0be5204f8"}, + "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, + "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, From 93bd5ff97cd839581faf708c41253cf173321ce0 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:00:24 -0300 Subject: [PATCH 12/27] rename typo folder --- .../.formatter.exs | 0 .../{user_managment => user_management}/.gitignore | 0 .../{user_managment => user_management}/README.md | 0 .../assets/css/app.css | 0 .../assets/js/app.js | 0 .../assets/tailwind.config.js | 0 .../assets/vendor/topbar.js | 0 .../config/config.exs | 0 .../config/dev.exs | 0 .../config/prod.exs | 0 .../config/runtime.exs | 0 .../config/test.exs | 0 .../lib/user_management.ex | 0 .../lib/user_management/application.ex | 0 .../lib/user_management/release.ex | 0 .../lib/user_management/repo.ex | 0 .../lib/user_management/supabase.ex | 0 .../lib/user_management_web.ex | 0 .../components/core_components.ex | 0 .../lib/user_management_web/components/layouts.ex | 0 .../components/layouts/app.html.heex | 0 .../components/layouts/root.html.heex | 0 .../user_management_web/controllers/error_html.ex | 0 .../user_management_web/controllers/error_json.ex | 0 .../lib/user_management_web/endpoint.ex | 0 .../lib/user_management_web/router.ex | 0 .../lib/user_management_web/telemetry.ex | 0 .../{user_managment => user_management}/mix.exs | 0 .../{user_managment => user_management}/mix.lock | 0 .../priv/repo/migrations/.formatter.exs | 0 .../priv/repo/seeds.exs | 0 .../priv/static/favicon.ico | Bin .../priv/static/images/logo.svg | 0 .../priv/static/robots.txt | 0 .../rel/overlays/bin/migrate | 0 .../rel/overlays/bin/migrate.bat | 0 .../rel/overlays/bin/server | 0 .../rel/overlays/bin/server.bat | 0 .../test/support/conn_case.ex | 0 .../test/support/data_case.ex | 0 .../test/test_helper.exs | 0 .../controllers/error_html_test.exs | 0 .../controllers/error_json_test.exs | 0 .../controllers/page_controller_test.exs | 0 44 files changed, 0 insertions(+), 0 deletions(-) rename examples/{user_managment => user_management}/.formatter.exs (100%) rename examples/{user_managment => user_management}/.gitignore (100%) rename examples/{user_managment => user_management}/README.md (100%) rename examples/{user_managment => user_management}/assets/css/app.css (100%) rename examples/{user_managment => user_management}/assets/js/app.js (100%) rename examples/{user_managment => user_management}/assets/tailwind.config.js (100%) rename examples/{user_managment => user_management}/assets/vendor/topbar.js (100%) rename examples/{user_managment => user_management}/config/config.exs (100%) rename examples/{user_managment => user_management}/config/dev.exs (100%) rename examples/{user_managment => user_management}/config/prod.exs (100%) rename examples/{user_managment => user_management}/config/runtime.exs (100%) rename examples/{user_managment => user_management}/config/test.exs (100%) rename examples/{user_managment => user_management}/lib/user_management.ex (100%) rename examples/{user_managment => user_management}/lib/user_management/application.ex (100%) rename examples/{user_managment => user_management}/lib/user_management/release.ex (100%) rename examples/{user_managment => user_management}/lib/user_management/repo.ex (100%) rename examples/{user_managment => user_management}/lib/user_management/supabase.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/components/core_components.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/components/layouts.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/components/layouts/app.html.heex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/components/layouts/root.html.heex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/controllers/error_html.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/controllers/error_json.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/endpoint.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/router.ex (100%) rename examples/{user_managment => user_management}/lib/user_management_web/telemetry.ex (100%) rename examples/{user_managment => user_management}/mix.exs (100%) rename examples/{user_managment => user_management}/mix.lock (100%) rename examples/{user_managment => user_management}/priv/repo/migrations/.formatter.exs (100%) rename examples/{user_managment => user_management}/priv/repo/seeds.exs (100%) rename examples/{user_managment => user_management}/priv/static/favicon.ico (100%) rename examples/{user_managment => user_management}/priv/static/images/logo.svg (100%) rename examples/{user_managment => user_management}/priv/static/robots.txt (100%) rename examples/{user_managment => user_management}/rel/overlays/bin/migrate (100%) rename examples/{user_managment => user_management}/rel/overlays/bin/migrate.bat (100%) rename examples/{user_managment => user_management}/rel/overlays/bin/server (100%) rename examples/{user_managment => user_management}/rel/overlays/bin/server.bat (100%) rename examples/{user_managment => user_management}/test/support/conn_case.ex (100%) rename examples/{user_managment => user_management}/test/support/data_case.ex (100%) rename examples/{user_managment => user_management}/test/test_helper.exs (100%) rename examples/{user_managment => user_management}/test/user_management_web/controllers/error_html_test.exs (100%) rename examples/{user_managment => user_management}/test/user_management_web/controllers/error_json_test.exs (100%) rename examples/{user_managment => user_management}/test/user_management_web/controllers/page_controller_test.exs (100%) diff --git a/examples/user_managment/.formatter.exs b/examples/user_management/.formatter.exs similarity index 100% rename from examples/user_managment/.formatter.exs rename to examples/user_management/.formatter.exs diff --git a/examples/user_managment/.gitignore b/examples/user_management/.gitignore similarity index 100% rename from examples/user_managment/.gitignore rename to examples/user_management/.gitignore diff --git a/examples/user_managment/README.md b/examples/user_management/README.md similarity index 100% rename from examples/user_managment/README.md rename to examples/user_management/README.md diff --git a/examples/user_managment/assets/css/app.css b/examples/user_management/assets/css/app.css similarity index 100% rename from examples/user_managment/assets/css/app.css rename to examples/user_management/assets/css/app.css diff --git a/examples/user_managment/assets/js/app.js b/examples/user_management/assets/js/app.js similarity index 100% rename from examples/user_managment/assets/js/app.js rename to examples/user_management/assets/js/app.js diff --git a/examples/user_managment/assets/tailwind.config.js b/examples/user_management/assets/tailwind.config.js similarity index 100% rename from examples/user_managment/assets/tailwind.config.js rename to examples/user_management/assets/tailwind.config.js diff --git a/examples/user_managment/assets/vendor/topbar.js b/examples/user_management/assets/vendor/topbar.js similarity index 100% rename from examples/user_managment/assets/vendor/topbar.js rename to examples/user_management/assets/vendor/topbar.js diff --git a/examples/user_managment/config/config.exs b/examples/user_management/config/config.exs similarity index 100% rename from examples/user_managment/config/config.exs rename to examples/user_management/config/config.exs diff --git a/examples/user_managment/config/dev.exs b/examples/user_management/config/dev.exs similarity index 100% rename from examples/user_managment/config/dev.exs rename to examples/user_management/config/dev.exs diff --git a/examples/user_managment/config/prod.exs b/examples/user_management/config/prod.exs similarity index 100% rename from examples/user_managment/config/prod.exs rename to examples/user_management/config/prod.exs diff --git a/examples/user_managment/config/runtime.exs b/examples/user_management/config/runtime.exs similarity index 100% rename from examples/user_managment/config/runtime.exs rename to examples/user_management/config/runtime.exs diff --git a/examples/user_managment/config/test.exs b/examples/user_management/config/test.exs similarity index 100% rename from examples/user_managment/config/test.exs rename to examples/user_management/config/test.exs diff --git a/examples/user_managment/lib/user_management.ex b/examples/user_management/lib/user_management.ex similarity index 100% rename from examples/user_managment/lib/user_management.ex rename to examples/user_management/lib/user_management.ex diff --git a/examples/user_managment/lib/user_management/application.ex b/examples/user_management/lib/user_management/application.ex similarity index 100% rename from examples/user_managment/lib/user_management/application.ex rename to examples/user_management/lib/user_management/application.ex diff --git a/examples/user_managment/lib/user_management/release.ex b/examples/user_management/lib/user_management/release.ex similarity index 100% rename from examples/user_managment/lib/user_management/release.ex rename to examples/user_management/lib/user_management/release.ex diff --git a/examples/user_managment/lib/user_management/repo.ex b/examples/user_management/lib/user_management/repo.ex similarity index 100% rename from examples/user_managment/lib/user_management/repo.ex rename to examples/user_management/lib/user_management/repo.ex diff --git a/examples/user_managment/lib/user_management/supabase.ex b/examples/user_management/lib/user_management/supabase.ex similarity index 100% rename from examples/user_managment/lib/user_management/supabase.ex rename to examples/user_management/lib/user_management/supabase.ex diff --git a/examples/user_managment/lib/user_management_web.ex b/examples/user_management/lib/user_management_web.ex similarity index 100% rename from examples/user_managment/lib/user_management_web.ex rename to examples/user_management/lib/user_management_web.ex diff --git a/examples/user_managment/lib/user_management_web/components/core_components.ex b/examples/user_management/lib/user_management_web/components/core_components.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/components/core_components.ex rename to examples/user_management/lib/user_management_web/components/core_components.ex diff --git a/examples/user_managment/lib/user_management_web/components/layouts.ex b/examples/user_management/lib/user_management_web/components/layouts.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/components/layouts.ex rename to examples/user_management/lib/user_management_web/components/layouts.ex diff --git a/examples/user_managment/lib/user_management_web/components/layouts/app.html.heex b/examples/user_management/lib/user_management_web/components/layouts/app.html.heex similarity index 100% rename from examples/user_managment/lib/user_management_web/components/layouts/app.html.heex rename to examples/user_management/lib/user_management_web/components/layouts/app.html.heex diff --git a/examples/user_managment/lib/user_management_web/components/layouts/root.html.heex b/examples/user_management/lib/user_management_web/components/layouts/root.html.heex similarity index 100% rename from examples/user_managment/lib/user_management_web/components/layouts/root.html.heex rename to examples/user_management/lib/user_management_web/components/layouts/root.html.heex diff --git a/examples/user_managment/lib/user_management_web/controllers/error_html.ex b/examples/user_management/lib/user_management_web/controllers/error_html.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/controllers/error_html.ex rename to examples/user_management/lib/user_management_web/controllers/error_html.ex diff --git a/examples/user_managment/lib/user_management_web/controllers/error_json.ex b/examples/user_management/lib/user_management_web/controllers/error_json.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/controllers/error_json.ex rename to examples/user_management/lib/user_management_web/controllers/error_json.ex diff --git a/examples/user_managment/lib/user_management_web/endpoint.ex b/examples/user_management/lib/user_management_web/endpoint.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/endpoint.ex rename to examples/user_management/lib/user_management_web/endpoint.ex diff --git a/examples/user_managment/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/router.ex rename to examples/user_management/lib/user_management_web/router.ex diff --git a/examples/user_managment/lib/user_management_web/telemetry.ex b/examples/user_management/lib/user_management_web/telemetry.ex similarity index 100% rename from examples/user_managment/lib/user_management_web/telemetry.ex rename to examples/user_management/lib/user_management_web/telemetry.ex diff --git a/examples/user_managment/mix.exs b/examples/user_management/mix.exs similarity index 100% rename from examples/user_managment/mix.exs rename to examples/user_management/mix.exs diff --git a/examples/user_managment/mix.lock b/examples/user_management/mix.lock similarity index 100% rename from examples/user_managment/mix.lock rename to examples/user_management/mix.lock diff --git a/examples/user_managment/priv/repo/migrations/.formatter.exs b/examples/user_management/priv/repo/migrations/.formatter.exs similarity index 100% rename from examples/user_managment/priv/repo/migrations/.formatter.exs rename to examples/user_management/priv/repo/migrations/.formatter.exs diff --git a/examples/user_managment/priv/repo/seeds.exs b/examples/user_management/priv/repo/seeds.exs similarity index 100% rename from examples/user_managment/priv/repo/seeds.exs rename to examples/user_management/priv/repo/seeds.exs diff --git a/examples/user_managment/priv/static/favicon.ico b/examples/user_management/priv/static/favicon.ico similarity index 100% rename from examples/user_managment/priv/static/favicon.ico rename to examples/user_management/priv/static/favicon.ico diff --git a/examples/user_managment/priv/static/images/logo.svg b/examples/user_management/priv/static/images/logo.svg similarity index 100% rename from examples/user_managment/priv/static/images/logo.svg rename to examples/user_management/priv/static/images/logo.svg diff --git a/examples/user_managment/priv/static/robots.txt b/examples/user_management/priv/static/robots.txt similarity index 100% rename from examples/user_managment/priv/static/robots.txt rename to examples/user_management/priv/static/robots.txt diff --git a/examples/user_managment/rel/overlays/bin/migrate b/examples/user_management/rel/overlays/bin/migrate similarity index 100% rename from examples/user_managment/rel/overlays/bin/migrate rename to examples/user_management/rel/overlays/bin/migrate diff --git a/examples/user_managment/rel/overlays/bin/migrate.bat b/examples/user_management/rel/overlays/bin/migrate.bat similarity index 100% rename from examples/user_managment/rel/overlays/bin/migrate.bat rename to examples/user_management/rel/overlays/bin/migrate.bat diff --git a/examples/user_managment/rel/overlays/bin/server b/examples/user_management/rel/overlays/bin/server similarity index 100% rename from examples/user_managment/rel/overlays/bin/server rename to examples/user_management/rel/overlays/bin/server diff --git a/examples/user_managment/rel/overlays/bin/server.bat b/examples/user_management/rel/overlays/bin/server.bat similarity index 100% rename from examples/user_managment/rel/overlays/bin/server.bat rename to examples/user_management/rel/overlays/bin/server.bat diff --git a/examples/user_managment/test/support/conn_case.ex b/examples/user_management/test/support/conn_case.ex similarity index 100% rename from examples/user_managment/test/support/conn_case.ex rename to examples/user_management/test/support/conn_case.ex diff --git a/examples/user_managment/test/support/data_case.ex b/examples/user_management/test/support/data_case.ex similarity index 100% rename from examples/user_managment/test/support/data_case.ex rename to examples/user_management/test/support/data_case.ex diff --git a/examples/user_managment/test/test_helper.exs b/examples/user_management/test/test_helper.exs similarity index 100% rename from examples/user_managment/test/test_helper.exs rename to examples/user_management/test/test_helper.exs diff --git a/examples/user_managment/test/user_management_web/controllers/error_html_test.exs b/examples/user_management/test/user_management_web/controllers/error_html_test.exs similarity index 100% rename from examples/user_managment/test/user_management_web/controllers/error_html_test.exs rename to examples/user_management/test/user_management_web/controllers/error_html_test.exs diff --git a/examples/user_managment/test/user_management_web/controllers/error_json_test.exs b/examples/user_management/test/user_management_web/controllers/error_json_test.exs similarity index 100% rename from examples/user_managment/test/user_management_web/controllers/error_json_test.exs rename to examples/user_management/test/user_management_web/controllers/error_json_test.exs diff --git a/examples/user_managment/test/user_management_web/controllers/page_controller_test.exs b/examples/user_management/test/user_management_web/controllers/page_controller_test.exs similarity index 100% rename from examples/user_managment/test/user_management_web/controllers/page_controller_test.exs rename to examples/user_management/test/user_management_web/controllers/page_controller_test.exs From 3c52a97626099c54769a6f037f6882f400e0f423 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:13:31 -0300 Subject: [PATCH 13/27] Configure Supabase integration with auth module --- examples/user_management/config/runtime.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/user_management/config/runtime.exs b/examples/user_management/config/runtime.exs index ff5763d..3516f3b 100644 --- a/examples/user_management/config/runtime.exs +++ b/examples/user_management/config/runtime.exs @@ -24,6 +24,8 @@ config :user_management, UserManagement.Supabase, base_url: System.fetch_env!("SUPABASE_URL"), api_key: System.fetch_env!("SUPABASE_KEY") +config :supabase_gotrue, auth_module: UserManagementWeb.Auth + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || From 012b06d3d1aa3aade4ed2962b8e3018e93c4064b Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:38:35 -0300 Subject: [PATCH 14/27] feat: generate live view with supabase-gotrue --- .../controllers/session_controller.ex | 37 +++ .../user_management_web/live/login_live.ex | 99 +++++++ .../live/registration_live.ex | 81 ++++++ .../lib/user_management_web/router.ex | 30 ++- .../lib/user_management_web/user_auth.ex | 254 ++++++++++++++++++ examples/user_management/mix.lock | 2 +- .../user_management/test/support/conn_case.ex | 25 ++ 7 files changed, 523 insertions(+), 5 deletions(-) create mode 100644 examples/user_management/lib/user_management_web/controllers/session_controller.ex create mode 100644 examples/user_management/lib/user_management_web/live/login_live.ex create mode 100644 examples/user_management/lib/user_management_web/live/registration_live.ex create mode 100644 examples/user_management/lib/user_management_web/user_auth.ex diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex new file mode 100644 index 0000000..1f787cc --- /dev/null +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -0,0 +1,37 @@ +defmodule UserManagementWeb.SessionController do + use UserManagementWeb, :controller + + def create(conn, %{"_action" => "confirmed"} = params) do + create(conn, params, "User confirmed successfully.") + end + + def create(conn, params) do + create(conn, params, "Welcome back!") + end + + defp create(conn, params, info) do + with {:ok, conn} <- log_in_with_strategy(conn, params) do + put_flash(conn, :info, info) + else + _ -> + conn + |> put_flash(:error, "Invalid credentials") + |> redirect(to: ~p"/login") + end + end + + def token(conn, %{"token" => token} = params) do + create(conn, Map.put(params, "token", token)) + end + + def delete(conn, _params) do + conn + |> put_flash(:info, "Logged out successfully.") + |> UserManagementWeb.UserAuth.log_out_user(:global) + end + + def log_in_with_strategy(conn, %{"user" => %{"token" => token}}) + when is_binary(token) do + UserManagementWeb.UserAuth.log_in_user_with_otp(conn, %{"token" => token}) + end +end diff --git a/examples/user_management/lib/user_management_web/live/login_live.ex b/examples/user_management/lib/user_management_web/live/login_live.ex new file mode 100644 index 0000000..d13511f --- /dev/null +++ b/examples/user_management/lib/user_management_web/live/login_live.ex @@ -0,0 +1,99 @@ +defmodule UserManagementWeb.LoginLive do + use UserManagementWeb, :live_view + + @impl true + def render(assigns) do + ~H""" +
+ <.header class="text-center"> +

Log in

+ <:subtitle> + Don't have an account? + <.link navigate={~p"/register"} class="font-semibold text-brand hover:underline"> + Sign up + + for an account now. + + + + <.login_form :for={s <- ["otp"]} strategy={s} form={@form} /> +
+ """ + end + + def login_form(%{strategy: "password"} = assigns) do + ~H""" + <.form :let={f} for={@form} id="login_form_password" action={~p"/login"} as={:user}> + <.input field={f[:email]} type="email" label="Email" autocomplete="username" required /> + <.input + field={f[:password]} + type="password" + label="Password" + autocomplete="current-password" + required + /> + <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" /> + <.button class="w-full"> + Log in with password + + + """ + end + + def login_form(%{strategy: "otp"} = assigns) do + ~H""" + <.form :let={f} for={@form} id="login_form_otp" action={~p"/login"} as={:user}> + <.input field={f[:email]} type="email" label="Email" required /> + <.button class="w-full"> + Send one-time password + + + """ + end + + def login_form(%{strategy: "oauth"} = assigns) do + ~H""" +
+

Log in with a provider

+
+ <.button + :for={provider <- ["github", "google", "facebook"]} + class="w-full" + phx-click="oauth_login" + phx-value-provider={provider} + > + {provider} + +
+
+ """ + end + + def login_form(%{strategy: "anon"} = assigns) do + ~H""" + <.form for={%{}} id="login_form_anon" action={~p"/login"} as={:user}> + <.button class="w-full"> + Continue anonymously + + + """ + end + + def login_form(%{strategy: _} = assigns) do + ~H""" +

Strategy not implemented in LiveView yet

+ """ + end + + @impl true + def mount(_params, _session, socket) do + form = to_form(%{"email" => nil}, as: "user") + {:ok, assign(socket, form: form)} + end + + @impl true + def handle_event("oauth_login", %{"provider" => provider}, socket) do + # This would be handled by the controller + {:noreply, push_navigate(socket, to: ~p"/login?provider=#{provider}")} + end +end diff --git a/examples/user_management/lib/user_management_web/live/registration_live.ex b/examples/user_management/lib/user_management_web/live/registration_live.ex new file mode 100644 index 0000000..f5bd794 --- /dev/null +++ b/examples/user_management/lib/user_management_web/live/registration_live.ex @@ -0,0 +1,81 @@ +defmodule UserManagementWeb.RegistrationLive do + use UserManagementWeb, :live_view + + @impl true + def render(assigns) do + ~H""" +
+ <.header class="text-center"> + Register + <:subtitle> + Already have an account? + <.link navigate={~p"/login"} class="font-semibold text-brand hover:underline"> + Sign in + + to your account now. + + + + <.simple_form + for={@form} + id="registration_form" + phx-submit="save" + phx-change="validate" + as={:user} + > + <.error :if={@check_errors}> + Oops, something went wrong! Please check the errors below. + + + <.input field={@form[:email]} type="email" label="Email" required /> + <.input field={@form[:password]} type="password" label="Password" required /> + + <:actions> + <.button phx-disable-with="Creating account..." class="w-full"> + Create an account + + + +
+ """ + end + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(check_errors: false) + |> assign_form()} + end + + @impl true + def handle_event("save", %{"user" => user_params}, socket) do + {:ok, client} = UserManagementWeb.UserAuth.get_client() + %{"email" => email, "password" => password} = user_params + + case Supabase.GoTrue.sign_up(client, %{email: email, password: password}) do + {:ok, _session} -> + {:noreply, + socket + |> put_flash(:info, "User created successfully. Please sign in.") + |> push_navigate(to: ~p"/login")} + + {:error, %Supabase.Error{metadata: metadata}} -> + message = get_in(metadata, [:resp_body, "msg"]) + + {:noreply, + socket + |> put_flash(:error, "Registration failed: #{message}") + |> assign(check_errors: true) + |> assign_form()} + end + end + + def handle_event("validate", %{"user" => user_params}, socket) do + {:noreply, assign_form(socket, user_params)} + end + + defp assign_form(socket, user_params \\ %{}) do + assign(socket, :form, to_form(user_params, as: "user")) + end +end diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index afb2d57..4c5edc4 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -1,6 +1,9 @@ defmodule UserManagementWeb.Router do use UserManagementWeb, :router + # Import authentication plugs + import UserManagementWeb.UserAuth + pipeline :browser do plug :accepts, ["html"] plug :fetch_session @@ -8,14 +11,33 @@ defmodule UserManagementWeb.Router do plug :put_root_layout, html: {UserManagementWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers + plug :fetch_current_user end pipeline :api do plug :accepts, ["json"] end - # Other scopes may use custom stacks. - # scope "/api", UserManagementWeb do - # pipe_through :api - # end + ## Authentication routes + scope "/", UserManagementWeb do + pipe_through [:browser, :require_authenticated_user] + + delete "/logout", SessionController, :delete + end + + scope "/", UserManagementWeb do + pipe_through [:browser, :redirect_if_user_is_authenticated] + + live_session :current_user, + on_mount: [ + {UserManagementWeb.UserAuth, :mount_current_user}, + {UserManagementWeb.UserAuth, :redirect_if_user_is_authenticated} + ] do + live "/login", LoginLive, :new + live "/register", RegistrationLive, :new + end + + post "/login", SessionController, :create + post "/login/:token", SessionController, :token + end end diff --git a/examples/user_management/lib/user_management_web/user_auth.ex b/examples/user_management/lib/user_management_web/user_auth.ex new file mode 100644 index 0000000..53459b7 --- /dev/null +++ b/examples/user_management/lib/user_management_web/user_auth.ex @@ -0,0 +1,254 @@ +defmodule UserManagementWeb.UserAuth do + use UserManagementWeb, :verified_routes + + import Plug.Conn + import Phoenix.Controller + + alias Supabase.GoTrue + alias Supabase.GoTrue.Admin + alias Supabase.GoTrue.Session + alias Supabase.GoTrue.User + + # Make the remember me cookie valid for 60 days. + # If you want bump or reduce this value, also change + # the token expiry itself in #{User} Token. + @max_age 60 * 60 * 24 * 60 + @remember_me_cookie "_user_management_web_user_remember_me" + @remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"] + + @extra_login_doc """ + It renews the session ID and clears the whole session + to avoid fixation attacks. See the renew_session + function to customize this behaviour. + + It also sets a `:live_socket_id` key in the session, + so LiveView sessions are identified and automatically + disconnected on log out. The line can be safely removed + if you are not using LiveView. + """ + + @doc "Logs the User in using the otp strategy.\n" <> @extra_login_doc + def log_in_user_with_otp(conn, params \\ %{}) do + with {:ok, client} <- get_client(), + {:ok, session} <- GoTrue.sign_in_with_otp(client, params) do + do_login(conn, session, params) + end + end + + defp do_login(conn, session, params) do + user_return_to = get_session(conn, :user_return_to) + + conn + |> renew_session() + |> put_token_in_session(session.access_token) + |> maybe_write_remember_me_cookie(session, params) + |> redirect(to: user_return_to || signed_in_path()) + end + + defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do + put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) + end + + defp maybe_write_remember_me_cookie(conn, _token, _params) do + conn + end + + # This function renews the session ID and erases the whole + # session to avoid fixation attacks. If there is any data + # in the session you may want to preserve after log in/log out, + # you must explicitly fetch the session data before clearing + # and then immediately set it after clearing, for example: + # + # defp renew_session(conn) do + # preferred_locale = get_session(conn, :preferred_locale) + # + # conn + # |> configure_session(renew: true) + # |> clear_session() + # |> put_session(:preferred_locale, preferred_locale) + # end + # + defp renew_session(conn) do + conn + |> configure_session(renew: true) + |> clear_session() + end + + @doc """ + Logs the user out. + + It clears all session data for safety. See renew_session. + """ + def log_out_user(conn, scope) do + {:ok, client} = get_client() + user_token = get_session(conn, :user_token) + session = %Session{access_token: user_token} + user_token && Admin.sign_out(client, session, scope) + + live_socket_id = get_session(conn, :live_socket_id) + + if live_socket_id do + UserManagementWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) + end + + conn + |> renew_session() + |> delete_resp_cookie(@remember_me_cookie) + |> redirect(to: ~p"/") + end + + @doc "Authenticates the #{User} by looking into the session and remember me token." + def fetch_current_user(conn, _opts) do + {user_token, conn} = ensure_user_token(conn) + user = user_token && fetch_user_from_session_token(user_token) + assign(conn, :current_user, user) + end + + defp fetch_user_from_session_token(user_token) do + {:ok, client} = get_client() + + case GoTrue.get_user(client, %Session{access_token: user_token}) do + {:ok, %User{} = user} -> user + _ -> nil + end + end + + defp ensure_user_token(conn) do + if token = get_session(conn, :user_token) do + {token, conn} + else + conn = fetch_cookies(conn, signed: [@remember_me_cookie]) + + if token = conn.cookies[@remember_me_cookie] do + {token, put_token_in_session(conn, token)} + else + {nil, conn} + end + end + end + + @doc """ + Handles mounting and authenticating the current_user in LiveViews. + + ## `on_mount` arguments + + * `:mount_current_user` - Assigns current_user + to socket assigns based on user_token, or nil if + there's no user_token or no matching user. + + * `:ensure_authenticated` - Authenticates the user from the session, + and assigns the current_user to socket assigns based + on user_token. + Redirects to login page if there's no logged user. + + * `:redirect_if_user_is_authenticated` - Authenticates the user from the session. + Redirects to signed_in_path if there's a logged user. + + ## Examples + + Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate + the current_user: + + defmodule UserManagementWeb.PageLive do + use UserManagementWeb, :live_view + + on_mount {UserManagementWeb.UserAuth, :mount_current_user} + ... + end + + Or use the `live_session` of your router to invoke the on_mount callback: + + live_session :authenticated, on_mount: [{UserManagementWeb.UserAuth, :ensure_authenticated}] do + live "/profile", ProfileLive, :index + end + """ + def on_mount(:mount_current_user, _params, session, socket) do + {:cont, mount_current_user(socket, session)} + end + + def on_mount(:ensure_authenticated, _params, session, socket) do + socket = mount_current_user(socket, session) + + if socket.assigns.current_user do + {:cont, socket} + else + socket = + socket + |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.") + |> Phoenix.LiveView.redirect(to: ~p"/login") + + {:halt, socket} + end + end + + def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do + socket = mount_current_user(socket, session) + + if socket.assigns.current_user do + {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path())} + else + {:cont, socket} + end + end + + def mount_current_user(socket, session) do + Phoenix.Component.assign_new(socket, :current_user, fn -> + if user_token = session["user_token"] do + maybe_get_current_user(%Session{access_token: user_token}) + end + end) + end + + defp maybe_get_current_user(session) do + {:ok, client} = get_client() + + case GoTrue.get_user(client, session) do + {:ok, %User{} = user} -> user + _ -> nil + end + end + + @doc """ + Used for routes that require the user to not be authenticated. + """ + def redirect_if_user_is_authenticated(conn, _opts) do + if conn.assigns[:current_user] do + conn + |> redirect(to: signed_in_path()) + |> halt() + else + conn + end + end + + @doc """ + Used for routes that require the user to be authenticated. + """ + def require_authenticated_user(conn, _opts) do + if conn.assigns[:current_user] do + conn + else + conn + |> put_flash(:error, "You must log in to access this page.") + |> maybe_store_return_to() + |> redirect(to: ~p"/login") + |> halt() + end + end + + defp put_token_in_session(conn, token) do + conn + |> put_session(:user_token, token) + |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") + end + + defp maybe_store_return_to(%{method: "GET"} = conn) do + put_session(conn, :user_return_to, current_path(conn)) + end + + defp maybe_store_return_to(conn), do: conn + + defp signed_in_path(), do: ~p"/" + + def get_client, do: UserManagement.Supabase.get_client() +end diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index bfb20e9..bdb8ad8 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -28,7 +28,7 @@ "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [: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", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.0", "1e553994dbb1d3bbb0f16003f1a4a51ff3c82c800ae82a1043ce4b04ea7a4eea", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "11cd4cb26e6bf2837250fd1b0ec5982c333791693b432523a908cbb0be5204f8"}, + "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.1", "f1b318827b6c7b74560004eb50ec67c0412a53f7688caf0b08fd703e1281852d", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "efb4f284665fcf9bdd5bca47604d49c5bc22fbc7330a8602b7794e9b90debef7"}, "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, diff --git a/examples/user_management/test/support/conn_case.ex b/examples/user_management/test/support/conn_case.ex index 325b8ba..302f98c 100644 --- a/examples/user_management/test/support/conn_case.ex +++ b/examples/user_management/test/support/conn_case.ex @@ -35,4 +35,29 @@ defmodule UserManagementWeb.ConnCase do UserManagement.DataCase.setup_sandbox(tags) {:ok, conn: Phoenix.ConnTest.build_conn()} end + + @doc """ + Setup helper that registers and logs in users. + + setup :register_and_log_in_user + + It stores an updated connection and a registered user in the + test context. + """ + def register_and_log_in_user(%{conn: conn}) do + user = %Supabase.GoTrue.User{id: Ecto.UUID.generate(), email: "user@example.com"} + session = %Supabase.GoTrue.Session{access_token: "123"} + %{conn: log_in_user(conn, session), user: user, session: session} + end + + def log_in_user(conn, session) do + token = session.access_token + + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> Plug.Conn.put_session(:user_token, token) + |> Plug.Conn.put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") + |> Phoenix.ConnTest.put_req_cookie("_user_management_web_user_remember_me", token) + |> UserManagementWeb.UserAuth.fetch_current_user([]) + end end From 0fa10d25ef132c167db3f90d7ccea8531943d31a Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:43:55 -0300 Subject: [PATCH 15/27] Create migrations for profiles and storage buckets --- .../20250521014037_create_profiles.exs | 43 +++++++++++++++++++ .../20250521014135_create_storage_buckets.exs | 36 ++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs create mode 100644 examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs diff --git a/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs b/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs new file mode 100644 index 0000000..35a7db0 --- /dev/null +++ b/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs @@ -0,0 +1,43 @@ +defmodule UserManagement.Repo.Migrations.CreateProfiles do + use Ecto.Migration + + def change do + # Create the profiles table + create table(:profiles, primary_key: false) do + add :id, :uuid, primary_key: true + add :user_id, :uuid, null: false + add :username, :string + add :website, :string + add :avatar_url, :string + + timestamps() + end + + create unique_index(:profiles, [:user_id]) + create unique_index(:profiles, [:username]) + + # Add Row Level Security (RLS) policies + execute "ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;", "" + + # Policy for public profiles (read-only) + execute """ + CREATE POLICY "Public profiles are viewable by everyone." + ON profiles FOR SELECT + USING (true); + """, "" + + # Policy for users to update their own profile + execute """ + CREATE POLICY "Users can update their own profile." + ON profiles FOR UPDATE + USING (auth.uid() = user_id); + """, "" + + # Policy for users to insert their own profile + execute """ + CREATE POLICY "Users can insert their own profile." + ON profiles FOR INSERT + WITH CHECK (auth.uid() = user_id); + """, "" + end +end diff --git a/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs b/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs new file mode 100644 index 0000000..77e4da3 --- /dev/null +++ b/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs @@ -0,0 +1,36 @@ +defmodule UserManagement.Repo.Migrations.CreateStorageBuckets do + use Ecto.Migration + + def change do + # Create storage bucket for avatars + execute """ + INSERT INTO storage.buckets (id, name, public) + VALUES ('avatars', 'avatars', true); + """, "" + + # Set up RLS policies for the avatars bucket + 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'); + """, "" + + execute """ + CREATE POLICY "Users can update their own avatars." + ON storage.objects FOR UPDATE + USING (bucket_id = 'avatars' AND owner = auth.uid()); + """, "" + + execute """ + CREATE POLICY "Users can delete their own avatars." + ON storage.objects FOR DELETE + USING (bucket_id = 'avatars' AND owner = auth.uid()); + """, "" + end +end From cca66a2d6734a144cc5cde5949df7d7f297131f9 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:47:10 -0300 Subject: [PATCH 16/27] Add profiles context and schema --- .../lib/user_management/profiles.ex | 79 +++++++++++++++++++ .../lib/user_management/profiles/profile.ex | 36 +++++++++ 2 files changed, 115 insertions(+) create mode 100644 examples/user_management/lib/user_management/profiles.ex create mode 100644 examples/user_management/lib/user_management/profiles/profile.ex diff --git a/examples/user_management/lib/user_management/profiles.ex b/examples/user_management/lib/user_management/profiles.ex new file mode 100644 index 0000000..99cb9be --- /dev/null +++ b/examples/user_management/lib/user_management/profiles.ex @@ -0,0 +1,79 @@ +defmodule UserManagement.Profiles do + @moduledoc """ + The Profiles context. + """ + + import Ecto.Query, warn: false + alias UserManagement.Repo + alias UserManagement.Profiles.Profile + + @doc """ + Returns the list of profiles. + """ + def list_profiles do + Repo.all(Profile) + end + + @doc """ + Gets a single profile by user_id. + """ + def get_profile_by_user_id(user_id) do + Repo.get_by(Profile, user_id: user_id) + end + + @doc """ + Gets a single profile by username. + """ + def get_profile_by_username(username) when is_binary(username) do + Repo.get_by(Profile, username: username) + end + + @doc """ + Gets a single profile. + + Raises `Ecto.NoResultsError` if the Profile does not exist. + """ + def get_profile!(id), do: Repo.get!(Profile, id) + + @doc """ + Creates a profile. + """ + def create_profile(attrs \\ %{}) do + %Profile{} + |> Profile.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a profile. + """ + def update_profile(%Profile{} = profile, attrs) do + profile + |> Profile.update_changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a profile. + """ + def delete_profile(%Profile{} = profile) do + Repo.delete(profile) + end + + @doc """ + Creates or updates a profile for a user. + """ + def upsert_profile(user_id, attrs) do + case get_profile_by_user_id(user_id) do + nil -> create_profile(Map.put(attrs, "user_id", user_id)) + profile -> update_profile(profile, attrs) + end + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking profile changes. + """ + def change_profile(%Profile{} = profile, attrs \\ %{}) do + Profile.changeset(profile, attrs) + end +end \ No newline at end of file diff --git a/examples/user_management/lib/user_management/profiles/profile.ex b/examples/user_management/lib/user_management/profiles/profile.ex new file mode 100644 index 0000000..d0c543a --- /dev/null +++ b/examples/user_management/lib/user_management/profiles/profile.ex @@ -0,0 +1,36 @@ +defmodule UserManagement.Profiles.Profile do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "profiles" do + field :user_id, :binary_id + field :username, :string + field :website, :string + field :avatar_url, :string + + timestamps() + end + + @doc false + def changeset(profile, attrs) do + profile + |> cast(attrs, [:user_id, :username, :website, :avatar_url]) + |> validate_required([:user_id]) + |> unique_constraint(:user_id) + |> unique_constraint(:username) + |> validate_format(:website, ~r/^https?:\/\//, message: "must start with http:// or https://") + end + + @doc """ + Changeset for updating a profile. + Only allows updating certain fields, not the user_id. + """ + def update_changeset(profile, attrs) do + profile + |> cast(attrs, [:username, :website, :avatar_url]) + |> unique_constraint(:username) + |> validate_format(:website, ~r/^https?:\/\//, message: "must start with http:// or https://") + end +end \ No newline at end of file From cbb429bbd9db86cc88fa610cc8e3974a2f459529 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 22:56:46 -0300 Subject: [PATCH 17/27] Customize Auth and add profile creation on login --- .../controllers/session_controller.ex | 28 ++++++++++++++++++- .../lib/user_management_web/user_auth.ex | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex index 1f787cc..35a0fe7 100644 --- a/examples/user_management/lib/user_management_web/controllers/session_controller.ex +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -1,6 +1,8 @@ defmodule UserManagementWeb.SessionController do use UserManagementWeb, :controller + alias UserManagement.Profiles + def create(conn, %{"_action" => "confirmed"} = params) do create(conn, params, "User confirmed successfully.") end @@ -11,7 +13,9 @@ defmodule UserManagementWeb.SessionController do defp create(conn, params, info) do with {:ok, conn} <- log_in_with_strategy(conn, params) do - put_flash(conn, :info, info) + conn + |> maybe_create_profile() + |> put_flash(:info, info) else _ -> conn @@ -20,6 +24,28 @@ defmodule UserManagementWeb.SessionController do end end + defp maybe_create_profile(conn) do + if user = conn.assigns[:current_user] do + case Profiles.get_profile_by_user_id(user.id) do + nil -> + email = user.email || "user-#{user.id}" + username = make_username(email) + + UserManagement.Profiles.create_profile(%{ + "user_id" => user.id, + "username" => username + }) + + _profile -> + :ok + end + end + end + + defp make_username(email) do + email |> String.split("@") |> hd() |> String.replace(~r/[^a-zA-Z0-9]/, "") + end + def token(conn, %{"token" => token} = params) do create(conn, Map.put(params, "token", token)) end diff --git a/examples/user_management/lib/user_management_web/user_auth.ex b/examples/user_management/lib/user_management_web/user_auth.ex index 53459b7..3be18c2 100644 --- a/examples/user_management/lib/user_management_web/user_auth.ex +++ b/examples/user_management/lib/user_management_web/user_auth.ex @@ -248,7 +248,7 @@ defmodule UserManagementWeb.UserAuth do defp maybe_store_return_to(conn), do: conn - defp signed_in_path(), do: ~p"/" + defp signed_in_path(), do: ~p"/profile" def get_client, do: UserManagement.Supabase.get_client() end From 4a9034092973de093572ec862b3d3a0bea26ff5e Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 23:09:48 -0300 Subject: [PATCH 18/27] Update login flow to use only magic links --- .../user_management_web/live/login_live.ex | 105 ++++++------------ .../lib/user_management_web/router.ex | 1 - 2 files changed, 31 insertions(+), 75 deletions(-) diff --git a/examples/user_management/lib/user_management_web/live/login_live.ex b/examples/user_management/lib/user_management_web/live/login_live.ex index d13511f..e77d2ab 100644 --- a/examples/user_management/lib/user_management_web/live/login_live.ex +++ b/examples/user_management/lib/user_management_web/live/login_live.ex @@ -6,94 +6,51 @@ defmodule UserManagementWeb.LoginLive do ~H"""
<.header class="text-center"> -

Log in

+

Sign in

<:subtitle> - Don't have an account? - <.link navigate={~p"/register"} class="font-semibold text-brand hover:underline"> - Sign up - - for an account now. + Sign in via magic link with your email below - <.login_form :for={s <- ["otp"]} strategy={s} form={@form} /> -
- """ - end - - def login_form(%{strategy: "password"} = assigns) do - ~H""" - <.form :let={f} for={@form} id="login_form_password" action={~p"/login"} as={:user}> - <.input field={f[:email]} type="email" label="Email" autocomplete="username" required /> - <.input - field={f[:password]} - type="password" - label="Password" - autocomplete="current-password" - required - /> - <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" /> - <.button class="w-full"> - Log in with password - - - """ - end - - def login_form(%{strategy: "otp"} = assigns) do - ~H""" - <.form :let={f} for={@form} id="login_form_otp" action={~p"/login"} as={:user}> - <.input field={f[:email]} type="email" label="Email" required /> - <.button class="w-full"> - Send one-time password - - - """ - end + <.simple_form for={@form} id="login_form" phx-submit="send_magic_link" as={:user}> + <.input field={@form[:email]} type="email" label="Email" required /> + <:actions> + <.button class="w-full" phx-disable-with="Sending..."> + Send magic link + + + - def login_form(%{strategy: "oauth"} = assigns) do - ~H""" -
-

Log in with a provider

-
- <.button - :for={provider <- ["github", "google", "facebook"]} - class="w-full" - phx-click="oauth_login" - phx-value-provider={provider} - > - {provider} - -
+ <.flash_group flash={@flash} />
""" end - def login_form(%{strategy: "anon"} = assigns) do - ~H""" - <.form for={%{}} id="login_form_anon" action={~p"/login"} as={:user}> - <.button class="w-full"> - Continue anonymously - - - """ - end - - def login_form(%{strategy: _} = assigns) do - ~H""" -

Strategy not implemented in LiveView yet

- """ - end - @impl true def mount(_params, _session, socket) do form = to_form(%{"email" => nil}, as: "user") - {:ok, assign(socket, form: form)} + {:ok, assign(socket, form: form, flash: %{})} end @impl true - def handle_event("oauth_login", %{"provider" => provider}, socket) do - # This would be handled by the controller - {:noreply, push_navigate(socket, to: ~p"/login?provider=#{provider}")} + def handle_event("send_magic_link", %{"user" => %{"email" => email}}, socket) do + {:ok, client} = UserManagementWeb.UserAuth.get_client() + + case Supabase.GoTrue.sign_in_with_otp(client, %{email: email}) do + {:ok, _result} -> + {:noreply, + socket + |> put_flash( + :info, + "Magic link sent to #{email}. Check your inbox and follow the link to sign in." + )} + + {:error, %Supabase.Error{metadata: metadata}} -> + message = get_in(metadata, [:resp_body, "msg"]) || "Unknown error" + + {:noreply, + socket + |> put_flash(:error, "Couldn't send magic link: #{message}")} + end end end diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index 4c5edc4..d301175 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -34,7 +34,6 @@ defmodule UserManagementWeb.Router do {UserManagementWeb.UserAuth, :redirect_if_user_is_authenticated} ] do live "/login", LoginLive, :new - live "/register", RegistrationLive, :new end post "/login", SessionController, :create From c6dd46d2006c0862586c21ac707cc2a0bfc166d7 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 23:14:03 -0300 Subject: [PATCH 19/27] Add profile page with avatar upload support --- .../user_management_web/live/profile_live.ex | 222 ++++++++++++++++++ .../lib/user_management_web/router.ex | 3 +- .../priv/static/images/placeholder-avatar.png | 1 + 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 examples/user_management/lib/user_management_web/live/profile_live.ex create mode 100644 examples/user_management/priv/static/images/placeholder-avatar.png diff --git a/examples/user_management/lib/user_management_web/live/profile_live.ex b/examples/user_management/lib/user_management_web/live/profile_live.ex new file mode 100644 index 0000000..7b4324b --- /dev/null +++ b/examples/user_management/lib/user_management_web/live/profile_live.ex @@ -0,0 +1,222 @@ +defmodule UserManagementWeb.ProfileLive do + use UserManagementWeb, :live_view + + alias UserManagement.Profiles + alias UserManagement.Profiles.Profile + + @impl true + def mount(_params, _session, socket) do + if user = socket.assigns.current_user do + profile = + case Profiles.get_profile_by_user_id(user.id) do + nil -> + # Create profile if it doesn't exist yet + email = user.email || "user-#{user.id}" + username = email |> String.split("@") |> hd() |> String.replace(~r/[^a-zA-Z0-9]/, "") + + {:ok, profile} = + Profiles.create_profile(%{ + "user_id" => user.id, + "username" => username + }) + + profile + + profile -> + profile + end + + changeset = Profiles.change_profile(profile) + + {:ok, + socket + |> assign(:profile, profile) + |> assign(:form, to_form(changeset)) + |> assign(:page_title, "Profile") + |> assign(:avatar_url, profile.avatar_url) + |> allow_upload(:avatar, + accept: ~w(.jpg .jpeg .png), + max_entries: 1, + max_file_size: 5_000_000)} + else + {:ok, socket} + end + end + + @impl true + def handle_event("save", %{"profile" => params}, socket) do + case Profiles.update_profile(socket.assigns.profile, params) do + {:ok, profile} -> + {:noreply, + socket + |> put_flash(:info, "Profile updated successfully") + |> assign(:profile, profile) + |> assign(:form, to_form(Profiles.change_profile(profile)))} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :form, to_form(changeset))} + end + end + + @impl true + def handle_event("validate", %{"profile" => params}, socket) do + changeset = + socket.assigns.profile + |> Profiles.change_profile(params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :form, to_form(changeset))} + end + + @impl true + def handle_event("cancel-upload", %{"ref" => ref}, socket) do + {:noreply, cancel_upload(socket, :avatar, ref)} + end + + @impl true + def handle_event("upload-avatar", _params, socket) do + # Upload avatar to Supabase storage + {:ok, client} = UserManagement.Supabase.get_client() + + uploaded_files = + consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry -> + file_name = "#{socket.assigns.current_user.id}-#{entry.client_name}" + file_type = entry.client_type + + # Upload file to Supabase storage in the avatars bucket + file = File.read!(path) + + case Supabase.Storage.from(client, "avatars") + |> Supabase.Storage.upload(file_name, file, content_type: file_type) do + {:ok, data} -> + file_path = "avatars/#{file_name}" + + # Get public URL + {:ok, %{"publicUrl" => public_url}} = + Supabase.Storage.from(client, "avatars") + |> Supabase.Storage.get_public_url(file_name) + + # Update profile with avatar URL + {:ok, _profile} = + Profiles.update_profile(socket.assigns.profile, %{"avatar_url" => public_url}) + + {:ok, public_url} + + {:error, error} -> + {:error, error} + end + end) + + case uploaded_files do + [url] -> + {:noreply, + socket + |> assign(:avatar_url, url) + |> put_flash(:info, "Avatar updated successfully")} + + _ -> + {:noreply, + socket + |> put_flash(:error, "Failed to upload avatar")} + end + end + + @impl true + def render(assigns) do + ~H""" +
+ <.header class="text-center"> + User Profile + <:subtitle> + Manage your profile information + + + + <.simple_form for={@form} id="profile-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:username]} type="text" label="Username" required /> + <.input field={@form[:website]} type="url" label="Website" placeholder="https://example.com" /> + +
+ +
+ Avatar + Default Avatar + +
+ <.live_file_input upload={@uploads.avatar} class="hidden" id="avatar-input" /> +
+ + +
+ +
+ <%= for entry <- @uploads.avatar.entries do %> +
+
<%= entry.client_name %>
+ +
+ + <.live_img_preview entry={entry} class="mt-2 h-16 w-16 rounded-full object-cover" /> + + <%= for err <- upload_errors(@uploads.avatar, entry) do %> +
<%= error_to_string(err) %>
+ <% end %> + <% end %> +
+ + <%= for err <- upload_errors(@uploads.avatar) do %> +
<%= error_to_string(err) %>
+ <% end %> +
+
+
+ + <:actions> + <.button class="w-full">Save Profile + + + +
+ <.link href={~p"/logout"} method="delete" class="text-sm font-semibold text-red-600 hover:underline"> + Sign Out + +
+
+ """ + end + + defp error_to_string(:too_large), do: "File is too large (max 5MB)" + defp error_to_string(:not_accepted), do: "Unacceptable file type (only .jpg, .jpeg, .png)" + defp error_to_string(:too_many_files), do: "Too many files (max 1)" + defp error_to_string(err), do: "Error: #{inspect(err)}" +end \ No newline at end of file diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index d301175..bd717c3 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -21,7 +21,8 @@ defmodule UserManagementWeb.Router do ## Authentication routes scope "/", UserManagementWeb do pipe_through [:browser, :require_authenticated_user] - + + live "/profile", ProfileLive, :index delete "/logout", SessionController, :delete end diff --git a/examples/user_management/priv/static/images/placeholder-avatar.png b/examples/user_management/priv/static/images/placeholder-avatar.png new file mode 100644 index 0000000..3a204d0 --- /dev/null +++ b/examples/user_management/priv/static/images/placeholder-avatar.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QgYEhkQNuZ89QAABQdJREFUeNrtnU9ME1kcx7+DTGlayrRQoP6pLWBRUFGQcNCTB+9ySNiz8bSHvXhcb3v1sNe9mmiiazReTJpsTMzGuyQqMWpQRAUV5U+10A4z+z5PvvCatnTezNuZ9vtJCFBm3rzf7/N+896vM2MQRVEEIWbJ4RAgBBBCACGAEAIIAYQQQAgBhBBACCCEAEIIIIQAQggghABCCCCEAEIIIFYLsOqDhEIhfPz4Ebdv38a9e/cwPT1t+sOdPn0abW1taGlpQUNDA2KxGN6+fYuZmRkcOXLE1PbGYjHcvHkTly9fxujoKJ4/f271EFumrq4OFy5cwNmzZ1FfX4/8/Pz0GykWOHz4cLS/vz86Pz+fVvv6+vqi9fX1Ue4R83R3d0fHxsaiv3MrKyuLdnV1RZ8+fRqdnJw0vb2Li4vRgYGB6MmTJ5V9rtbWVlszsPTlLCwsRF+9ehUVBCFlAf7aJiYmom1tbdz/2dHW1hadm5uLmWE+n4+2tLREHz16FB0fH0/Z3qmpqWhPT09UVVVl7/vQoUPRhYWFuDMsiFxYVlZGV69eRXd3N/Lz89PK0MXFRXz//h3Pnj3D7du3ce3aNRQXF3P/b0NxcTGuX7+OmzdvoqKiIub3qqri6tWruHfvHjo7O1FYWGiqvZqmYWRkBHfu3MHQ0BDCap7UPJLkAGVlZXj8+DGuXLmCggLrLldVVdHV1YXR0VHcuXOHBGRAbW0t+vv70dPTg7KyspTen5eXh87OTty/fx9dXV2Sg0l1dTUGBwfR29tr+SFVVRW9vb0YGBhAdXU1979BKisrcevWLXR0dMDj8Ri+z+Px4MKFC7h//z4qKyuNBwKxsrISg4ODOHfunG0BksvlOH/+PAYGBlBRUUECSsjlcvT19aGtrQ2SJKX1LFmW0dHRgb6+PrhcLvMZoKqqCo8ePUJtba3tAaqqqsLjx49RVVVFAuIxMDCA1tZWS89saWnBwMBA5gIkSZLtmf9f9fX1SKYvdw0YvvDSpUsoKytz7LOLi4tx8eLFzDKALMtoa2tztAH6Ea49AXbs2IHm5mZHP7u5uRnbt2/PTEBDQ4PjU7iGhoYfX47cMBP2wIEDjtY+/QTWkRxgXu5JaLYdR/rQnylnjBJAFHH4SxRACCCEAEIIIIQAQggghABCCCCEAEIIIIQAQggghABCCCCEAEIIIIQAQggg6eHRNCiqCldoGXnhkNT8CZCdkiTAJwgoWl1BqeJDifcLSlc+oyjsh+sXk61oW/+boMVLwEpBMULuYgQ9JdjwlGCtoATLBUVY8WyDJrhsF0AFUi7Q9IxczcbcwCvb/jXlRwJ4vV7b59s7jSAI8Pl86QuQBKwU7kA40/Gv3YhSBPeOIgQ9xViXS7Eu70LE5XG8H7Z0GSiKgO4HBI2/vPSIYevAv3ks4vXt/0hN/1vENdduhDxFWMsvQshdBI/Lg4jLDS3HBY3LWEEErEk7EZY9+zJqn6ZpEAQBmqbBUcfTRfRjFd0XRCxiAWt5BdAERTquiUH0YwWCoFsQhaAVQl9IkdNtRRAECEJ0K1T5KKJoXv4PEASEXAXQJBltTY34NjOHsoICLAQCWAoE8NkfhJb0foHcyY/JBURWlxHxfYaq+BAJhyDLCnQRiqIAmoJ8LZjRZ60WFCEsu+F2uyGK+olGr8+Lal8QC0G/2QkqQUQkEEJ4dRXh4CrkiA/5Wghulw+u/HwUunxY9IQQTpMVTRSx4nJjpaAYPo8Hq+EIlsMRm/8/kMTTwJycHKiqCk3T6HSwGwIIcSj/ANmZSy02PC9oAAAAAElFTkSuQmCC \ No newline at end of file From b23f5072758a97b5db87e85000521fe2e7edd910 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 23:16:02 -0300 Subject: [PATCH 20/27] Update CSS styling and layouts to match Nuxt version --- examples/user_management/assets/css/app.css | 128 ++++++++++++++++++ .../components/layouts/app.html.heex | 36 ++--- 2 files changed, 139 insertions(+), 25 deletions(-) diff --git a/examples/user_management/assets/css/app.css b/examples/user_management/assets/css/app.css index 378c8f9..26939df 100644 --- a/examples/user_management/assets/css/app.css +++ b/examples/user_management/assets/css/app.css @@ -3,3 +3,131 @@ @import "tailwindcss/utilities"; /* This file is for your main application CSS */ + +/* Custom styles for user management app */ +.container { + margin: 0 auto; + max-width: 500px; + padding: 50px 20px; +} + +.avatar { + border-radius: 50%; + overflow: hidden; + max-width: 100%; +} + +.avatar.image { + object-fit: cover; + height: 10rem; + width: 10rem; +} + +.form-widget { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-widget .button { + align-self: flex-end; +} + +.password-input { + position: relative; +} + +.password-input input { + width: 100%; + padding-right: 80px; +} + +.password-input .button { + position: absolute; + top: 2px; + right: 2px; +} + +.button { + display: inline-block; + text-align: center; + font-weight: 600; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + background-color: #3b82f6; + color: white; + transition: background-color 0.2s; +} + +.button:hover { + background-color: #2563eb; +} + +.button.block { + display: block; + width: 100%; +} + +.button.primary { + background-color: #3b82f6; + color: white; +} + +.button.primary:hover { + background-color: #2563eb; +} + +.button.secondary { + background-color: #e5e7eb; + color: #1f2937; +} + +.button.secondary:hover { + background-color: #d1d5db; +} + +.button.danger { + background-color: #ef4444; + color: white; +} + +.button.danger:hover { + background-color: #dc2626; +} + +h1 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + text-align: center; +} + +h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.75rem; +} + +p { + margin-bottom: 1rem; +} + +input, textarea, select { + width: 100%; + padding: 0.5rem; + border: 1px solid #d1d5db; + border-radius: 0.25rem; +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25); +} + +label { + font-weight: 500; + margin-bottom: 0.25rem; + display: block; +} diff --git a/examples/user_management/lib/user_management_web/components/layouts/app.html.heex b/examples/user_management/lib/user_management_web/components/layouts/app.html.heex index 3b3b607..4f94bbe 100644 --- a/examples/user_management/lib/user_management_web/components/layouts/app.html.heex +++ b/examples/user_management/lib/user_management_web/components/layouts/app.html.heex @@ -1,32 +1,18 @@ -
-
-
- - +
+
-
-
- <.flash_group flash={@flash} /> - {@inner_content} -
+
+ <.flash_group flash={@flash} /> + {@inner_content}
From 59946fbed309d2337e9cb5a6af3daa58df9e2840 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Tue, 20 May 2025 23:19:02 -0300 Subject: [PATCH 21/27] Add Supabase project configuration --- examples/user_management/supabase/config.toml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 examples/user_management/supabase/config.toml diff --git a/examples/user_management/supabase/config.toml b/examples/user_management/supabase/config.toml new file mode 100644 index 0000000..dea5444 --- /dev/null +++ b/examples/user_management/supabase/config.toml @@ -0,0 +1,32 @@ +project_id = "examples-user-management" + +[api] +enabled = true +port = 54321 +schemas = ["public", "storage", "auth"] + +[db] +port = 54322 +shadow_port = 54320 + +[studio] +enabled = true +port = 54323 +api_url = "http://localhost:54321" + +[auth] +site_url = "http://localhost:4000" +additional_redirect_urls = ["https://localhost:4000"] +jwt_expiry = 3600 +enable_signup = true + +[auth.email] +enable_signup = true +double_confirm_changes = true +enable_confirmations = false + +[storage] +file_size_limit = "50MiB" + +[local.meta] +schemas = ["public", "storage", "auth", "extensions"] From 14b14774e5bc2e65d11ba600aafbd1fc61722655 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Wed, 21 May 2025 09:28:23 -0300 Subject: [PATCH 22/27] minimal usable version, have bugfixes --- examples/user_management/README.md | 97 +++- examples/user_management/assets/css/app.css | 442 ++++++++++++++--- examples/user_management/assets/js/app.js | 1 + .../user_management/assets/tailwind.config.js | 74 --- examples/user_management/config/config.exs | 12 - examples/user_management/config/dev.exs | 8 +- .../lib/user_management/profiles.ex | 2 +- .../lib/user_management/profiles/profile.ex | 5 +- .../components/core_components.ex | 455 ++---------------- .../components/layouts/app.html.heex | 16 +- .../components/layouts/root.html.heex | 6 +- .../controllers/session_controller.ex | 2 +- .../user_management_web/live/login_live.ex | 45 +- .../user_management_web/live/profile_live.ex | 199 ++++---- .../live/registration_live.ex | 4 +- .../lib/user_management_web/router.ex | 8 +- .../lib/user_management_web/user_auth.ex | 4 +- examples/user_management/mix.exs | 13 +- .../20250521014037_create_profiles.exs | 43 -- .../20250521014135_create_storage_buckets.exs | 36 -- .../supabase/.branches/_current_branch | 1 + .../user_management/supabase/.temp/cli-latest | 1 + examples/user_management/supabase/config.toml | 7 +- examples/user_management/supabase/seed.sql | 53 ++ 24 files changed, 697 insertions(+), 837 deletions(-) delete mode 100644 examples/user_management/assets/tailwind.config.js delete mode 100644 examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs delete mode 100644 examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs create mode 100644 examples/user_management/supabase/.branches/_current_branch create mode 100644 examples/user_management/supabase/.temp/cli-latest create mode 100644 examples/user_management/supabase/seed.sql diff --git a/examples/user_management/README.md b/examples/user_management/README.md index 5fcb50b..780042f 100644 --- a/examples/user_management/README.md +++ b/examples/user_management/README.md @@ -1,18 +1,93 @@ -# UserManagement +# Supabase Phoenix User Management -To start your Phoenix server: +This example demonstrates how to build a user management application using Phoenix LiveView and Supabase. It's a Phoenix port of the [Nuxt 3 User Management example](https://github.com/supabase/supabase/tree/master/examples/user-management/nuxt3-user-management). - * Run `mix setup` to install and setup dependencies - * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` +## Features + +This example shows how to: + +- Sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link) +- Store and retrieve user profile data with [Supabase database](https://supabase.io/docs/guides/database) +- Upload and display avatar images using [Supabase storage](https://supabase.io/docs/guides/storage) +- Protect routes with Phoenix authentication + +## Getting Started + +Before running this app, you need to create a Supabase project and set up your environment variables. + +1. Create a Supabase project +2. Copy your Supabase URL and API key from the project settings +3. Set environment variables: + +```bash +export SUPABASE_URL=https://your-project.supabase.co +export SUPABASE_KEY=your-anon-key +``` + +4. Install dependencies and setup the database: + +```bash +mix setup +``` + +5. Start the Phoenix server: + +```bash +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). +## Database Schema + +The example uses the following database schema for user profiles and storage: + +```sql +-- Create a table for public "profiles" +create table profiles ( + id uuid primary key, + user_id uuid not null, + username text, + website text, + avatar_url text, + inserted_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +alter table profiles enable row level security; + +create policy "Public profiles are viewable by everyone." + on profiles for select + using ( true ); + +create policy "Users can insert their own profile." + on profiles for insert + with check ( auth.uid() = user_id ); + +create policy "Users can update own profile." + on profiles for update + using ( auth.uid() = user_id ); + +-- Set up Storage +insert into storage.buckets (id, name) +values ('avatars', 'avatars'); + +create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + +create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); +``` + +## Implementation Notes + +This example uses: -## Learn more +- [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) for interactive UI +- [Supabase Auth (GoTrue)](https://github.com/zoedsoupe/supabase-ex/tree/main/auth-ex) for authentication +- [Supabase Storage](https://github.com/zoedsoupe/supabase-ex/tree/main/storage-ex) for file uploads +- [Supabase Database](https://github.com/zoedsoupe/supabase-ex) for data access through Ecto - * 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 +Authentication is implemented using magic links (email OTP) with the help of the `supabase.gen.auth` mix task from the `auth-ex` package. \ No newline at end of file diff --git a/examples/user_management/assets/css/app.css b/examples/user_management/assets/css/app.css index 26939df..4c2b333 100644 --- a/examples/user_management/assets/css/app.css +++ b/examples/user_management/assets/css/app.css @@ -1,133 +1,421 @@ -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; +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; -/* This file is for your main application CSS */ + 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 */ -/* Custom styles for user management app */ .container { - margin: 0 auto; - max-width: 500px; - padding: 50px 20px; + width: 90%; + margin-left: auto; + margin-right: auto; } -.avatar { - border-radius: 50%; - overflow: hidden; - max-width: 100%; +.row { + position: relative; + width: 100%; } -.avatar.image { - object-fit: cover; - height: 10rem; - width: 10rem; +.row [class^="col"] { + float: left; + margin: 0.5rem 2%; + min-height: 0.125rem; } -.form-widget { +.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; - gap: 20px; } -.form-widget .button { - align-self: flex-end; +.flex.row { + flex-direction: row; } -.password-input { - position: relative; +.flex.flex-1 { + flex: 1 1 0; } -.password-input input { - width: 100%; - padding-right: 80px; +.flex-end { + justify-content: flex-end; } -.password-input .button { - position: absolute; - top: 2px; - right: 2px; +.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; - font-weight: 600; - padding: 8px 16px; + border-radius: var(--custom-border-radius); + padding: 0.5rem 1rem; cursor: pointer; - border-radius: 4px; - background-color: #3b82f6; - color: white; - transition: background-color 0.2s; + text-align: center; + font-size: 0.9rem; + text-transform: uppercase; } -.button:hover { - background-color: #2563eb; +button.primary, +.button.primary { + background-color: var(--custom-color-brand); + border: 1px solid var(--custom-color-brand); } -.button.block { - display: block; +/* Widgets */ + +.card { width: 100%; + display: block; + border: var(--custom-border); + border-radius: var(--custom-border-radius); + padding: var(--custom-spacing); } -.button.primary { - background-color: #3b82f6; - color: white; +.avatar { + border-radius: var(--custom-border-radius); + overflow: hidden; + max-width: 100%; } -.button.primary:hover { - background-color: #2563eb; +.avatar.image { + object-fit: cover; } -.button.secondary { - background-color: #e5e7eb; - color: #1f2937; +.avatar.no-image { + background-color: #333; + border: 1px solid rgb(200, 200, 200); + border-radius: 5px; } -.button.secondary:hover { - background-color: #d1d5db; +.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); } -.button.danger { - background-color: #ef4444; - color: white; +.footer div { + padding: var(--custom-spacing); + display: flex; + align-items: center; + width: 100%; } -.button.danger:hover { - background-color: #dc2626; +.footer div > img { + height: 20px; + margin-left: 10px; } -h1 { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 1rem; - text-align: center; +.footer > div:first-child { + display: none; } -h2 { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 0.75rem; +.footer > div:nth-child(2) { + justify-content: left; } -p { - margin-bottom: 1rem; +@media only screen and (min-width: 60em) { + /* 960px */ + .footer > div:first-child { + display: flex; + } + + .footer > div:nth-child(2) { + justify-content: center; + } } -input, textarea, select { +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.mainHeader { width: 100%; - padding: 0.5rem; - border: 1px solid #d1d5db; - border-radius: 0.25rem; + font-size: 1.3rem; + margin-bottom: 20px; } -input:focus, textarea:focus, select:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25); +.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; } -label { - font-weight: 500; - margin-bottom: 0.25rem; - display: block; +.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/user_management/assets/js/app.js b/examples/user_management/assets/js/app.js index d5e278a..4baf040 100644 --- a/examples/user_management/assets/js/app.js +++ b/examples/user_management/assets/js/app.js @@ -21,6 +21,7 @@ import "phoenix_html" import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" +import "../css/app.css" let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { diff --git a/examples/user_management/assets/tailwind.config.js b/examples/user_management/assets/tailwind.config.js deleted file mode 100644 index 15a5ef0..0000000 --- a/examples/user_management/assets/tailwind.config.js +++ /dev/null @@ -1,74 +0,0 @@ -// See the Tailwind configuration guide for advanced usage -// https://tailwindcss.com/docs/configuration - -const plugin = require("tailwindcss/plugin") -const fs = require("fs") -const path = require("path") - -module.exports = { - content: [ - "./js/**/*.js", - "../lib/user_management_web.ex", - "../lib/user_management_web/**/*.*ex" - ], - theme: { - extend: { - colors: { - brand: "#FD4F00", - } - }, - }, - plugins: [ - require("@tailwindcss/forms"), - // Allows prefixing tailwind classes with LiveView classes to add rules - // only when LiveView classes are applied, for example: - // - //
- // - plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), - plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), - plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), - - // Embeds Heroicons (https://heroicons.com) into your app.css bundle - // See your `CoreComponents.icon/1` for more information. - // - plugin(function({matchComponents, theme}) { - let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") - let values = {} - let icons = [ - ["", "/24/outline"], - ["-solid", "/24/solid"], - ["-mini", "/20/solid"], - ["-micro", "/16/solid"] - ] - icons.forEach(([suffix, dir]) => { - fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { - let name = path.basename(file, ".svg") + suffix - values[name] = {name, fullPath: path.join(iconsDir, dir, file)} - }) - }) - matchComponents({ - "hero": ({name, fullPath}) => { - let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") - let size = theme("spacing.6") - if (name.endsWith("-mini")) { - size = theme("spacing.5") - } else if (name.endsWith("-micro")) { - size = theme("spacing.4") - } - return { - [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, - "-webkit-mask": `var(--hero-${name})`, - "mask": `var(--hero-${name})`, - "mask-repeat": "no-repeat", - "background-color": "currentColor", - "vertical-align": "middle", - "display": "inline-block", - "width": size, - "height": size - } - } - }, {values}) - }) - ] -} diff --git a/examples/user_management/config/config.exs b/examples/user_management/config/config.exs index 43a76c6..126afe2 100644 --- a/examples/user_management/config/config.exs +++ b/examples/user_management/config/config.exs @@ -32,18 +32,6 @@ config :esbuild, env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] -# Configure tailwind (the version is required) -config :tailwind, - version: "3.4.3", - user_management: [ - args: ~w( - --config=tailwind.config.js - --input=css/app.css - --output=../priv/static/assets/app.css - ), - cd: Path.expand("../assets", __DIR__) - ] - # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", diff --git a/examples/user_management/config/dev.exs b/examples/user_management/config/dev.exs index 04c4d13..366fc3e 100644 --- a/examples/user_management/config/dev.exs +++ b/examples/user_management/config/dev.exs @@ -4,8 +4,9 @@ import Config config :user_management, UserManagement.Repo, username: "postgres", password: "postgres", - hostname: "localhost", - database: "user_management_dev", + hostname: "127.0.0.1", + port: 54322, + database: "postgres", stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10 @@ -25,8 +26,7 @@ config :user_management, UserManagementWeb.Endpoint, debug_errors: true, secret_key_base: "qPuV8P5jtLUGTkjfVLst2MXrzCUI9r+8dBrkdngvVM64mVzuPnVLSphbFIlpNaR/", watchers: [ - esbuild: {Esbuild, :install_and_run, [:user_management, ~w(--sourcemap=inline --watch)]}, - tailwind: {Tailwind, :install_and_run, [:user_management, ~w(--watch)]} + esbuild: {Esbuild, :install_and_run, [:user_management, ~w(--sourcemap=inline --watch)]} ] # ## SSL Support diff --git a/examples/user_management/lib/user_management/profiles.ex b/examples/user_management/lib/user_management/profiles.ex index 99cb9be..442fbb1 100644 --- a/examples/user_management/lib/user_management/profiles.ex +++ b/examples/user_management/lib/user_management/profiles.ex @@ -76,4 +76,4 @@ defmodule UserManagement.Profiles do def change_profile(%Profile{} = profile, attrs \\ %{}) do Profile.changeset(profile, attrs) end -end \ No newline at end of file +end diff --git a/examples/user_management/lib/user_management/profiles/profile.ex b/examples/user_management/lib/user_management/profiles/profile.ex index d0c543a..8332549 100644 --- a/examples/user_management/lib/user_management/profiles/profile.ex +++ b/examples/user_management/lib/user_management/profiles/profile.ex @@ -5,12 +5,11 @@ defmodule UserManagement.Profiles.Profile do @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "profiles" do - field :user_id, :binary_id field :username, :string field :website, :string field :avatar_url, :string - timestamps() + timestamps(inserted_at: false) end @doc false @@ -33,4 +32,4 @@ defmodule UserManagement.Profiles.Profile do |> unique_constraint(:username) |> validate_format(:website, ~r/^https?:\/\//, message: "must start with http:// or https://") end -end \ No newline at end of file +end diff --git a/examples/user_management/lib/user_management_web/components/core_components.ex b/examples/user_management/lib/user_management_web/components/core_components.ex index d730478..2d1cd47 100644 --- a/examples/user_management/lib/user_management_web/components/core_components.ex +++ b/examples/user_management/lib/user_management_web/components/core_components.ex @@ -1,93 +1,13 @@ defmodule UserManagementWeb.CoreComponents do @moduledoc """ - Provides core UI components. + Provides core UI components for Supabase User Management example. - At first glance, this module may seem daunting, but its goal is to provide - core building blocks for your application, such as modals, tables, and - forms. The components consist mostly of markup and are well-documented - with doc strings and declarative assigns. You may customize and style - them in any way you want, based on your application growth and needs. - - The default components use Tailwind CSS, a utility-first CSS framework. - See the [Tailwind CSS documentation](https://tailwindcss.com) to learn - how to customize them or feel free to swap in another framework altogether. - - Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + These components use Phoenix's component structure while matching the style of the Nuxt3 version. """ use Phoenix.Component alias Phoenix.LiveView.JS - @doc """ - Renders a modal. - - ## Examples - - <.modal id="confirm-modal"> - This is a modal. - - - JS commands may be passed to the `:on_cancel` to configure - the closing/cancel event, for example: - - <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> - This is another modal. - - - """ - attr :id, :string, required: true - attr :show, :boolean, default: false - attr :on_cancel, JS, default: %JS{} - slot :inner_block, required: true - - def modal(assigns) do - ~H""" - """ end @@ -146,36 +57,14 @@ defmodule UserManagementWeb.CoreComponents do 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")} - hidden - > - Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> - - - <.flash - id="server-error" - kind={:error} - title="Something went wrong!" - phx-disconnected={show(".phx-server-error #server-error")} - phx-connected={hide("#server-error")} - hidden - > - Hang in there while we get back on track - <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> - + <.flash kind={:info} flash={@flash} /> + <.flash kind={:error} flash={@flash} />
""" end @doc """ - Renders a simple form. + Renders a simple form that matches the Nuxt styling. ## Examples @@ -191,7 +80,7 @@ defmodule UserManagementWeb.CoreComponents do attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" attr :rest, :global, - include: ~w(autocomplete name rel action enctype method novalidate target multipart), + include: ~w(autocomplete name rel action enctype method novalidate target multipart class), doc: "the arbitrary HTML attributes to apply to the form tag" slot :inner_block, required: true @@ -199,24 +88,22 @@ defmodule UserManagementWeb.CoreComponents do def simple_form(assigns) do ~H""" - <.form :let={f} for={@for} as={@as} {@rest}> -
- {render_slot(@inner_block, f)} -
- {render_slot(action, f)} -
+ <.form :let={f} for={@for} as={@as} class={"form-widget #{Map.get(@rest, :class, "")}"} {@rest}> + {render_slot(@inner_block, f)} +
+ {render_slot(action, f)}
""" end @doc """ - Renders a button. + Renders a button using Supabase styling. ## Examples <.button>Send! - <.button phx-click="go" class="ml-2">Send! + <.button phx-click="go" class="primary">Send! """ attr :type, :string, default: nil attr :class, :string, default: nil @@ -226,40 +113,14 @@ defmodule UserManagementWeb.CoreComponents do def button(assigns) do ~H""" - """ end @doc """ - Renders an input with label and error messages. - - A `Phoenix.HTML.FormField` may be passed as argument, - which is used to retrieve the input name, id, and values. - Otherwise all attributes may be passed explicitly. - - ## Types - - This function accepts all HTML input types, considering that: - - * You may also set `type="select"` to render a ` - + {@label} <.error :for={msg <- @errors}>{msg} @@ -329,14 +183,8 @@ defmodule UserManagementWeb.CoreComponents do def input(%{type: "select"} = assigns) do ~H"""
- <.label for={@id}>{@label} - {Phoenix.HTML.Form.options_for_select(@options, @value)} @@ -348,17 +196,8 @@ defmodule UserManagementWeb.CoreComponents do def input(%{type: "textarea"} = assigns) do ~H"""
- <.label for={@id}>{@label} - + + <.error :for={msg <- @errors}>{msg}
""" @@ -368,17 +207,13 @@ defmodule UserManagementWeb.CoreComponents do def input(assigns) do ~H"""
- <.label for={@id}>{@label} + <.error :for={msg <- @errors}>{msg} @@ -386,20 +221,6 @@ defmodule UserManagementWeb.CoreComponents do """ end - @doc """ - Renders a label. - """ - attr :for, :string, default: nil - slot :inner_block, required: true - - def label(assigns) do - ~H""" - - """ - end - @doc """ Generates a generic error message. """ @@ -407,8 +228,7 @@ defmodule UserManagementWeb.CoreComponents do def error(assigns) do ~H""" -

- <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> +

{render_slot(@inner_block)}

""" @@ -425,176 +245,20 @@ defmodule UserManagementWeb.CoreComponents do def header(assigns) do ~H""" -
-
-

- {render_slot(@inner_block)} -

-

- {render_slot(@subtitle)} -

-
-
{render_slot(@actions)}
-
- """ - end - - @doc ~S""" - Renders a table with generic styling. - - ## Examples - - <.table id="users" rows={@users}> - <:col :let={user} label="id">{user.id} - <:col :let={user} label="username">{user.username} - - """ - attr :id, :string, required: true - attr :rows, :list, required: true - attr :row_id, :any, default: nil, doc: "the function for generating the row id" - attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" - - attr :row_item, :any, - default: &Function.identity/1, - doc: "the function for mapping each row before calling the :col and :action slots" - - slot :col, required: true do - attr :label, :string - end - - slot :action, doc: "the slot for showing user actions in the last table column" - - def table(assigns) do - assigns = - with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do - assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) - end - - ~H""" -
- - - - - - - - - - - - - -
{col[:label]} - Actions -
-
- - - {render_slot(col, @row_item.(row))} - -
-
-
- - - {render_slot(action, @row_item.(row))} - -
-
-
- """ - end - - @doc """ - Renders a data list. - - ## Examples - - <.list> - <:item title="Title">{@post.title} - <:item title="Views">{@post.views} - - """ - slot :item, required: true do - attr :title, :string, required: true - end - - def list(assigns) do - ~H""" -
-
-
-
{item.title}
-
{render_slot(item)}
-
-
-
- """ - end - - @doc """ - Renders a back navigation link. - - ## Examples - - <.back navigate={~p"/posts"}>Back to posts - """ - attr :navigate, :any, required: true - slot :inner_block, required: true - - def back(assigns) do - ~H""" -
- <.link - navigate={@navigate} - class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" - > - <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> +
+

{render_slot(@inner_block)} - +

+

+ {render_slot(@subtitle)} +

+
+ {render_slot(@actions)} +
""" end - @doc """ - Renders a [Heroicon](https://heroicons.com). - - Heroicons come in three styles – outline, solid, and mini. - By default, the outline style is used, but solid and mini may - be applied by using the `-solid` and `-mini` suffix. - - You can customize the size and colors of the icons by setting - width, height, and background color classes. - - Icons are extracted from the `deps/heroicons` directory and bundled within - your compiled app.css by the plugin in your `assets/tailwind.config.js`. - - ## Examples - - <.icon name="hero-x-mark-solid" /> - <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> - """ - attr :name, :string, required: true - attr :class, :string, default: nil - - def icon(%{name: "hero-" <> _} = assigns) do - ~H""" - - """ - end - ## JS Commands def show(js \\ %JS{}, selector) do @@ -619,53 +283,12 @@ defmodule UserManagementWeb.CoreComponents do ) end - def show_modal(js \\ %JS{}, id) when is_binary(id) do - js - |> JS.show(to: "##{id}") - |> JS.show( - to: "##{id}-bg", - time: 300, - transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} - ) - |> show("##{id}-container") - |> JS.add_class("overflow-hidden", to: "body") - |> JS.focus_first(to: "##{id}-content") - end - - def hide_modal(js \\ %JS{}, id) do - js - |> JS.hide( - to: "##{id}-bg", - transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} - ) - |> hide("##{id}-container") - |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) - |> JS.remove_class("overflow-hidden", to: "body") - |> JS.pop_focus() - end - @doc """ - Translates an error message using gettext. + Translates an error message. """ def translate_error({msg, opts}) do - # You can make use of gettext to translate error messages by - # uncommenting and adjusting the following code: - - # if count = opts[:count] do - # Gettext.dngettext(UserManagementWeb.Gettext, "errors", msg, msg, count, opts) - # else - # Gettext.dgettext(UserManagementWeb.Gettext, "errors", msg, opts) - # end - Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) end) end - - @doc """ - Translates the errors for a field from a keyword list of errors. - """ - def translate_errors(errors, field) when is_list(errors) do - for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) - end end diff --git a/examples/user_management/lib/user_management_web/components/layouts/app.html.heex b/examples/user_management/lib/user_management_web/components/layouts/app.html.heex index 4f94bbe..598b744 100644 --- a/examples/user_management/lib/user_management_web/components/layouts/app.html.heex +++ b/examples/user_management/lib/user_management_web/components/layouts/app.html.heex @@ -1,18 +1,4 @@ -
- -
-
+
<.flash_group flash={@flash} /> {@inner_content}
diff --git a/examples/user_management/lib/user_management_web/components/layouts/root.html.heex b/examples/user_management/lib/user_management_web/components/layouts/root.html.heex index edddc68..a2a9c45 100644 --- a/examples/user_management/lib/user_management_web/components/layouts/root.html.heex +++ b/examples/user_management/lib/user_management_web/components/layouts/root.html.heex @@ -1,17 +1,17 @@ - + - <.live_title default="UserManagement" suffix=" · Phoenix Framework"> + <.live_title default="Supabase User Management"> {assigns[:page_title]} - + {@inner_content} diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex index 35a0fe7..1fa5457 100644 --- a/examples/user_management/lib/user_management_web/controllers/session_controller.ex +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -20,7 +20,7 @@ defmodule UserManagementWeb.SessionController do _ -> conn |> put_flash(:error, "Invalid credentials") - |> redirect(to: ~p"/login") + |> redirect(to: ~p"/") end end diff --git a/examples/user_management/lib/user_management_web/live/login_live.ex b/examples/user_management/lib/user_management_web/live/login_live.ex index e77d2ab..2b77afa 100644 --- a/examples/user_management/lib/user_management_web/live/login_live.ex +++ b/examples/user_management/lib/user_management_web/live/login_live.ex @@ -4,24 +4,26 @@ defmodule UserManagementWeb.LoginLive do @impl true def render(assigns) do ~H""" -
- <.header class="text-center"> -

Sign in

- <:subtitle> - Sign in via magic link with your email below - - - - <.simple_form for={@form} id="login_form" phx-submit="send_magic_link" as={:user}> - <.input field={@form[:email]} type="email" label="Email" required /> - <:actions> - <.button class="w-full" phx-disable-with="Sending..."> - Send magic link - - - - - <.flash_group flash={@flash} /> +
+
+ <.header> + Supabase + Phoenix + <:subtitle> + Sign in via magic link with your email below + + + + <.simple_form for={@form} id="login_form" phx-submit="send_magic_link" as={:user}> + <.input field={@form[:email]} type="email" placeholder="Your email" required /> + <:actions> + <.button class="block" phx-disable-with="Loading..."> + {if @loading, do: "Loading", else: "Send magic link"} + + + + + <.flash_group flash={@flash} /> +
""" end @@ -29,17 +31,21 @@ defmodule UserManagementWeb.LoginLive do @impl true def mount(_params, _session, socket) do form = to_form(%{"email" => nil}, as: "user") - {:ok, assign(socket, form: form, flash: %{})} + {:ok, assign(socket, form: form, loading: false)} end @impl true def handle_event("send_magic_link", %{"user" => %{"email" => email}}, socket) do + # Set loading state + socket = assign(socket, loading: true) + {:ok, client} = UserManagementWeb.UserAuth.get_client() case Supabase.GoTrue.sign_in_with_otp(client, %{email: email}) do {:ok, _result} -> {:noreply, socket + |> assign(loading: false) |> put_flash( :info, "Magic link sent to #{email}. Check your inbox and follow the link to sign in." @@ -50,6 +56,7 @@ defmodule UserManagementWeb.LoginLive do {:noreply, socket + |> assign(loading: false) |> put_flash(:error, "Couldn't send magic link: #{message}")} end end diff --git a/examples/user_management/lib/user_management_web/live/profile_live.ex b/examples/user_management/lib/user_management_web/live/profile_live.ex index 7b4324b..b6c593e 100644 --- a/examples/user_management/lib/user_management_web/live/profile_live.ex +++ b/examples/user_management/lib/user_management_web/live/profile_live.ex @@ -2,7 +2,7 @@ defmodule UserManagementWeb.ProfileLive do use UserManagementWeb, :live_view alias UserManagement.Profiles - alias UserManagement.Profiles.Profile + alias Supabase.Storage @impl true def mount(_params, _session, socket) do @@ -34,10 +34,11 @@ defmodule UserManagementWeb.ProfileLive do |> assign(:form, to_form(changeset)) |> assign(:page_title, "Profile") |> assign(:avatar_url, profile.avatar_url) - |> allow_upload(:avatar, - accept: ~w(.jpg .jpeg .png), - max_entries: 1, - max_file_size: 5_000_000)} + |> allow_upload(:avatar, + accept: ~w(.jpg .jpeg .png), + max_entries: 1, + max_file_size: 5_000_000 + )} else {:ok, socket} end @@ -82,26 +83,32 @@ defmodule UserManagementWeb.ProfileLive do consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry -> file_name = "#{socket.assigns.current_user.id}-#{entry.client_name}" file_type = entry.client_type - + # Upload file to Supabase storage in the avatars bucket file = File.read!(path) - - case Supabase.Storage.from(client, "avatars") - |> Supabase.Storage.upload(file_name, file, content_type: file_type) do - {:ok, data} -> - file_path = "avatars/#{file_name}" - + + # Create a temporary file and upload it + temp_path = Path.join(System.tmp_dir(), file_name) + File.write!(temp_path, file) + + # Upload the file to Supabase + case Storage.from(client, "avatars") + |> Storage.File.upload(temp_path, file_name, %{content_type: file_type}) do + {:ok, %{path: _path}} -> + # Clean up temporary file + File.rm!(temp_path) + # Get public URL - {:ok, %{"publicUrl" => public_url}} = - Supabase.Storage.from(client, "avatars") - |> Supabase.Storage.get_public_url(file_name) - + {:ok, public_url} = + Storage.from(client, "avatars") + |> Storage.File.get_public_url(file_name) + # Update profile with avatar URL - {:ok, _profile} = + {:ok, _profile} = Profiles.update_profile(socket.assigns.profile, %{"avatar_url" => public_url}) - + {:ok, public_url} - + {:error, error} -> {:error, error} end @@ -109,13 +116,13 @@ defmodule UserManagementWeb.ProfileLive do case uploaded_files do [url] -> - {:noreply, - socket + {:noreply, + socket |> assign(:avatar_url, url) |> put_flash(:info, "Avatar updated successfully")} - + _ -> - {:noreply, + {:noreply, socket |> put_flash(:error, "Failed to upload avatar")} end @@ -124,90 +131,82 @@ defmodule UserManagementWeb.ProfileLive do @impl true def render(assigns) do ~H""" -
- <.header class="text-center"> - User Profile - <:subtitle> - Manage your profile information - - +
+
+
+
+ <%= if @avatar_url do %> + Avatar + <% else %> +
+ <% end %> +
- <.simple_form for={@form} id="profile-form" phx-change="validate" phx-submit="save"> - <.input field={@form[:username]} type="text" label="Username" required /> - <.input field={@form[:website]} type="url" label="Website" placeholder="https://example.com" /> - -
- -
- Avatar - Default Avatar + <.live_file_input + upload={@uploads.avatar} + style="position: absolute; visibility: hidden;" + id="avatar-input" /> - -
- <.live_file_input upload={@uploads.avatar} class="hidden" id="avatar-input" /> -
- - -
- -
- <%= for entry <- @uploads.avatar.entries do %> -
-
<%= entry.client_name %>
- -
- - <.live_img_preview entry={entry} class="mt-2 h-16 w-16 rounded-full object-cover" /> - - <%= for err <- upload_errors(@uploads.avatar, entry) do %> -
<%= error_to_string(err) %>
- <% end %> - <% end %> -
- - <%= for err <- upload_errors(@uploads.avatar) do %> -
<%= error_to_string(err) %>
- <% end %> -
+ + +
- + +
+ <%= for entry <- @uploads.avatar.entries do %> +
+ {entry.client_name} + +
+ + <.live_img_preview entry={entry} class="avatar image" style="height: 5em; width: 5em;" /> + + <%= for err <- upload_errors(@uploads.avatar, entry) do %> +
{error_to_string(err)}
+ <% end %> + <% end %> +
+ + <%= for err <- upload_errors(@uploads.avatar) do %> +
{error_to_string(err)}
+ <% end %> +
+ + <.simple_form for={@form} id="profile-form" phx-change="validate" phx-submit="save"> +
+ + +
+ + <.input field={@form[:username]} type="text" label="Name" /> + <.input field={@form[:website]} type="url" label="Website" /> + <:actions> - <.button class="w-full">Save Profile + <.button class="primary block">Update -
- <.link href={~p"/logout"} method="delete" class="text-sm font-semibold text-red-600 hover:underline"> +
+ <.link href={~p"/logout"} method="delete" class="button block"> Sign Out
@@ -219,4 +218,4 @@ defmodule UserManagementWeb.ProfileLive do defp error_to_string(:not_accepted), do: "Unacceptable file type (only .jpg, .jpeg, .png)" defp error_to_string(:too_many_files), do: "Too many files (max 1)" defp error_to_string(err), do: "Error: #{inspect(err)}" -end \ No newline at end of file +end diff --git a/examples/user_management/lib/user_management_web/live/registration_live.ex b/examples/user_management/lib/user_management_web/live/registration_live.ex index f5bd794..ec49e9a 100644 --- a/examples/user_management/lib/user_management_web/live/registration_live.ex +++ b/examples/user_management/lib/user_management_web/live/registration_live.ex @@ -9,7 +9,7 @@ defmodule UserManagementWeb.RegistrationLive do Register <:subtitle> Already have an account? - <.link navigate={~p"/login"} class="font-semibold text-brand hover:underline"> + <.link navigate={~p"/"} class="font-semibold text-brand hover:underline"> Sign in to your account now. @@ -58,7 +58,7 @@ defmodule UserManagementWeb.RegistrationLive do {:noreply, socket |> put_flash(:info, "User created successfully. Please sign in.") - |> push_navigate(to: ~p"/login")} + |> push_navigate(to: ~p"/")} {:error, %Supabase.Error{metadata: metadata}} -> message = get_in(metadata, [:resp_body, "msg"]) diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index bd717c3..52571ff 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -21,7 +21,7 @@ defmodule UserManagementWeb.Router do ## Authentication routes scope "/", UserManagementWeb do pipe_through [:browser, :require_authenticated_user] - + live "/profile", ProfileLive, :index delete "/logout", SessionController, :delete end @@ -34,10 +34,10 @@ defmodule UserManagementWeb.Router do {UserManagementWeb.UserAuth, :mount_current_user}, {UserManagementWeb.UserAuth, :redirect_if_user_is_authenticated} ] do - live "/login", LoginLive, :new + live "/", LoginLive, :new end - post "/login", SessionController, :create - post "/login/:token", SessionController, :token + post "/", SessionController, :create + post "/:token", SessionController, :token end end diff --git a/examples/user_management/lib/user_management_web/user_auth.ex b/examples/user_management/lib/user_management_web/user_auth.ex index 3be18c2..d0b1c3b 100644 --- a/examples/user_management/lib/user_management_web/user_auth.ex +++ b/examples/user_management/lib/user_management_web/user_auth.ex @@ -175,7 +175,7 @@ defmodule UserManagementWeb.UserAuth do socket = socket |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.") - |> Phoenix.LiveView.redirect(to: ~p"/login") + |> Phoenix.LiveView.redirect(to: ~p"/") {:halt, socket} end @@ -231,7 +231,7 @@ defmodule UserManagementWeb.UserAuth do conn |> put_flash(:error, "You must log in to access this page.") |> maybe_store_return_to() - |> redirect(to: ~p"/login") + |> redirect(to: ~p"/") |> halt() end end diff --git a/examples/user_management/mix.exs b/examples/user_management/mix.exs index 5c5868b..fa4ecd5 100644 --- a/examples/user_management/mix.exs +++ b/examples/user_management/mix.exs @@ -41,17 +41,9 @@ defmodule UserManagement.MixProject do {:phoenix_live_view, "~> 1.0"}, {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, - {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev}, {:supabase_potion, "~> 0.6"}, {:supabase_gotrue, "~> 0.5"}, {:supabase_storage, "~> 0.4"}, - {:heroicons, - github: "tailwindlabs/heroicons", - tag: "v2.1.1", - sparse: "optimized", - app: false, - compile: false, - depth: 1}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, {:jason, "~> 1.2"}, @@ -72,10 +64,9 @@ defmodule UserManagement.MixProject do "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": ["tailwind.install --if-missing", "esbuild.install --if-missing"], - "assets.build": ["tailwind user_management", "esbuild user_management"], + "assets.setup": ["esbuild.install --if-missing"], + "assets.build": ["esbuild user_management"], "assets.deploy": [ - "tailwind user_management --minify", "esbuild user_management --minify", "phx.digest" ] diff --git a/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs b/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs deleted file mode 100644 index 35a7db0..0000000 --- a/examples/user_management/priv/repo/migrations/20250521014037_create_profiles.exs +++ /dev/null @@ -1,43 +0,0 @@ -defmodule UserManagement.Repo.Migrations.CreateProfiles do - use Ecto.Migration - - def change do - # Create the profiles table - create table(:profiles, primary_key: false) do - add :id, :uuid, primary_key: true - add :user_id, :uuid, null: false - add :username, :string - add :website, :string - add :avatar_url, :string - - timestamps() - end - - create unique_index(:profiles, [:user_id]) - create unique_index(:profiles, [:username]) - - # Add Row Level Security (RLS) policies - execute "ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;", "" - - # Policy for public profiles (read-only) - execute """ - CREATE POLICY "Public profiles are viewable by everyone." - ON profiles FOR SELECT - USING (true); - """, "" - - # Policy for users to update their own profile - execute """ - CREATE POLICY "Users can update their own profile." - ON profiles FOR UPDATE - USING (auth.uid() = user_id); - """, "" - - # Policy for users to insert their own profile - execute """ - CREATE POLICY "Users can insert their own profile." - ON profiles FOR INSERT - WITH CHECK (auth.uid() = user_id); - """, "" - end -end diff --git a/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs b/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs deleted file mode 100644 index 77e4da3..0000000 --- a/examples/user_management/priv/repo/migrations/20250521014135_create_storage_buckets.exs +++ /dev/null @@ -1,36 +0,0 @@ -defmodule UserManagement.Repo.Migrations.CreateStorageBuckets do - use Ecto.Migration - - def change do - # Create storage bucket for avatars - execute """ - INSERT INTO storage.buckets (id, name, public) - VALUES ('avatars', 'avatars', true); - """, "" - - # Set up RLS policies for the avatars bucket - 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'); - """, "" - - execute """ - CREATE POLICY "Users can update their own avatars." - ON storage.objects FOR UPDATE - USING (bucket_id = 'avatars' AND owner = auth.uid()); - """, "" - - execute """ - CREATE POLICY "Users can delete their own avatars." - ON storage.objects FOR DELETE - USING (bucket_id = 'avatars' AND owner = auth.uid()); - """, "" - end -end diff --git a/examples/user_management/supabase/.branches/_current_branch b/examples/user_management/supabase/.branches/_current_branch new file mode 100644 index 0000000..88d050b --- /dev/null +++ b/examples/user_management/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/examples/user_management/supabase/.temp/cli-latest b/examples/user_management/supabase/.temp/cli-latest new file mode 100644 index 0000000..624e112 --- /dev/null +++ b/examples/user_management/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.23.4 \ No newline at end of file diff --git a/examples/user_management/supabase/config.toml b/examples/user_management/supabase/config.toml index dea5444..13eae1f 100644 --- a/examples/user_management/supabase/config.toml +++ b/examples/user_management/supabase/config.toml @@ -1,7 +1,7 @@ project_id = "examples-user-management" [api] -enabled = true +enabled = false port = 54321 schemas = ["public", "storage", "auth"] @@ -28,5 +28,6 @@ enable_confirmations = false [storage] file_size_limit = "50MiB" -[local.meta] -schemas = ["public", "storage", "auth", "extensions"] +[inbucket] +enabled = true +port = 54324 diff --git a/examples/user_management/supabase/seed.sql b/examples/user_management/supabase/seed.sql new file mode 100644 index 0000000..e398b24 --- /dev/null +++ b/examples/user_management/supabase/seed.sql @@ -0,0 +1,53 @@ +-- Seed data for Supabase Local Development +-- This file will be executed when running `supabase start` + +-- Create a table for public "profiles" +create table profiles ( + id uuid references auth.users not null, + updated_at timestamp with time zone, + username text unique, + avatar_url text, + website text, + + primary key (id), + unique(username), + constraint username_length check (char_length(username) >= 3) +); + +alter table profiles enable row level security; + +create policy "Public profiles are viewable by everyone." + on profiles for select + using ( true ); + +create policy "Users can insert their own profile." + on profiles for insert + with check ( (select auth.uid()) = id ); + +create policy "Users can update own profile." + on profiles for update + using ( (select auth.uid()) = id ); + +-- Set up Storage! +insert into storage.buckets (id, name) +values ('avatars', 'avatars'); + +create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + +create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); + +-- Create test users first (for local development only) +INSERT INTO auth.users (id, email, confirmation_sent_at) +VALUES + ('00000000-0000-0000-0000-000000000001', 'test1@example.com', NOW()), + ('00000000-0000-0000-0000-000000000002', 'test2@example.com', NOW()), + ('00000000-0000-0000-0000-000000000003', 'test3@example.com', NOW()) +ON CONFLICT (id) DO NOTHING; + +-- Note: The avatars storage bucket is created in the migrations +-- This is just a reminder that you can use the following path pattern for user avatars: +-- Storage path example: 'avatars/{user_id}.png' From c9f4fbc82be40195b8216fb064cf9e80bf149234 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sun, 29 Jun 2025 18:24:37 -0300 Subject: [PATCH 23/27] chore: correct pattern matching --- .envrc | 4 ++++ examples/user_management/.gitignore | 1 - .../user_management_web/live/login_live.ex | 2 +- examples/user_management/mix.lock | 24 +++++++++---------- .../user_management/supabase/.temp/cli-latest | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.envrc b/.envrc index cfbecd0..9d6365b 100644 --- a/.envrc +++ b/.envrc @@ -8,4 +8,8 @@ export ERL_AFLAGS="-kernel shell_history enabled" export LANG=en_US.UTF-8 +if [ -f .env.dev ]; then + source .env.dev +fi + use flake diff --git a/examples/user_management/.gitignore b/examples/user_management/.gitignore index 72b8a8b..c62af53 100644 --- a/examples/user_management/.gitignore +++ b/examples/user_management/.gitignore @@ -34,4 +34,3 @@ user_management-*.tar # In case you use Node.js/npm, you want to ignore these. npm-debug.log /assets/node_modules/ - diff --git a/examples/user_management/lib/user_management_web/live/login_live.ex b/examples/user_management/lib/user_management_web/live/login_live.ex index 2b77afa..55e8775 100644 --- a/examples/user_management/lib/user_management_web/live/login_live.ex +++ b/examples/user_management/lib/user_management_web/live/login_live.ex @@ -42,7 +42,7 @@ defmodule UserManagementWeb.LoginLive do {:ok, client} = UserManagementWeb.UserAuth.get_client() case Supabase.GoTrue.sign_in_with_otp(client, %{email: email}) do - {:ok, _result} -> + :ok -> {:noreply, socket |> assign(loading: false) diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index bdb8ad8..d6d3699 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -1,15 +1,15 @@ %{ - "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.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", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"}, + "bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [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", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [: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", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [: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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, + "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [: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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [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", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "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.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -17,25 +17,25 @@ "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [: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", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "peri": {:hex, :peri, "0.3.2", "79a2a91d6947d8bb7ed2d28911904e1dc20c0fe3257c3a86b010c833bb35acda", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "900b8d7a9cf4d32be98e8f84e2b720c40133d0477e78e83e41d81e0fe00d9b18"}, + "peri": {:hex, :peri, "0.5.0", "c71e57d1c9abd26ae05f82cefb3a3f19ec2cf19602385a329843679af15a3082", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "526a93bfae9ba567f7cb0e87694de68b9e708e038a2cec7a3001851bcd4bfe71"}, "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [: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", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [: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", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [: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", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.12", "a37134b9bb3602efbfa5a7a8cb51d50e796f7acff7075af9d9796f30de04c66a", [: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 or ~> 1.8.0-rc", [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", "058e06e59fd38f1feeca59bbf167bec5d44aacd9b745e4363e2ac342ca32e546"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [: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 or ~> 1.8.0-rc", [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", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"}, "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.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [: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", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [: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", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.1", "f1b318827b6c7b74560004eb50ec67c0412a53f7688caf0b08fd703e1281852d", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "efb4f284665fcf9bdd5bca47604d49c5bc22fbc7330a8602b7794e9b90debef7"}, + "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.2", "969dca930672db4a3a003ef08d44d43519c4da572e4eae0ec15a3ba9c613f898", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "a2295a655cf3850eaf191f681970f0b409423afdc8897485203e9ebf1b594277"}, "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, - "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, + "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, } diff --git a/examples/user_management/supabase/.temp/cli-latest b/examples/user_management/supabase/.temp/cli-latest index 624e112..868afb7 100644 --- a/examples/user_management/supabase/.temp/cli-latest +++ b/examples/user_management/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.23.4 \ No newline at end of file +v2.26.9 \ No newline at end of file From 9cdaef11fb5cceaa5c718f4750bb70a908c92cec Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sun, 29 Jun 2025 21:07:36 -0300 Subject: [PATCH 24/27] wip: fixes to a working example --- .../lib/user_management/profiles.ex | 2 +- .../lib/user_management/profiles/profile.ex | 9 +++--- .../controllers/session_controller.ex | 28 +++++++++---------- .../user_management_web/live/profile_live.ex | 20 +------------ .../lib/user_management_web/router.ex | 12 ++++++-- examples/user_management/mix.exs | 2 +- examples/user_management/mix.lock | 2 +- examples/user_management/supabase/config.toml | 16 +++++++++++ .../supabase/templates/confirmation.html | 21 ++++++++++++++ .../supabase/templates/invite.html | 21 ++++++++++++++ .../supabase/templates/magic_link.html | 21 ++++++++++++++ .../supabase/templates/recovery.html | 21 ++++++++++++++ 12 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 examples/user_management/supabase/templates/confirmation.html create mode 100644 examples/user_management/supabase/templates/invite.html create mode 100644 examples/user_management/supabase/templates/magic_link.html create mode 100644 examples/user_management/supabase/templates/recovery.html diff --git a/examples/user_management/lib/user_management/profiles.ex b/examples/user_management/lib/user_management/profiles.ex index 442fbb1..765d4a2 100644 --- a/examples/user_management/lib/user_management/profiles.ex +++ b/examples/user_management/lib/user_management/profiles.ex @@ -18,7 +18,7 @@ defmodule UserManagement.Profiles do Gets a single profile by user_id. """ def get_profile_by_user_id(user_id) do - Repo.get_by(Profile, user_id: user_id) + Repo.get_by(Profile, id: user_id) end @doc """ diff --git a/examples/user_management/lib/user_management/profiles/profile.ex b/examples/user_management/lib/user_management/profiles/profile.ex index 8332549..e16dd98 100644 --- a/examples/user_management/lib/user_management/profiles/profile.ex +++ b/examples/user_management/lib/user_management/profiles/profile.ex @@ -2,7 +2,7 @@ defmodule UserManagement.Profiles.Profile do use Ecto.Schema import Ecto.Changeset - @primary_key {:id, :binary_id, autogenerate: true} + @primary_key {:id, :binary_id, autogenerate: false} @foreign_key_type :binary_id schema "profiles" do field :username, :string @@ -15,10 +15,11 @@ defmodule UserManagement.Profiles.Profile do @doc false def changeset(profile, attrs) do profile - |> cast(attrs, [:user_id, :username, :website, :avatar_url]) - |> validate_required([:user_id]) - |> unique_constraint(:user_id) + |> cast(attrs, [:id, :username, :website, :avatar_url]) + |> validate_required([:id]) + |> unique_constraint(:id) |> unique_constraint(:username) + |> foreign_key_constraint(:id) |> validate_format(:website, ~r/^https?:\/\//, message: "must start with http:// or https://") end diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex index 1fa5457..701fd32 100644 --- a/examples/user_management/lib/user_management_web/controllers/session_controller.ex +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -2,6 +2,7 @@ defmodule UserManagementWeb.SessionController do use UserManagementWeb, :controller alias UserManagement.Profiles + alias UserManagementWeb.UserAuth def create(conn, %{"_action" => "confirmed"} = params) do create(conn, params, "User confirmed successfully.") @@ -11,8 +12,14 @@ defmodule UserManagementWeb.SessionController do create(conn, params, "Welcome back!") end - defp create(conn, params, info) do - with {:ok, conn} <- log_in_with_strategy(conn, params) do + def token(conn, %{"token_hash" => _} = params) do + create(conn, %{"user" => params}) + end + + defp create(conn, %{"user" => %{"token_hash" => token}}, info) when is_binary(token) do + params = %{token_hash: token, type: :magiclink} + + with {:ok, conn} <- UserAuth.verify_otp_and_log_in(conn, params) do conn |> maybe_create_profile() |> put_flash(:info, info) @@ -25,16 +32,16 @@ defmodule UserManagementWeb.SessionController do end defp maybe_create_profile(conn) do + dbg(conn.assigns) + if user = conn.assigns[:current_user] do case Profiles.get_profile_by_user_id(user.id) do nil -> email = user.email || "user-#{user.id}" username = make_username(email) + params = %{"id" => user.id, "username" => username} - UserManagement.Profiles.create_profile(%{ - "user_id" => user.id, - "username" => username - }) + UserManagement.Profiles.create_profile(params) _profile -> :ok @@ -46,18 +53,9 @@ defmodule UserManagementWeb.SessionController do email |> String.split("@") |> hd() |> String.replace(~r/[^a-zA-Z0-9]/, "") end - def token(conn, %{"token" => token} = params) do - create(conn, Map.put(params, "token", token)) - end - def delete(conn, _params) do conn |> put_flash(:info, "Logged out successfully.") |> UserManagementWeb.UserAuth.log_out_user(:global) end - - def log_in_with_strategy(conn, %{"user" => %{"token" => token}}) - when is_binary(token) do - UserManagementWeb.UserAuth.log_in_user_with_otp(conn, %{"token" => token}) - end end diff --git a/examples/user_management/lib/user_management_web/live/profile_live.ex b/examples/user_management/lib/user_management_web/live/profile_live.ex index b6c593e..0e0afda 100644 --- a/examples/user_management/lib/user_management_web/live/profile_live.ex +++ b/examples/user_management/lib/user_management_web/live/profile_live.ex @@ -7,25 +7,7 @@ defmodule UserManagementWeb.ProfileLive do @impl true def mount(_params, _session, socket) do if user = socket.assigns.current_user do - profile = - case Profiles.get_profile_by_user_id(user.id) do - nil -> - # Create profile if it doesn't exist yet - email = user.email || "user-#{user.id}" - username = email |> String.split("@") |> hd() |> String.replace(~r/[^a-zA-Z0-9]/, "") - - {:ok, profile} = - Profiles.create_profile(%{ - "user_id" => user.id, - "username" => username - }) - - profile - - profile -> - profile - end - + profile = Profiles.get_profile_by_user_id(user.id) changeset = Profiles.change_profile(profile) {:ok, diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index 52571ff..a20766a 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -22,14 +22,21 @@ defmodule UserManagementWeb.Router do scope "/", UserManagementWeb do pipe_through [:browser, :require_authenticated_user] - live "/profile", ProfileLive, :index + live_session :authenticated, + on_mount: [ + {UserManagementWeb.UserAuth, :mount_current_user}, + {UserManagementWeb.UserAuth, :ensure_authenticated} + ] do + live "/profile", ProfileLive, :index + end + delete "/logout", SessionController, :delete end scope "/", UserManagementWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] - live_session :current_user, + live_session :non_authenticated, on_mount: [ {UserManagementWeb.UserAuth, :mount_current_user}, {UserManagementWeb.UserAuth, :redirect_if_user_is_authenticated} @@ -39,5 +46,6 @@ defmodule UserManagementWeb.Router do post "/", SessionController, :create post "/:token", SessionController, :token + get "/sessions/token", SessionController, :token end end diff --git a/examples/user_management/mix.exs b/examples/user_management/mix.exs index fa4ecd5..2d0ee69 100644 --- a/examples/user_management/mix.exs +++ b/examples/user_management/mix.exs @@ -42,7 +42,7 @@ defmodule UserManagement.MixProject do {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:supabase_potion, "~> 0.6"}, - {:supabase_gotrue, "~> 0.5"}, + {:supabase_gotrue, github: "supabase-community/auth-ex", branch: "fix/31"}, {:supabase_storage, "~> 0.4"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index d6d3699..a661a29 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -28,7 +28,7 @@ "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [: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", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "supabase_gotrue": {:hex, :supabase_gotrue, "0.5.2", "969dca930672db4a3a003ef08d44d43519c4da572e4eae0ec15a3ba9c613f898", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "a2295a655cf3850eaf191f681970f0b409423afdc8897485203e9ebf1b594277"}, + "supabase_gotrue": {:git, "https://github.com/supabase-community/auth-ex.git", "cae3227a91e44f1ac065f2fa3b985de89c1e34b4", [branch: "fix/31"]}, "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, diff --git a/examples/user_management/supabase/config.toml b/examples/user_management/supabase/config.toml index 13eae1f..f8d79f5 100644 --- a/examples/user_management/supabase/config.toml +++ b/examples/user_management/supabase/config.toml @@ -25,6 +25,22 @@ enable_signup = true double_confirm_changes = true enable_confirmations = false +[auth.email.template.invite] +subject = "You have been invited" +content_path = "./supabase/templates/invite.html" + +[auth.email.template.confirmation] +subject = "Confirm your signup" +content_path = "./supabase/templates/confirmation.html" + +[auth.email.template.recovery] +subject = "Reset your password" +content_path = "./supabase/templates/recovery.html" + +[auth.email.template.magic_link] +subject = "Your Magic Link" +content_path = "./supabase/templates/magic_link.html" + [storage] file_size_limit = "50MiB" diff --git a/examples/user_management/supabase/templates/confirmation.html b/examples/user_management/supabase/templates/confirmation.html new file mode 100644 index 0000000..7cfd26f --- /dev/null +++ b/examples/user_management/supabase/templates/confirmation.html @@ -0,0 +1,21 @@ + + + + + + Confirm your signup + + +
+ +
+ + \ No newline at end of file diff --git a/examples/user_management/supabase/templates/invite.html b/examples/user_management/supabase/templates/invite.html new file mode 100644 index 0000000..9aa54fc --- /dev/null +++ b/examples/user_management/supabase/templates/invite.html @@ -0,0 +1,21 @@ + + + + + + You have been invited + + +
+ +
+ + \ No newline at end of file diff --git a/examples/user_management/supabase/templates/magic_link.html b/examples/user_management/supabase/templates/magic_link.html new file mode 100644 index 0000000..119803a --- /dev/null +++ b/examples/user_management/supabase/templates/magic_link.html @@ -0,0 +1,21 @@ + + + + + + Magic Link + + +
+ +
+ + \ No newline at end of file diff --git a/examples/user_management/supabase/templates/recovery.html b/examples/user_management/supabase/templates/recovery.html new file mode 100644 index 0000000..05d3ea2 --- /dev/null +++ b/examples/user_management/supabase/templates/recovery.html @@ -0,0 +1,21 @@ + + + + + + Reset your password + + +
+ +
+ + \ No newline at end of file From 2249d6754e4dfe32545522a0ee342853fdfbb887 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sun, 29 Jun 2025 21:23:08 -0300 Subject: [PATCH 25/27] wip: more minor fixes for a working example --- .../controllers/session_controller.ex | 51 ++++-------- .../live/registration_live.ex | 81 ------------------- .../lib/user_management_web/user_auth.ex | 19 +++-- examples/user_management/mix.exs | 2 +- examples/user_management/mix.lock | 2 +- .../user_management/test/support/conn_case.ex | 25 ++++++ 6 files changed, 57 insertions(+), 123 deletions(-) delete mode 100644 examples/user_management/lib/user_management_web/live/registration_live.ex diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex index 701fd32..25c355b 100644 --- a/examples/user_management/lib/user_management_web/controllers/session_controller.ex +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -1,9 +1,6 @@ defmodule UserManagementWeb.SessionController do use UserManagementWeb, :controller - alias UserManagement.Profiles - alias UserManagementWeb.UserAuth - def create(conn, %{"_action" => "confirmed"} = params) do create(conn, params, "User confirmed successfully.") end @@ -12,45 +9,19 @@ defmodule UserManagementWeb.SessionController do create(conn, params, "Welcome back!") end - def token(conn, %{"token_hash" => _} = params) do - create(conn, %{"user" => params}) - end - - defp create(conn, %{"user" => %{"token_hash" => token}}, info) when is_binary(token) do - params = %{token_hash: token, type: :magiclink} - - with {:ok, conn} <- UserAuth.verify_otp_and_log_in(conn, params) do - conn - |> maybe_create_profile() - |> put_flash(:info, info) + defp create(conn, params, info) do + with {:ok, conn} <- log_in_with_strategy(conn, params) do + put_flash(conn, :info, info) else _ -> conn |> put_flash(:error, "Invalid credentials") - |> redirect(to: ~p"/") - end - end - - defp maybe_create_profile(conn) do - dbg(conn.assigns) - - if user = conn.assigns[:current_user] do - case Profiles.get_profile_by_user_id(user.id) do - nil -> - email = user.email || "user-#{user.id}" - username = make_username(email) - params = %{"id" => user.id, "username" => username} - - UserManagement.Profiles.create_profile(params) - - _profile -> - :ok - end + |> redirect(to: ~p"/login") end end - defp make_username(email) do - email |> String.split("@") |> hd() |> String.replace(~r/[^a-zA-Z0-9]/, "") + def token(conn, params) do + create(conn, %{"user" => params}) end def delete(conn, _params) do @@ -58,4 +29,14 @@ defmodule UserManagementWeb.SessionController do |> put_flash(:info, "Logged out successfully.") |> UserManagementWeb.UserAuth.log_out_user(:global) end + + def log_in_with_strategy(conn, %{"user" => %{"token_hash" => token, "type" => type}}) + when is_binary(token) do + UserManagementWeb.UserAuth.verify_otp_and_log_in(conn, %{token_hash: token, type: type}) + end + + def log_in_with_strategy(conn, %{"user" => %{"token" => token}}) + when is_binary(token) do + UserManagementWeb.UserAuth.log_in_user_with_otp(conn, %{"token" => token}) + end end diff --git a/examples/user_management/lib/user_management_web/live/registration_live.ex b/examples/user_management/lib/user_management_web/live/registration_live.ex deleted file mode 100644 index ec49e9a..0000000 --- a/examples/user_management/lib/user_management_web/live/registration_live.ex +++ /dev/null @@ -1,81 +0,0 @@ -defmodule UserManagementWeb.RegistrationLive do - use UserManagementWeb, :live_view - - @impl true - def render(assigns) do - ~H""" -
- <.header class="text-center"> - Register - <:subtitle> - Already have an account? - <.link navigate={~p"/"} class="font-semibold text-brand hover:underline"> - Sign in - - to your account now. - - - - <.simple_form - for={@form} - id="registration_form" - phx-submit="save" - phx-change="validate" - as={:user} - > - <.error :if={@check_errors}> - Oops, something went wrong! Please check the errors below. - - - <.input field={@form[:email]} type="email" label="Email" required /> - <.input field={@form[:password]} type="password" label="Password" required /> - - <:actions> - <.button phx-disable-with="Creating account..." class="w-full"> - Create an account - - - -
- """ - end - - @impl true - def mount(_params, _session, socket) do - {:ok, - socket - |> assign(check_errors: false) - |> assign_form()} - end - - @impl true - def handle_event("save", %{"user" => user_params}, socket) do - {:ok, client} = UserManagementWeb.UserAuth.get_client() - %{"email" => email, "password" => password} = user_params - - case Supabase.GoTrue.sign_up(client, %{email: email, password: password}) do - {:ok, _session} -> - {:noreply, - socket - |> put_flash(:info, "User created successfully. Please sign in.") - |> push_navigate(to: ~p"/")} - - {:error, %Supabase.Error{metadata: metadata}} -> - message = get_in(metadata, [:resp_body, "msg"]) - - {:noreply, - socket - |> put_flash(:error, "Registration failed: #{message}") - |> assign(check_errors: true) - |> assign_form()} - end - end - - def handle_event("validate", %{"user" => user_params}, socket) do - {:noreply, assign_form(socket, user_params)} - end - - defp assign_form(socket, user_params \\ %{}) do - assign(socket, :form, to_form(user_params, as: "user")) - end -end diff --git a/examples/user_management/lib/user_management_web/user_auth.ex b/examples/user_management/lib/user_management_web/user_auth.ex index d0b1c3b..fb8ff6a 100644 --- a/examples/user_management/lib/user_management_web/user_auth.ex +++ b/examples/user_management/lib/user_management_web/user_auth.ex @@ -35,6 +35,14 @@ defmodule UserManagementWeb.UserAuth do end end + @doc "Verifies an OTP token and logs the user in.\n" <> @extra_login_doc + def verify_otp_and_log_in(conn, params) do + with {:ok, client} <- get_client(), + {:ok, session} <- GoTrue.verify_otp(client, params) do + do_login(conn, session, params) + end + end + defp do_login(conn, session, params) do user_return_to = get_session(conn, :user_return_to) @@ -42,14 +50,15 @@ defmodule UserManagementWeb.UserAuth do |> renew_session() |> put_token_in_session(session.access_token) |> maybe_write_remember_me_cookie(session, params) + |> fetch_current_user([]) |> redirect(to: user_return_to || signed_in_path()) end - defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do - put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) + defp maybe_write_remember_me_cookie(conn, session, %{"remember_me" => "true"}) do + put_resp_cookie(conn, @remember_me_cookie, session.access_token, @remember_me_options) end - defp maybe_write_remember_me_cookie(conn, _token, _params) do + defp maybe_write_remember_me_cookie(conn, _session, _params) do conn end @@ -248,7 +257,7 @@ defmodule UserManagementWeb.UserAuth do defp maybe_store_return_to(conn), do: conn - defp signed_in_path(), do: ~p"/profile" + defp signed_in_path(), do: ~p"/" - def get_client, do: UserManagement.Supabase.get_client() + def get_client, do: Elixir.UserManagement.Supabase.get_client() end diff --git a/examples/user_management/mix.exs b/examples/user_management/mix.exs index 2d0ee69..8e5dfeb 100644 --- a/examples/user_management/mix.exs +++ b/examples/user_management/mix.exs @@ -42,7 +42,7 @@ defmodule UserManagement.MixProject do {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:supabase_potion, "~> 0.6"}, - {:supabase_gotrue, github: "supabase-community/auth-ex", branch: "fix/31"}, + {:supabase_gotrue, github: "supabase-community/auth-ex", branch: "fix/31", rev: "bf2cd9d"}, {:supabase_storage, "~> 0.4"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index a661a29..643f991 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -28,7 +28,7 @@ "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [: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", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "supabase_gotrue": {:git, "https://github.com/supabase-community/auth-ex.git", "cae3227a91e44f1ac065f2fa3b985de89c1e34b4", [branch: "fix/31"]}, + "supabase_gotrue": {:git, "https://github.com/supabase-community/auth-ex.git", "bf2cd9d40b2035c7c2db2a048706381cc054357e", [branch: "fix/31"]}, "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, diff --git a/examples/user_management/test/support/conn_case.ex b/examples/user_management/test/support/conn_case.ex index 302f98c..a04fd56 100644 --- a/examples/user_management/test/support/conn_case.ex +++ b/examples/user_management/test/support/conn_case.ex @@ -60,4 +60,29 @@ defmodule UserManagementWeb.ConnCase do |> Phoenix.ConnTest.put_req_cookie("_user_management_web_user_remember_me", token) |> UserManagementWeb.UserAuth.fetch_current_user([]) end + + @doc """ + Setup helper that registers and logs in users. + + setup :register_and_log_in_user + + It stores an updated connection and a registered user in the + test context. + """ + def register_and_log_in_user(%{conn: conn}) do + user = %Supabase.GoTrue.User{id: Ecto.UUID.generate(), email: "user@example.com"} + session = %Supabase.GoTrue.Session{access_token: "123"} + %{conn: log_in_user(conn, session), user: user, session: session} + end + + def log_in_user(conn, session) do + token = session.access_token + + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> Plug.Conn.put_session(:user_token, token) + |> Plug.Conn.put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") + |> Phoenix.ConnTest.put_req_cookie("_user_management_web_user_remember_me", token) + |> Elixir.UserManagementWeb.UserAuth.fetch_current_user([]) + end end From f9c830a41e217598010538f3d92318d5b5ba4ba6 Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sun, 20 Jul 2025 11:57:00 -0300 Subject: [PATCH 26/27] wip --- examples/user_management/mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index 643f991..d5d8611 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -8,7 +8,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [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", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, - "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"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [: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", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, @@ -17,7 +17,7 @@ "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [: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", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "peri": {:hex, :peri, "0.5.0", "c71e57d1c9abd26ae05f82cefb3a3f19ec2cf19602385a329843679af15a3082", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "526a93bfae9ba567f7cb0e87694de68b9e708e038a2cec7a3001851bcd4bfe71"}, + "peri": {:hex, :peri, "0.6.0", "0758aa037f862f7a3aa0823cb82195916f61a8071f6eaabcff02103558e61a70", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "b27f118f3317fbc357c4a04b3f3c98561efdd8865edd4ec0e24fd936c7ff36c8"}, "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [: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", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [: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", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, @@ -25,7 +25,7 @@ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [: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 or ~> 1.8.0-rc", [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", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"}, "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.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [: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", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [: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", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, "supabase_gotrue": {:git, "https://github.com/supabase-community/auth-ex.git", "bf2cd9d40b2035c7c2db2a048706381cc054357e", [branch: "fix/31"]}, @@ -34,7 +34,7 @@ "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, From ac1ed9f83675cbdbd3e1bba39c0dfb9015639e2e Mon Sep 17 00:00:00 2001 From: Zoey Pessanha Date: Sun, 20 Jul 2025 18:13:49 -0300 Subject: [PATCH 27/27] chore: minor fixes --- examples/user_management/config/runtime.exs | 2 +- .../controllers/session_controller.ex | 9 +- .../user_management_web/live/login_live.ex | 124 +++++++++++------- .../lib/user_management_web/router.ex | 16 +-- .../lib/user_management_web/user_auth.ex | 16 +-- examples/user_management/mix.exs | 3 +- examples/user_management/mix.lock | 1 + .../user_management/test/support/conn_case.ex | 25 ++++ 8 files changed, 124 insertions(+), 72 deletions(-) diff --git a/examples/user_management/config/runtime.exs b/examples/user_management/config/runtime.exs index 3516f3b..3634f04 100644 --- a/examples/user_management/config/runtime.exs +++ b/examples/user_management/config/runtime.exs @@ -24,7 +24,7 @@ config :user_management, UserManagement.Supabase, base_url: System.fetch_env!("SUPABASE_URL"), api_key: System.fetch_env!("SUPABASE_KEY") -config :supabase_gotrue, auth_module: UserManagementWeb.Auth +config :supabase_auth, auth_module: UserManagementWeb.Auth if config_env() == :prod do database_url = diff --git a/examples/user_management/lib/user_management_web/controllers/session_controller.ex b/examples/user_management/lib/user_management_web/controllers/session_controller.ex index 25c355b..9cb0d75 100644 --- a/examples/user_management/lib/user_management_web/controllers/session_controller.ex +++ b/examples/user_management/lib/user_management_web/controllers/session_controller.ex @@ -16,11 +16,11 @@ defmodule UserManagementWeb.SessionController do _ -> conn |> put_flash(:error, "Invalid credentials") - |> redirect(to: ~p"/login") + |> redirect(to: ~p"/") end end - def token(conn, params) do + def token(conn, %{} = params) do create(conn, %{"user" => params}) end @@ -35,8 +35,7 @@ defmodule UserManagementWeb.SessionController do UserManagementWeb.UserAuth.verify_otp_and_log_in(conn, %{token_hash: token, type: type}) end - def log_in_with_strategy(conn, %{"user" => %{"token" => token}}) - when is_binary(token) do - UserManagementWeb.UserAuth.log_in_user_with_otp(conn, %{"token" => token}) + def log_in_with_strategy(conn, %{"user" => %{} = params}) do + UserManagementWeb.UserAuth.log_in_user_with_otp(conn, params) end end diff --git a/examples/user_management/lib/user_management_web/live/login_live.ex b/examples/user_management/lib/user_management_web/live/login_live.ex index 55e8775..316edaa 100644 --- a/examples/user_management/lib/user_management_web/live/login_live.ex +++ b/examples/user_management/lib/user_management_web/live/login_live.ex @@ -4,60 +4,96 @@ defmodule UserManagementWeb.LoginLive do @impl true def render(assigns) do ~H""" -
-
- <.header> - Supabase + Phoenix - <:subtitle> - Sign in via magic link with your email below - - - - <.simple_form for={@form} id="login_form" phx-submit="send_magic_link" as={:user}> - <.input field={@form[:email]} type="email" placeholder="Your email" required /> - <:actions> - <.button class="block" phx-disable-with="Loading..."> - {if @loading, do: "Loading", else: "Send magic link"} - - - - - <.flash_group flash={@flash} /> +
+ <.header class="text-center"> +

Log in

+ <:subtitle> + Don't have an account? + <.link navigate={~p"/register"} class="font-semibold text-brand hover:underline"> + Sign up + + for an account now. + + + + <.login_form :for={s <- ["otp"]} strategy={s} form={@form} /> +
+ """ + end + + def login_form(%{strategy: "password"} = assigns) do + ~H""" + <.form :let={f} for={@form} id="login_form_password" action={~p"/"} as={:user}> + <.input field={f[:email]} type="email" label="Email" autocomplete="username" required /> + <.input + field={f[:password]} + type="password" + label="Password" + autocomplete="current-password" + required + /> + <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" /> + <.button class="w-full"> + Log in with password + + + """ + end + + def login_form(%{strategy: "otp"} = assigns) do + ~H""" + <.form :let={f} for={@form} id="login_form_otp" action={~p"/"} as={:user}> + <.input field={f[:email]} type="email" label="Email" required /> + <.button class="w-full"> + Send one-time password + + + """ + end + + def login_form(%{strategy: "oauth"} = assigns) do + ~H""" +
+

Log in with a provider

+
+ <.button + :for={provider <- ["github", "google", "facebook"]} + class="w-full" + phx-click="oauth_login" + phx-value-provider={provider} + > + {provider} +
""" end + def login_form(%{strategy: "anon"} = assigns) do + ~H""" + <.form for={%{}} id="login_form_anon" action={~p"/"} as={:user}> + <.button class="w-full"> + Continue anonymously + + + """ + end + + def login_form(%{strategy: _} = assigns) do + ~H""" +

Strategy not implemented in LiveView yet

+ """ + end + @impl true def mount(_params, _session, socket) do form = to_form(%{"email" => nil}, as: "user") - {:ok, assign(socket, form: form, loading: false)} + {:ok, assign(socket, form: form)} end @impl true - def handle_event("send_magic_link", %{"user" => %{"email" => email}}, socket) do - # Set loading state - socket = assign(socket, loading: true) - - {:ok, client} = UserManagementWeb.UserAuth.get_client() - - case Supabase.GoTrue.sign_in_with_otp(client, %{email: email}) do - :ok -> - {:noreply, - socket - |> assign(loading: false) - |> put_flash( - :info, - "Magic link sent to #{email}. Check your inbox and follow the link to sign in." - )} - - {:error, %Supabase.Error{metadata: metadata}} -> - message = get_in(metadata, [:resp_body, "msg"]) || "Unknown error" - - {:noreply, - socket - |> assign(loading: false) - |> put_flash(:error, "Couldn't send magic link: #{message}")} - end + def handle_event("oauth_login", %{"provider" => provider}, socket) do + # This would be handled by the controller + {:noreply, push_navigate(socket, to: ~p"/?provider=#{provider}")} end end diff --git a/examples/user_management/lib/user_management_web/router.ex b/examples/user_management/lib/user_management_web/router.ex index a20766a..5d10dcf 100644 --- a/examples/user_management/lib/user_management_web/router.ex +++ b/examples/user_management/lib/user_management_web/router.ex @@ -1,7 +1,6 @@ defmodule UserManagementWeb.Router do use UserManagementWeb, :router - # Import authentication plugs import UserManagementWeb.UserAuth pipeline :browser do @@ -19,24 +18,16 @@ defmodule UserManagementWeb.Router do end ## Authentication routes - scope "/", UserManagementWeb do + scope "/", Elixir.UserManagementWeb do pipe_through [:browser, :require_authenticated_user] - live_session :authenticated, - on_mount: [ - {UserManagementWeb.UserAuth, :mount_current_user}, - {UserManagementWeb.UserAuth, :ensure_authenticated} - ] do - live "/profile", ProfileLive, :index - end - delete "/logout", SessionController, :delete end - scope "/", UserManagementWeb do + scope "/", Elixir.UserManagementWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] - live_session :non_authenticated, + live_session :current_user, on_mount: [ {UserManagementWeb.UserAuth, :mount_current_user}, {UserManagementWeb.UserAuth, :redirect_if_user_is_authenticated} @@ -46,6 +37,5 @@ defmodule UserManagementWeb.Router do post "/", SessionController, :create post "/:token", SessionController, :token - get "/sessions/token", SessionController, :token end end diff --git a/examples/user_management/lib/user_management_web/user_auth.ex b/examples/user_management/lib/user_management_web/user_auth.ex index fb8ff6a..d08ad86 100644 --- a/examples/user_management/lib/user_management_web/user_auth.ex +++ b/examples/user_management/lib/user_management_web/user_auth.ex @@ -4,10 +4,10 @@ defmodule UserManagementWeb.UserAuth do import Plug.Conn import Phoenix.Controller - alias Supabase.GoTrue - alias Supabase.GoTrue.Admin - alias Supabase.GoTrue.Session - alias Supabase.GoTrue.User + alias Supabase.Auth + alias Supabase.Auth.Admin + alias Supabase.Auth.Session + alias Supabase.Auth.User # Make the remember me cookie valid for 60 days. # If you want bump or reduce this value, also change @@ -30,7 +30,7 @@ defmodule UserManagementWeb.UserAuth do @doc "Logs the User in using the otp strategy.\n" <> @extra_login_doc def log_in_user_with_otp(conn, params \\ %{}) do with {:ok, client} <- get_client(), - {:ok, session} <- GoTrue.sign_in_with_otp(client, params) do + {:ok, session} <- Auth.sign_in_with_otp(client, params) do do_login(conn, session, params) end end @@ -38,7 +38,7 @@ defmodule UserManagementWeb.UserAuth do @doc "Verifies an OTP token and logs the user in.\n" <> @extra_login_doc def verify_otp_and_log_in(conn, params) do with {:ok, client} <- get_client(), - {:ok, session} <- GoTrue.verify_otp(client, params) do + {:ok, session} <- Auth.verify_otp(client, params) do do_login(conn, session, params) end end @@ -116,7 +116,7 @@ defmodule UserManagementWeb.UserAuth do defp fetch_user_from_session_token(user_token) do {:ok, client} = get_client() - case GoTrue.get_user(client, %Session{access_token: user_token}) do + case Auth.get_user(client, %Session{access_token: user_token}) do {:ok, %User{} = user} -> user _ -> nil end @@ -211,7 +211,7 @@ defmodule UserManagementWeb.UserAuth do defp maybe_get_current_user(session) do {:ok, client} = get_client() - case GoTrue.get_user(client, session) do + case Auth.get_user(client, session) do {:ok, %User{} = user} -> user _ -> nil end diff --git a/examples/user_management/mix.exs b/examples/user_management/mix.exs index 8e5dfeb..e02407b 100644 --- a/examples/user_management/mix.exs +++ b/examples/user_management/mix.exs @@ -42,7 +42,8 @@ defmodule UserManagement.MixProject do {:floki, ">= 0.30.0", only: :test}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:supabase_potion, "~> 0.6"}, - {:supabase_gotrue, github: "supabase-community/auth-ex", branch: "fix/31", rev: "bf2cd9d"}, + # {:supabase_auth, "~> 0.6"}, + {:supabase_auth, path: "../../../auth-ex"}, {:supabase_storage, "~> 0.4"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, diff --git a/examples/user_management/mix.lock b/examples/user_management/mix.lock index d5d8611..0d55632 100644 --- a/examples/user_management/mix.lock +++ b/examples/user_management/mix.lock @@ -28,6 +28,7 @@ "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [: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", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "supabase_auth": {:hex, :supabase_auth, "0.6.1", "05c332315d83febb562b6734664269969831d902b5afe07657377071b3889c9b", [:mix], [{:peri, "~> 0.3", [hex: :peri, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "f5c14bb1433575fa4d2df75ad691d0dbb39c607845f0bb0e85dc240d90f8a32a"}, "supabase_gotrue": {:git, "https://github.com/supabase-community/auth-ex.git", "bf2cd9d40b2035c7c2db2a048706381cc054357e", [branch: "fix/31"]}, "supabase_potion": {:hex, :supabase_potion, "0.6.2", "5014072df37074de09624dcf2c0a37c89998929156f07880e427c65943966c97", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c9d55ef7240bd5ca6e057e7896917e12e3decab5ad65aa6cc56c3242c0137995"}, "supabase_storage": {:hex, :supabase_storage, "0.4.2", "a066c6dc99cd86def87ec65674dd872d035193de16a1026d4aed788fb854a2c8", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_potion, "~> 0.6", [hex: :supabase_potion, repo: "hexpm", optional: false]}], "hexpm", "0c6b89232bd118e33985725908ba3140bb8449cb4331d72e377a4c6db586d63b"}, diff --git a/examples/user_management/test/support/conn_case.ex b/examples/user_management/test/support/conn_case.ex index a04fd56..5b62d87 100644 --- a/examples/user_management/test/support/conn_case.ex +++ b/examples/user_management/test/support/conn_case.ex @@ -85,4 +85,29 @@ defmodule UserManagementWeb.ConnCase do |> Phoenix.ConnTest.put_req_cookie("_user_management_web_user_remember_me", token) |> Elixir.UserManagementWeb.UserAuth.fetch_current_user([]) end + + @doc """ + Setup helper that registers and logs in users. + + setup :register_and_log_in_user + + It stores an updated connection and a registered user in the + test context. + """ + def register_and_log_in_user(%{conn: conn}) do + user = %Supabase.Auth.User{id: Ecto.UUID.generate(), email: "user@example.com"} + session = %Supabase.Auth.Session{access_token: "123"} + %{conn: log_in_user(conn, session), user: user, session: session} + end + + def log_in_user(conn, session) do + token = session.access_token + + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> Plug.Conn.put_session(:user_token, token) + |> Plug.Conn.put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") + |> Phoenix.ConnTest.put_req_cookie("_user_management_web_user_remember_me", token) + |> Elixir.UserManagementWeb.UserAuth.fetch_current_user([]) + end end