From 73a68d98756eab0aafd33aa36e08e2f4857544cd Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Sun, 12 May 2024 11:06:15 -0400 Subject: [PATCH 01/26] nix shell for elixir --- src/chatservice/README.md | 3 + src/chatservice/nix/default.nix | 12 ++ src/chatservice/nix/sources.json | 14 +++ src/chatservice/nix/sources.nix | 198 +++++++++++++++++++++++++++++++ src/chatservice/shell.nix | 21 ++++ 5 files changed, 248 insertions(+) create mode 100644 src/chatservice/README.md create mode 100644 src/chatservice/nix/default.nix create mode 100644 src/chatservice/nix/sources.json create mode 100644 src/chatservice/nix/sources.nix create mode 100644 src/chatservice/shell.nix diff --git a/src/chatservice/README.md b/src/chatservice/README.md new file mode 100644 index 0000000000..b76dd513bc --- /dev/null +++ b/src/chatservice/README.md @@ -0,0 +1,3 @@ +## Nix +If you have nix installed, you can use `nix-shell` for a temporary shell +that will include elixir and OTel Desktop Viewer. diff --git a/src/chatservice/nix/default.nix b/src/chatservice/nix/default.nix new file mode 100644 index 0000000000..1224bf1cf9 --- /dev/null +++ b/src/chatservice/nix/default.nix @@ -0,0 +1,12 @@ +{ sources ? import ./sources.nix +, pkgs ? import sources.nixpkgs { } +}: + +with pkgs; + +buildEnv { + name = "builder"; + paths = [ + elixir + ]; +} diff --git a/src/chatservice/nix/sources.json b/src/chatservice/nix/sources.json new file mode 100644 index 0000000000..711b141b48 --- /dev/null +++ b/src/chatservice/nix/sources.json @@ -0,0 +1,14 @@ +{ + "nixpkgs": { + "branch": "nixos-unstable", + "description": "Nix Packages collection", + "homepage": null, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6c43a3495a11e261e5f41e5d7eda2d71dae1b2fe", + "sha256": "16f329z831bq7l3wn1dfvbkh95l2gcggdwn6rk3cisdmv2aa3189", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/6c43a3495a11e261e5f41e5d7eda2d71dae1b2fe.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/src/chatservice/nix/sources.nix b/src/chatservice/nix/sources.nix new file mode 100644 index 0000000000..fe3dadf7eb --- /dev/null +++ b/src/chatservice/nix/sources.nix @@ -0,0 +1,198 @@ +# This file has been generated by Niv. + +let + + # + # The fetchers. fetch_ fetches specs of type . + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + spec.ref or ( + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" + ); + submodules = spec.submodules or false; + submoduleArg = + let + nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; + emptyArgWithWarning = + if submodules + then + builtins.trace + ( + "The niv input \"${name}\" uses submodules " + + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " + + "does not support them" + ) + { } + else { }; + in + if nixSupportsSubmodules + then { inherit submodules; } + else emptyArgWithWarning; + in + builtins.fetchGit + ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import { } + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else { }; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/src/chatservice/shell.nix b/src/chatservice/shell.nix new file mode 100644 index 0000000000..62bbac3a9c --- /dev/null +++ b/src/chatservice/shell.nix @@ -0,0 +1,21 @@ +{ sources ? import ./nix/sources.nix +, pkgs ? import { } +}: + +with pkgs; +let + inherit (lib) optional optionals; +in + +mkShell { + buildInputs = [ + (import ./nix/default.nix { inherit pkgs; }) + niv + otel-desktop-viewer + ] ++ optional stdenv.isLinux inotify-tools + ++ optional stdenv.isDarwin terminal-notifier + ++ optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ + CoreFoundation + CoreServices + ]); +} From 4eb36277b0f9adcc2fd225fba9ffd91ecc2773a9 Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Sun, 12 May 2024 11:09:11 -0400 Subject: [PATCH 02/26] mix phx.new --- src/chatservice/.formatter.exs | 6 + src/chatservice/.gitignore | 37 + src/chatservice/README.md | 21 +- src/chatservice/assets/css/app.css | 5 + src/chatservice/assets/js/app.js | 44 ++ src/chatservice/assets/tailwind.config.js | 75 ++ src/chatservice/config/config.exs | 66 ++ src/chatservice/config/dev.exs | 82 +++ src/chatservice/config/prod.exs | 21 + src/chatservice/config/runtime.exs | 117 +++ src/chatservice/config/test.exs | 33 + src/chatservice/lib/chatservice.ex | 9 + .../lib/chatservice/application.ex | 36 + src/chatservice/lib/chatservice/mailer.ex | 3 + src/chatservice/lib/chatservice/repo.ex | 5 + src/chatservice/lib/chatservice_web.ex | 113 +++ .../components/core_components.ex | 675 ++++++++++++++++++ .../lib/chatservice_web/components/layouts.ex | 5 + .../components/layouts/app.html.heex | 32 + .../components/layouts/root.html.heex | 17 + .../chatservice_web/controllers/error_html.ex | 19 + .../chatservice_web/controllers/error_json.ex | 15 + .../controllers/page_controller.ex | 9 + .../chatservice_web/controllers/page_html.ex | 5 + .../controllers/page_html/home.html.heex | 222 ++++++ .../lib/chatservice_web/endpoint.ex | 53 ++ .../lib/chatservice_web/gettext.ex | 24 + src/chatservice/lib/chatservice_web/router.ex | 44 ++ .../lib/chatservice_web/telemetry.ex | 92 +++ src/chatservice/mix.exs | 85 +++ src/chatservice/mix.lock | 41 ++ .../priv/gettext/en/LC_MESSAGES/errors.po | 112 +++ src/chatservice/priv/gettext/errors.pot | 109 +++ .../priv/repo/migrations/.formatter.exs | 4 + src/chatservice/priv/repo/seeds.exs | 11 + src/chatservice/priv/static/favicon.ico | Bin 0 -> 152 bytes src/chatservice/priv/static/images/logo.svg | 6 + src/chatservice/priv/static/robots.txt | 5 + .../controllers/error_html_test.exs | 14 + .../controllers/error_json_test.exs | 12 + .../controllers/page_controller_test.exs | 8 + src/chatservice/test/support/conn_case.ex | 38 + src/chatservice/test/support/data_case.ex | 58 ++ src/chatservice/test/test_helper.exs | 2 + 44 files changed, 2387 insertions(+), 3 deletions(-) create mode 100644 src/chatservice/.formatter.exs create mode 100644 src/chatservice/.gitignore create mode 100644 src/chatservice/assets/css/app.css create mode 100644 src/chatservice/assets/js/app.js create mode 100644 src/chatservice/assets/tailwind.config.js create mode 100644 src/chatservice/config/config.exs create mode 100644 src/chatservice/config/dev.exs create mode 100644 src/chatservice/config/prod.exs create mode 100644 src/chatservice/config/runtime.exs create mode 100644 src/chatservice/config/test.exs create mode 100644 src/chatservice/lib/chatservice.ex create mode 100644 src/chatservice/lib/chatservice/application.ex create mode 100644 src/chatservice/lib/chatservice/mailer.ex create mode 100644 src/chatservice/lib/chatservice/repo.ex create mode 100644 src/chatservice/lib/chatservice_web.ex create mode 100644 src/chatservice/lib/chatservice_web/components/core_components.ex create mode 100644 src/chatservice/lib/chatservice_web/components/layouts.ex create mode 100644 src/chatservice/lib/chatservice_web/components/layouts/app.html.heex create mode 100644 src/chatservice/lib/chatservice_web/components/layouts/root.html.heex create mode 100644 src/chatservice/lib/chatservice_web/controllers/error_html.ex create mode 100644 src/chatservice/lib/chatservice_web/controllers/error_json.ex create mode 100644 src/chatservice/lib/chatservice_web/controllers/page_controller.ex create mode 100644 src/chatservice/lib/chatservice_web/controllers/page_html.ex create mode 100644 src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex create mode 100644 src/chatservice/lib/chatservice_web/endpoint.ex create mode 100644 src/chatservice/lib/chatservice_web/gettext.ex create mode 100644 src/chatservice/lib/chatservice_web/router.ex create mode 100644 src/chatservice/lib/chatservice_web/telemetry.ex create mode 100644 src/chatservice/mix.exs create mode 100644 src/chatservice/mix.lock create mode 100644 src/chatservice/priv/gettext/en/LC_MESSAGES/errors.po create mode 100644 src/chatservice/priv/gettext/errors.pot create mode 100644 src/chatservice/priv/repo/migrations/.formatter.exs create mode 100644 src/chatservice/priv/repo/seeds.exs create mode 100644 src/chatservice/priv/static/favicon.ico create mode 100644 src/chatservice/priv/static/images/logo.svg create mode 100644 src/chatservice/priv/static/robots.txt create mode 100644 src/chatservice/test/chatservice_web/controllers/error_html_test.exs create mode 100644 src/chatservice/test/chatservice_web/controllers/error_json_test.exs create mode 100644 src/chatservice/test/chatservice_web/controllers/page_controller_test.exs create mode 100644 src/chatservice/test/support/conn_case.ex create mode 100644 src/chatservice/test/support/data_case.ex create mode 100644 src/chatservice/test/test_helper.exs diff --git a/src/chatservice/.formatter.exs b/src/chatservice/.formatter.exs new file mode 100644 index 0000000000..ef8840ce6f --- /dev/null +++ b/src/chatservice/.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/src/chatservice/.gitignore b/src/chatservice/.gitignore new file mode 100644 index 0000000000..7ccee7e1cf --- /dev/null +++ b/src/chatservice/.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"). +chatservice-*.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/src/chatservice/README.md b/src/chatservice/README.md index b76dd513bc..7ee13b4a83 100644 --- a/src/chatservice/README.md +++ b/src/chatservice/README.md @@ -1,3 +1,18 @@ -## Nix -If you have nix installed, you can use `nix-shell` for a temporary shell -that will include elixir and OTel Desktop Viewer. +# Chatservice + +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/src/chatservice/assets/css/app.css b/src/chatservice/assets/css/app.css new file mode 100644 index 0000000000..378c8f9056 --- /dev/null +++ b/src/chatservice/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/src/chatservice/assets/js/app.js b/src/chatservice/assets/js/app.js new file mode 100644 index 0000000000..d5e278afe5 --- /dev/null +++ b/src/chatservice/assets/js/app.js @@ -0,0 +1,44 @@ +// 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 csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, { + longPollFallbackMs: 2500, + 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()) + +// 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/src/chatservice/assets/tailwind.config.js b/src/chatservice/assets/tailwind.config.js new file mode 100644 index 0000000000..bcc126a1a0 --- /dev/null +++ b/src/chatservice/assets/tailwind.config.js @@ -0,0 +1,75 @@ +// 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/chatservice_web.ex", + "../lib/chatservice_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-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), + 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/src/chatservice/config/config.exs b/src/chatservice/config/config.exs new file mode 100644 index 0000000000..5341170a7f --- /dev/null +++ b/src/chatservice/config/config.exs @@ -0,0 +1,66 @@ +# 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 :chatservice, + ecto_repos: [Chatservice.Repo], + generators: [timestamp_type: :utc_datetime] + +# Configures the endpoint +config :chatservice, ChatserviceWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [html: ChatserviceWeb.ErrorHTML, json: ChatserviceWeb.ErrorJSON], + layout: false + ], + pubsub_server: Chatservice.PubSub, + live_view: [signing_salt: "vhPzRN9o"] + +# Configures the mailer +# +# By default it uses the "Local" adapter which stores the emails +# locally. You can see the emails in your browser, at "/dev/mailbox". +# +# For production it's recommended to configure a different adapter +# at the `config/runtime.exs`. +config :chatservice, Chatservice.Mailer, adapter: Swoosh.Adapters.Local + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.17.11", + chatservice: [ + 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.0", + chatservice: [ + 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", + 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/src/chatservice/config/dev.exs b/src/chatservice/config/dev.exs new file mode 100644 index 0000000000..cbd75df4ec --- /dev/null +++ b/src/chatservice/config/dev.exs @@ -0,0 +1,82 @@ +import Config + +# Configure your database +config :chatservice, Chatservice.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "chatservice_dev", + 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 :chatservice, ChatserviceWeb.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: "n+w1XD8A1cQBvipOZZpvch1nKpb1enMm4Jhqn/uNPr+ypQ/PA8JxiztV6VPElipy", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:chatservice, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:chatservice, ~w(--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 :chatservice, ChatserviceWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/chatservice_web/(controllers|live|components)/.*(ex|heex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :chatservice, 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 + +# Include HEEx debug annotations as HTML comments in rendered markup +config :phoenix_live_view, :debug_heex_annotations, true + +# Disable swoosh api client as it is only required for production adapters. +config :swoosh, :api_client, false diff --git a/src/chatservice/config/prod.exs b/src/chatservice/config/prod.exs new file mode 100644 index 0000000000..1da38999d4 --- /dev/null +++ b/src/chatservice/config/prod.exs @@ -0,0 +1,21 @@ +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 :chatservice, ChatserviceWeb.Endpoint, + cache_static_manifest: "priv/static/cache_manifest.json" + +# Configures Swoosh API Client +config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Chatservice.Finch + +# Disable Swoosh Local Memory Storage +config :swoosh, local: false + +# 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/src/chatservice/config/runtime.exs b/src/chatservice/config/runtime.exs new file mode 100644 index 0000000000..ecd780a72a --- /dev/null +++ b/src/chatservice/config/runtime.exs @@ -0,0 +1,117 @@ +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/chatservice 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 :chatservice, ChatserviceWeb.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 :chatservice, Chatservice.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 :chatservice, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + + config :chatservice, ChatserviceWeb.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 :chatservice, ChatserviceWeb.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 :chatservice, ChatserviceWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. + + # ## Configuring the mailer + # + # In production you need to configure the mailer to use a different adapter. + # Also, you may need to configure the Swoosh API client of your choice if you + # are not using SMTP. Here is an example of the configuration: + # + # config :chatservice, Chatservice.Mailer, + # adapter: Swoosh.Adapters.Mailgun, + # api_key: System.get_env("MAILGUN_API_KEY"), + # domain: System.get_env("MAILGUN_DOMAIN") + # + # For this example you need include a HTTP client required by Swoosh API client. + # Swoosh supports Hackney and Finch out of the box: + # + # config :swoosh, :api_client, Swoosh.ApiClient.Hackney + # + # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. +end diff --git a/src/chatservice/config/test.exs b/src/chatservice/config/test.exs new file mode 100644 index 0000000000..770cb2392e --- /dev/null +++ b/src/chatservice/config/test.exs @@ -0,0 +1,33 @@ +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 :chatservice, Chatservice.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "chatservice_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 :chatservice, ChatserviceWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "cfHSrMhdqQLdzdAiLRZazXjUBlnd12ZuG3ilwKigBsbA58cOWzW0Rm2cUa5oF8ts", + server: false + +# In test we don't send emails. +config :chatservice, Chatservice.Mailer, adapter: Swoosh.Adapters.Test + +# Disable swoosh api client as it is only required for production adapters. +config :swoosh, :api_client, 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 diff --git a/src/chatservice/lib/chatservice.ex b/src/chatservice/lib/chatservice.ex new file mode 100644 index 0000000000..07416a895c --- /dev/null +++ b/src/chatservice/lib/chatservice.ex @@ -0,0 +1,9 @@ +defmodule Chatservice do + @moduledoc """ + Chatservice 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/src/chatservice/lib/chatservice/application.ex b/src/chatservice/lib/chatservice/application.ex new file mode 100644 index 0000000000..0fdce57b08 --- /dev/null +++ b/src/chatservice/lib/chatservice/application.ex @@ -0,0 +1,36 @@ +defmodule Chatservice.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 = [ + ChatserviceWeb.Telemetry, + Chatservice.Repo, + {DNSCluster, query: Application.get_env(:chatservice, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: Chatservice.PubSub}, + # Start the Finch HTTP client for sending emails + {Finch, name: Chatservice.Finch}, + # Start a worker by calling: Chatservice.Worker.start_link(arg) + # {Chatservice.Worker, arg}, + # Start to serve requests, typically the last entry + ChatserviceWeb.Endpoint + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Chatservice.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 + ChatserviceWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/src/chatservice/lib/chatservice/mailer.ex b/src/chatservice/lib/chatservice/mailer.ex new file mode 100644 index 0000000000..2ba372a47e --- /dev/null +++ b/src/chatservice/lib/chatservice/mailer.ex @@ -0,0 +1,3 @@ +defmodule Chatservice.Mailer do + use Swoosh.Mailer, otp_app: :chatservice +end diff --git a/src/chatservice/lib/chatservice/repo.ex b/src/chatservice/lib/chatservice/repo.ex new file mode 100644 index 0000000000..16d2142369 --- /dev/null +++ b/src/chatservice/lib/chatservice/repo.ex @@ -0,0 +1,5 @@ +defmodule Chatservice.Repo do + use Ecto.Repo, + otp_app: :chatservice, + adapter: Ecto.Adapters.Postgres +end diff --git a/src/chatservice/lib/chatservice_web.ex b/src/chatservice/lib/chatservice_web.ex new file mode 100644 index 0000000000..3a5a543a34 --- /dev/null +++ b/src/chatservice/lib/chatservice_web.ex @@ -0,0 +1,113 @@ +defmodule ChatserviceWeb 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 ChatserviceWeb, :controller + use ChatserviceWeb, :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: ChatserviceWeb.Layouts] + + import Plug.Conn + import ChatserviceWeb.Gettext + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {ChatserviceWeb.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 + # Core UI components and translation + import ChatserviceWeb.CoreComponents + import ChatserviceWeb.Gettext + + # 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: ChatserviceWeb.Endpoint, + router: ChatserviceWeb.Router, + statics: ChatserviceWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/src/chatservice/lib/chatservice_web/components/core_components.ex b/src/chatservice/lib/chatservice_web/components/core_components.ex new file mode 100644 index 0000000000..3e567511b0 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/components/core_components.ex @@ -0,0 +1,675 @@ +defmodule ChatserviceWeb.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 + import ChatserviceWeb.Gettext + + @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] %> + <%= gettext("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, + 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", + 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 + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(ChatserviceWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(ChatserviceWeb.Gettext, "errors", msg, opts) + 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/src/chatservice/lib/chatservice_web/components/layouts.ex b/src/chatservice/lib/chatservice_web/components/layouts.ex new file mode 100644 index 0000000000..87dfb6b7e4 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/components/layouts.ex @@ -0,0 +1,5 @@ +defmodule ChatserviceWeb.Layouts do + use ChatserviceWeb, :html + + embed_templates "layouts/*" +end diff --git a/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex b/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex new file mode 100644 index 0000000000..e23bfc81c4 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex @@ -0,0 +1,32 @@ +
+
+
+ + + +

+ v<%= Application.spec(:phoenix, :vsn) %> +

+
+ +
+
+
+
+ <.flash_group flash={@flash} /> + <%= @inner_content %> +
+
diff --git a/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex b/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex new file mode 100644 index 0000000000..183a3a46f0 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex @@ -0,0 +1,17 @@ + + + + + + + <.live_title suffix=" · Phoenix Framework"> + <%= assigns[:page_title] || "Chatservice" %> + + + + + + <%= @inner_content %> + + diff --git a/src/chatservice/lib/chatservice_web/controllers/error_html.ex b/src/chatservice/lib/chatservice_web/controllers/error_html.ex new file mode 100644 index 0000000000..9193c4fe50 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/controllers/error_html.ex @@ -0,0 +1,19 @@ +defmodule ChatserviceWeb.ErrorHTML do + use ChatserviceWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/chatservice_web/controllers/error_html/404.html.heex + # * lib/chatservice_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/src/chatservice/lib/chatservice_web/controllers/error_json.ex b/src/chatservice/lib/chatservice_web/controllers/error_json.ex new file mode 100644 index 0000000000..74ce6e8fb2 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/controllers/error_json.ex @@ -0,0 +1,15 @@ +defmodule ChatserviceWeb.ErrorJSON do + # 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/src/chatservice/lib/chatservice_web/controllers/page_controller.ex b/src/chatservice/lib/chatservice_web/controllers/page_controller.ex new file mode 100644 index 0000000000..c56b78a041 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/controllers/page_controller.ex @@ -0,0 +1,9 @@ +defmodule ChatserviceWeb.PageController do + use ChatserviceWeb, :controller + + def home(conn, _params) do + # The home page is often custom made, + # so skip the default app layout. + render(conn, :home, layout: false) + end +end diff --git a/src/chatservice/lib/chatservice_web/controllers/page_html.ex b/src/chatservice/lib/chatservice_web/controllers/page_html.ex new file mode 100644 index 0000000000..bdc9149d0d --- /dev/null +++ b/src/chatservice/lib/chatservice_web/controllers/page_html.ex @@ -0,0 +1,5 @@ +defmodule ChatserviceWeb.PageHTML do + use ChatserviceWeb, :html + + embed_templates "page_html/*" +end diff --git a/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex b/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex new file mode 100644 index 0000000000..dc1820b11e --- /dev/null +++ b/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex @@ -0,0 +1,222 @@ +<.flash_group flash={@flash} /> + +
+
+ +

+ Phoenix Framework + + v<%= Application.spec(:phoenix, :vsn) %> + +

+

+ Peace of mind from prototype to production. +

+

+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. +

+ +
+
diff --git a/src/chatservice/lib/chatservice_web/endpoint.ex b/src/chatservice/lib/chatservice_web/endpoint.ex new file mode 100644 index 0000000000..ea8466c0f8 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/endpoint.ex @@ -0,0 +1,53 @@ +defmodule ChatserviceWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :chatservice + + # 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: "_chatservice_key", + signing_salt: "7zpyAVGL", + 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: :chatservice, + gzip: false, + only: ChatserviceWeb.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: :chatservice + end + + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + + 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 ChatserviceWeb.Router +end diff --git a/src/chatservice/lib/chatservice_web/gettext.ex b/src/chatservice/lib/chatservice_web/gettext.ex new file mode 100644 index 0000000000..79a1754c21 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/gettext.ex @@ -0,0 +1,24 @@ +defmodule ChatserviceWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import ChatserviceWeb.Gettext + + # Simple translation + gettext("Here is the string to translate") + + # Plural translation + ngettext("Here is the string to translate", + "Here are the strings to translate", + 3) + + # Domain-based translation + dgettext("errors", "Here is the error message to translate") + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :chatservice +end diff --git a/src/chatservice/lib/chatservice_web/router.ex b/src/chatservice/lib/chatservice_web/router.ex new file mode 100644 index 0000000000..3327a99f6c --- /dev/null +++ b/src/chatservice/lib/chatservice_web/router.ex @@ -0,0 +1,44 @@ +defmodule ChatserviceWeb.Router do + use ChatserviceWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {ChatserviceWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", ChatserviceWeb do + pipe_through :browser + + get "/", PageController, :home + end + + # Other scopes may use custom stacks. + # scope "/api", ChatserviceWeb do + # pipe_through :api + # end + + # Enable LiveDashboard and Swoosh mailbox preview in development + if Application.compile_env(:chatservice, :dev_routes) do + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + import Phoenix.LiveDashboard.Router + + scope "/dev" do + pipe_through :browser + + live_dashboard "/dashboard", metrics: ChatserviceWeb.Telemetry + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + end +end diff --git a/src/chatservice/lib/chatservice_web/telemetry.ex b/src/chatservice/lib/chatservice_web/telemetry.ex new file mode 100644 index 0000000000..351502a6bc --- /dev/null +++ b/src/chatservice/lib/chatservice_web/telemetry.ex @@ -0,0 +1,92 @@ +defmodule ChatserviceWeb.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("chatservice.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("chatservice.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("chatservice.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("chatservice.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("chatservice.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. + # {ChatserviceWeb, :count_users, []} + ] + end +end diff --git a/src/chatservice/mix.exs b/src/chatservice/mix.exs new file mode 100644 index 0000000000..3322d0a54a --- /dev/null +++ b/src/chatservice/mix.exs @@ -0,0 +1,85 @@ +defmodule Chatservice.MixProject do + use Mix.Project + + def project do + [ + app: :chatservice, + 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: {Chatservice.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 + [ + {:phoenix, "~> 1.7.11"}, + {:phoenix_ecto, "~> 4.4"}, + {:ecto_sql, "~> 3.10"}, + {:postgrex, ">= 0.0.0"}, + {:phoenix_html, "~> 4.0"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 0.20.2"}, + {:floki, ">= 0.30.0", only: :test}, + {:phoenix_live_dashboard, "~> 0.8.3"}, + {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, + {:heroicons, + github: "tailwindlabs/heroicons", + tag: "v2.1.1", + sparse: "optimized", + app: false, + compile: false, + depth: 1}, + {:swoosh, "~> 1.5"}, + {:finch, "~> 0.13"}, + {:telemetry_metrics, "~> 0.6"}, + {:telemetry_poller, "~> 1.0"}, + {:gettext, "~> 0.20"}, + {:jason, "~> 1.2"}, + {:dns_cluster, "~> 0.1.1"}, + {:bandit, "~> 1.2"} + ] + 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": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["tailwind chatservice", "esbuild chatservice"], + "assets.deploy": [ + "tailwind chatservice --minify", + "esbuild chatservice --minify", + "phx.digest" + ] + ] + end +end diff --git a/src/chatservice/mix.lock b/src/chatservice/mix.lock new file mode 100644 index 0000000000..2263d4145a --- /dev/null +++ b/src/chatservice/mix.lock @@ -0,0 +1,41 @@ +%{ + "bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [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", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [: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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, + "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"}, + "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, + "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [: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", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [: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]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, + "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"}, + "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, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [: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", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"}, + "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.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [: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", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [: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", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, + "swoosh": {:hex, :swoosh, "1.16.5", "5742f24c4d081671ebe87d8e7f6595cf75205d7f808cc5d55b09e4598b583413", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2324cf696b09ee52e5e1049dcc77880a11fe618a381e2df1c5ca5d69c380eb0"}, + "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, + "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.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [: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", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, +} diff --git a/src/chatservice/priv/gettext/en/LC_MESSAGES/errors.po b/src/chatservice/priv/gettext/en/LC_MESSAGES/errors.po new file mode 100644 index 0000000000..844c4f5cea --- /dev/null +++ b/src/chatservice/priv/gettext/en/LC_MESSAGES/errors.po @@ -0,0 +1,112 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" + +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/src/chatservice/priv/gettext/errors.pot b/src/chatservice/priv/gettext/errors.pot new file mode 100644 index 0000000000..eef2de2ba4 --- /dev/null +++ b/src/chatservice/priv/gettext/errors.pot @@ -0,0 +1,109 @@ +## This is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here has no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/src/chatservice/priv/repo/migrations/.formatter.exs b/src/chatservice/priv/repo/migrations/.formatter.exs new file mode 100644 index 0000000000..49f9151ed2 --- /dev/null +++ b/src/chatservice/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/src/chatservice/priv/repo/seeds.exs b/src/chatservice/priv/repo/seeds.exs new file mode 100644 index 0000000000..3642d915a2 --- /dev/null +++ b/src/chatservice/priv/repo/seeds.exs @@ -0,0 +1,11 @@ +# 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: +# +# Chatservice.Repo.insert!(%Chatservice.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. diff --git a/src/chatservice/priv/static/favicon.ico b/src/chatservice/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/src/chatservice/priv/static/images/logo.svg b/src/chatservice/priv/static/images/logo.svg new file mode 100644 index 0000000000..9f26babac2 --- /dev/null +++ b/src/chatservice/priv/static/images/logo.svg @@ -0,0 +1,6 @@ + diff --git a/src/chatservice/priv/static/robots.txt b/src/chatservice/priv/static/robots.txt new file mode 100644 index 0000000000..26e06b5f19 --- /dev/null +++ b/src/chatservice/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/src/chatservice/test/chatservice_web/controllers/error_html_test.exs b/src/chatservice/test/chatservice_web/controllers/error_html_test.exs new file mode 100644 index 0000000000..ef45278d16 --- /dev/null +++ b/src/chatservice/test/chatservice_web/controllers/error_html_test.exs @@ -0,0 +1,14 @@ +defmodule ChatserviceWeb.ErrorHTMLTest do + use ChatserviceWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template + + test "renders 404.html" do + assert render_to_string(ChatserviceWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(ChatserviceWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/src/chatservice/test/chatservice_web/controllers/error_json_test.exs b/src/chatservice/test/chatservice_web/controllers/error_json_test.exs new file mode 100644 index 0000000000..6d311ccbf8 --- /dev/null +++ b/src/chatservice/test/chatservice_web/controllers/error_json_test.exs @@ -0,0 +1,12 @@ +defmodule ChatserviceWeb.ErrorJSONTest do + use ChatserviceWeb.ConnCase, async: true + + test "renders 404" do + assert ChatserviceWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert ChatserviceWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/src/chatservice/test/chatservice_web/controllers/page_controller_test.exs b/src/chatservice/test/chatservice_web/controllers/page_controller_test.exs new file mode 100644 index 0000000000..26fcb8c374 --- /dev/null +++ b/src/chatservice/test/chatservice_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule ChatserviceWeb.PageControllerTest do + use ChatserviceWeb.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/src/chatservice/test/support/conn_case.ex b/src/chatservice/test/support/conn_case.ex new file mode 100644 index 0000000000..791111f0d3 --- /dev/null +++ b/src/chatservice/test/support/conn_case.ex @@ -0,0 +1,38 @@ +defmodule ChatserviceWeb.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 ChatserviceWeb.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 ChatserviceWeb.Endpoint + + use ChatserviceWeb, :verified_routes + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import ChatserviceWeb.ConnCase + end + end + + setup tags do + Chatservice.DataCase.setup_sandbox(tags) + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/src/chatservice/test/support/data_case.ex b/src/chatservice/test/support/data_case.ex new file mode 100644 index 0000000000..b51861323f --- /dev/null +++ b/src/chatservice/test/support/data_case.ex @@ -0,0 +1,58 @@ +defmodule Chatservice.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 Chatservice.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + alias Chatservice.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import Chatservice.DataCase + end + end + + setup tags do + Chatservice.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!(Chatservice.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/src/chatservice/test/test_helper.exs b/src/chatservice/test/test_helper.exs new file mode 100644 index 0000000000..83d36c8b2d --- /dev/null +++ b/src/chatservice/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(Chatservice.Repo, :manual) From 1bc45e170796507400407bf726c16d1fc473dd36 Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Sun, 12 May 2024 11:24:10 -0400 Subject: [PATCH 03/26] Adds OpenTelemetry to Phoenix This commit adds OpenTelemetry instrumentation to Phoenix: 1. Dependencies are added in mix.exs 2. Configuration options are added in config/config.exs 3. The OTel instrumentation libraries for Phoenix and ecto are started with the application --- src/chatservice/config/config.exs | 9 +++++++++ src/chatservice/lib/chatservice/application.ex | 4 ++++ src/chatservice/mix.exs | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/chatservice/config/config.exs b/src/chatservice/config/config.exs index 5341170a7f..51f1ef5c3e 100644 --- a/src/chatservice/config/config.exs +++ b/src/chatservice/config/config.exs @@ -61,6 +61,15 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +# Configure the OpenTelemetry SDK & Exporter +config :opentelemetry, + span_processor: :batch, + traces_exporter: :otlp + +config :opentelemetry_exporter, + otlp_protocol: :http_protobuf, + otlp_endpoint: "http://localhost:4318" + # 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/src/chatservice/lib/chatservice/application.ex b/src/chatservice/lib/chatservice/application.ex index 0fdce57b08..2b79b4b07d 100644 --- a/src/chatservice/lib/chatservice/application.ex +++ b/src/chatservice/lib/chatservice/application.ex @@ -7,6 +7,10 @@ defmodule Chatservice.Application do @impl true def start(_type, _args) do + # Set up OpenTelemetry instrumentation + OpentelemetryPhoenix.setup() + OpentelemetryEcto.setup([:chatservice, :repo]) + children = [ ChatserviceWeb.Telemetry, Chatservice.Repo, diff --git a/src/chatservice/mix.exs b/src/chatservice/mix.exs index 3322d0a54a..eaa1fd4629 100644 --- a/src/chatservice/mix.exs +++ b/src/chatservice/mix.exs @@ -57,7 +57,15 @@ defmodule Chatservice.MixProject do {:gettext, "~> 0.20"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.2"} + {:bandit, "~> 1.2"}, + + # OpenTelemetry + {:opentelemetry, "~> 1.4"}, + {:opentelemetry_api, "~> 1.3"}, + {:opentelemetry_exporter, "~> 1.6"}, + {:opentelemetry_cowboy, "~> 0.3"}, + {:opentelemetry_ecto, "~> 1.2"}, + {:opentelemetry_phoenix, "~> 1.2"} ] end From a705e958b803f587be88e3ab27dfb0b1587b8562 Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Sun, 12 May 2024 11:35:10 -0400 Subject: [PATCH 04/26] docker compose for development postgres --- src/chatservice/docker-compose.yml | 10 ++++++++++ src/chatservice/mix.lock | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/chatservice/docker-compose.yml diff --git a/src/chatservice/docker-compose.yml b/src/chatservice/docker-compose.yml new file mode 100644 index 0000000000..6f45d2b1e8 --- /dev/null +++ b/src/chatservice/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.9' +services: + postgres: + image: postgres:16.1 + container_name: postgres + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - "5432:5432" diff --git a/src/chatservice/mix.lock b/src/chatservice/mix.lock index 2263d4145a..aa1ce1d366 100644 --- a/src/chatservice/mix.lock +++ b/src/chatservice/mix.lock @@ -1,6 +1,12 @@ %{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, "bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [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", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, @@ -12,13 +18,25 @@ "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, + "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "opentelemetry": {:hex, :opentelemetry, "1.4.0", "f928923ed80adb5eb7894bac22e9a198478e6a8f04020ae1d6f289fdcad0b498", [:rebar3], [{:opentelemetry_api, "~> 1.3.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "50b32ce127413e5d87b092b4d210a3449ea80cd8224090fe68d73d576a3faa15"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.3.0", "03e2177f28dd8d11aaa88e8522c81c2f6a788170fe52f7a65262340961e663f9", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "b9e5ff775fd064fa098dba3c398490b77649a352b40b0b730a6b7dc0bdd68858"}, + "opentelemetry_cowboy": {:hex, :opentelemetry_cowboy, "0.3.0", "0144b211fa6cda0e6211c340cebd1bbd9158e350099ea3bf3d838f993cb4b90e", [:rebar3], [{:cowboy_telemetry, "~> 0.4", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4f44537b4c7430018198d480f55bc88a40f7d0582c3ad927a5bab4ceb39e80ea"}, + "opentelemetry_ecto": {:hex, :opentelemetry_ecto, "1.2.0", "2382cb47ddc231f953d3b8263ed029d87fbf217915a1da82f49159d122b64865", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "70dfa2e79932e86f209df00e36c980b17a32f82d175f0068bf7ef9a96cf080cf"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.7.0", "dec4e90c0667cf11a3642f7fe71982dbc0c6bfbb8725a0b13766830718cf0d98", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.4.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.3.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "d0f25f6439ec43f2561537c3fabbe177b38547cddaa3a692cbb8f4770dbefc1e"}, + "opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "1.2.0", "b8a53ee595b24970571a7d2fcaef3e4e1a021c68e97cac163ca5d9875fad5e9f", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "acab991d14ed3efc3f780c5a20cabba27149cf731005b1cc6454c160859debe5"}, + "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.3.0", "ef5b2059403a1e2b2d2c65914e6962e56371570b8c3ab5323d7a8d3444fb7f84", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "7243cb6de1523c473cba5b1aefa3f85e1ff8cc75d08f367104c1e11919c8c029"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.1", "4a73bfa29d7780ffe33db345465919cef875034854649c37ac789eb8e8f38b21", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ee43b14e6866123a3ee1344e3c0d3d7591f4537542c2a925fcdbf46249c9b50b"}, "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [: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", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [: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]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, @@ -30,12 +48,15 @@ "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [: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", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [: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", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "swoosh": {:hex, :swoosh, "1.16.5", "5742f24c4d081671ebe87d8e7f6595cf75205d7f808cc5d55b09e4598b583413", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2324cf696b09ee52e5e1049dcc77880a11fe618a381e2df1c5ca5d69c380eb0"}, "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "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"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.22.1", "0f450cc1568a67a65ce5e15df53c53f9a098c3da081c5f126199a72505858dc1", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3092be0babdc0e14c2e900542351e066c0fa5a9cf4b3597559ad1e67f07938c0"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [: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", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, } From f7a06eaa24772dbc58561f570df77d533f88082b Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Sun, 12 May 2024 13:51:23 -0400 Subject: [PATCH 05/26] rename Chatservice to ChatService --- src/chatservice/README.md | 2 +- src/chatservice/config/config.exs | 10 ++++----- src/chatservice/config/dev.exs | 6 ++--- src/chatservice/config/prod.exs | 4 ++-- src/chatservice/config/runtime.exs | 12 +++++----- src/chatservice/config/test.exs | 6 ++--- src/chatservice/lib/chatservice.ex | 4 ++-- .../lib/chatservice/application.ex | 20 ++++++++--------- src/chatservice/lib/chatservice/mailer.ex | 2 +- src/chatservice/lib/chatservice/repo.ex | 2 +- src/chatservice/lib/chatservice_web.ex | 22 +++++++++---------- .../components/core_components.ex | 8 +++---- .../lib/chatservice_web/components/layouts.ex | 4 ++-- .../components/layouts/root.html.heex | 2 +- .../chatservice_web/controllers/error_html.ex | 4 ++-- .../chatservice_web/controllers/error_json.ex | 2 +- .../controllers/page_controller.ex | 4 ++-- .../chatservice_web/controllers/page_html.ex | 4 ++-- .../lib/chatservice_web/endpoint.ex | 6 ++--- .../lib/chatservice_web/gettext.ex | 4 ++-- src/chatservice/lib/chatservice_web/router.ex | 12 +++++----- .../lib/chatservice_web/telemetry.ex | 4 ++-- src/chatservice/mix.exs | 4 ++-- src/chatservice/priv/repo/seeds.exs | 2 +- .../controllers/error_html_test.exs | 8 +++---- .../controllers/error_json_test.exs | 8 +++---- .../controllers/page_controller_test.exs | 4 ++-- src/chatservice/test/support/conn_case.ex | 12 +++++----- src/chatservice/test/support/data_case.ex | 12 +++++----- src/chatservice/test/test_helper.exs | 2 +- 30 files changed, 98 insertions(+), 98 deletions(-) diff --git a/src/chatservice/README.md b/src/chatservice/README.md index 7ee13b4a83..38a931f744 100644 --- a/src/chatservice/README.md +++ b/src/chatservice/README.md @@ -1,4 +1,4 @@ -# Chatservice +# ChatService To start your Phoenix server: diff --git a/src/chatservice/config/config.exs b/src/chatservice/config/config.exs index 51f1ef5c3e..54a8540c09 100644 --- a/src/chatservice/config/config.exs +++ b/src/chatservice/config/config.exs @@ -8,18 +8,18 @@ import Config config :chatservice, - ecto_repos: [Chatservice.Repo], + ecto_repos: [ChatService.Repo], generators: [timestamp_type: :utc_datetime] # Configures the endpoint -config :chatservice, ChatserviceWeb.Endpoint, +config :chatservice, ChatServiceWeb.Endpoint, url: [host: "localhost"], adapter: Bandit.PhoenixAdapter, render_errors: [ - formats: [html: ChatserviceWeb.ErrorHTML, json: ChatserviceWeb.ErrorJSON], + formats: [html: ChatServiceWeb.ErrorHTML, json: ChatServiceWeb.ErrorJSON], layout: false ], - pubsub_server: Chatservice.PubSub, + pubsub_server: ChatService.PubSub, live_view: [signing_salt: "vhPzRN9o"] # Configures the mailer @@ -29,7 +29,7 @@ config :chatservice, ChatserviceWeb.Endpoint, # # For production it's recommended to configure a different adapter # at the `config/runtime.exs`. -config :chatservice, Chatservice.Mailer, adapter: Swoosh.Adapters.Local +config :chatservice, ChatService.Mailer, adapter: Swoosh.Adapters.Local # Configure esbuild (the version is required) config :esbuild, diff --git a/src/chatservice/config/dev.exs b/src/chatservice/config/dev.exs index cbd75df4ec..ffdef520d8 100644 --- a/src/chatservice/config/dev.exs +++ b/src/chatservice/config/dev.exs @@ -1,7 +1,7 @@ import Config # Configure your database -config :chatservice, Chatservice.Repo, +config :chatservice, ChatService.Repo, username: "postgres", password: "postgres", hostname: "localhost", @@ -16,7 +16,7 @@ config :chatservice, Chatservice.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 :chatservice, ChatserviceWeb.Endpoint, +config :chatservice, ChatServiceWeb.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], @@ -53,7 +53,7 @@ config :chatservice, ChatserviceWeb.Endpoint, # different ports. # Watch static and templates for browser reloading. -config :chatservice, ChatserviceWeb.Endpoint, +config :chatservice, ChatServiceWeb.Endpoint, live_reload: [ patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", diff --git a/src/chatservice/config/prod.exs b/src/chatservice/config/prod.exs index 1da38999d4..ba2fab03eb 100644 --- a/src/chatservice/config/prod.exs +++ b/src/chatservice/config/prod.exs @@ -5,11 +5,11 @@ 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 :chatservice, ChatserviceWeb.Endpoint, +config :chatservice, ChatServiceWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" # Configures Swoosh API Client -config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Chatservice.Finch +config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: ChatService.Finch # Disable Swoosh Local Memory Storage config :swoosh, local: false diff --git a/src/chatservice/config/runtime.exs b/src/chatservice/config/runtime.exs index ecd780a72a..3c737c53de 100644 --- a/src/chatservice/config/runtime.exs +++ b/src/chatservice/config/runtime.exs @@ -17,7 +17,7 @@ import Config # 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 :chatservice, ChatserviceWeb.Endpoint, server: true + config :chatservice, ChatServiceWeb.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 :chatservice, Chatservice.Repo, + config :chatservice, ChatService.Repo, # ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), @@ -53,7 +53,7 @@ if config_env() == :prod do config :chatservice, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - config :chatservice, ChatserviceWeb.Endpoint, + config :chatservice, ChatServiceWeb.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 :chatservice, ChatserviceWeb.Endpoint, + # config :chatservice, ChatServiceWeb.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 :chatservice, ChatserviceWeb.Endpoint, + # config :chatservice, ChatServiceWeb.Endpoint, # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. @@ -103,7 +103,7 @@ if config_env() == :prod do # Also, you may need to configure the Swoosh API client of your choice if you # are not using SMTP. Here is an example of the configuration: # - # config :chatservice, Chatservice.Mailer, + # config :chatservice, ChatService.Mailer, # adapter: Swoosh.Adapters.Mailgun, # api_key: System.get_env("MAILGUN_API_KEY"), # domain: System.get_env("MAILGUN_DOMAIN") diff --git a/src/chatservice/config/test.exs b/src/chatservice/config/test.exs index 770cb2392e..769bec3708 100644 --- a/src/chatservice/config/test.exs +++ b/src/chatservice/config/test.exs @@ -5,7 +5,7 @@ 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 :chatservice, Chatservice.Repo, +config :chatservice, ChatService.Repo, username: "postgres", password: "postgres", hostname: "localhost", @@ -15,13 +15,13 @@ config :chatservice, Chatservice.Repo, # We don't run a server during test. If one is required, # you can enable the server option below. -config :chatservice, ChatserviceWeb.Endpoint, +config :chatservice, ChatServiceWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], secret_key_base: "cfHSrMhdqQLdzdAiLRZazXjUBlnd12ZuG3ilwKigBsbA58cOWzW0Rm2cUa5oF8ts", server: false # In test we don't send emails. -config :chatservice, Chatservice.Mailer, adapter: Swoosh.Adapters.Test +config :chatservice, ChatService.Mailer, adapter: Swoosh.Adapters.Test # Disable swoosh api client as it is only required for production adapters. config :swoosh, :api_client, false diff --git a/src/chatservice/lib/chatservice.ex b/src/chatservice/lib/chatservice.ex index 07416a895c..ff47b8c3c0 100644 --- a/src/chatservice/lib/chatservice.ex +++ b/src/chatservice/lib/chatservice.ex @@ -1,6 +1,6 @@ -defmodule Chatservice do +defmodule ChatService do @moduledoc """ - Chatservice keeps the contexts that define your domain + ChatService keeps the contexts that define your domain and business logic. Contexts are also responsible for managing your data, regardless diff --git a/src/chatservice/lib/chatservice/application.ex b/src/chatservice/lib/chatservice/application.ex index 2b79b4b07d..7ee422aec1 100644 --- a/src/chatservice/lib/chatservice/application.ex +++ b/src/chatservice/lib/chatservice/application.ex @@ -1,4 +1,4 @@ -defmodule Chatservice.Application do +defmodule ChatService.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false @@ -12,21 +12,21 @@ defmodule Chatservice.Application do OpentelemetryEcto.setup([:chatservice, :repo]) children = [ - ChatserviceWeb.Telemetry, - Chatservice.Repo, + ChatServiceWeb.Telemetry, + ChatService.Repo, {DNSCluster, query: Application.get_env(:chatservice, :dns_cluster_query) || :ignore}, - {Phoenix.PubSub, name: Chatservice.PubSub}, + {Phoenix.PubSub, name: ChatService.PubSub}, # Start the Finch HTTP client for sending emails - {Finch, name: Chatservice.Finch}, - # Start a worker by calling: Chatservice.Worker.start_link(arg) - # {Chatservice.Worker, arg}, + {Finch, name: ChatService.Finch}, + # Start a worker by calling: ChatService.Worker.start_link(arg) + # {ChatService.Worker, arg}, # Start to serve requests, typically the last entry - ChatserviceWeb.Endpoint + ChatServiceWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options - opts = [strategy: :one_for_one, name: Chatservice.Supervisor] + opts = [strategy: :one_for_one, name: ChatService.Supervisor] Supervisor.start_link(children, opts) end @@ -34,7 +34,7 @@ defmodule Chatservice.Application do # whenever the application is updated. @impl true def config_change(changed, _new, removed) do - ChatserviceWeb.Endpoint.config_change(changed, removed) + ChatServiceWeb.Endpoint.config_change(changed, removed) :ok end end diff --git a/src/chatservice/lib/chatservice/mailer.ex b/src/chatservice/lib/chatservice/mailer.ex index 2ba372a47e..f631f6069f 100644 --- a/src/chatservice/lib/chatservice/mailer.ex +++ b/src/chatservice/lib/chatservice/mailer.ex @@ -1,3 +1,3 @@ -defmodule Chatservice.Mailer do +defmodule ChatService.Mailer do use Swoosh.Mailer, otp_app: :chatservice end diff --git a/src/chatservice/lib/chatservice/repo.ex b/src/chatservice/lib/chatservice/repo.ex index 16d2142369..9de41c3e48 100644 --- a/src/chatservice/lib/chatservice/repo.ex +++ b/src/chatservice/lib/chatservice/repo.ex @@ -1,4 +1,4 @@ -defmodule Chatservice.Repo do +defmodule ChatService.Repo do use Ecto.Repo, otp_app: :chatservice, adapter: Ecto.Adapters.Postgres diff --git a/src/chatservice/lib/chatservice_web.ex b/src/chatservice/lib/chatservice_web.ex index 3a5a543a34..5889be4cf2 100644 --- a/src/chatservice/lib/chatservice_web.ex +++ b/src/chatservice/lib/chatservice_web.ex @@ -1,12 +1,12 @@ -defmodule ChatserviceWeb do +defmodule ChatServiceWeb 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 ChatserviceWeb, :controller - use ChatserviceWeb, :html + use ChatServiceWeb, :controller + use ChatServiceWeb, :html The definitions below will be executed for every controller, component, etc, so keep them short and clean, focused @@ -40,10 +40,10 @@ defmodule ChatserviceWeb do quote do use Phoenix.Controller, formats: [:html, :json], - layouts: [html: ChatserviceWeb.Layouts] + layouts: [html: ChatServiceWeb.Layouts] import Plug.Conn - import ChatserviceWeb.Gettext + import ChatServiceWeb.Gettext unquote(verified_routes()) end @@ -52,7 +52,7 @@ defmodule ChatserviceWeb do def live_view do quote do use Phoenix.LiveView, - layout: {ChatserviceWeb.Layouts, :app} + layout: {ChatServiceWeb.Layouts, :app} unquote(html_helpers()) end @@ -84,8 +84,8 @@ defmodule ChatserviceWeb do # HTML escaping functionality import Phoenix.HTML # Core UI components and translation - import ChatserviceWeb.CoreComponents - import ChatserviceWeb.Gettext + import ChatServiceWeb.CoreComponents + import ChatServiceWeb.Gettext # Shortcut for generating JS commands alias Phoenix.LiveView.JS @@ -98,9 +98,9 @@ defmodule ChatserviceWeb do def verified_routes do quote do use Phoenix.VerifiedRoutes, - endpoint: ChatserviceWeb.Endpoint, - router: ChatserviceWeb.Router, - statics: ChatserviceWeb.static_paths() + endpoint: ChatServiceWeb.Endpoint, + router: ChatServiceWeb.Router, + statics: ChatServiceWeb.static_paths() end end diff --git a/src/chatservice/lib/chatservice_web/components/core_components.ex b/src/chatservice/lib/chatservice_web/components/core_components.ex index 3e567511b0..7e2161df67 100644 --- a/src/chatservice/lib/chatservice_web/components/core_components.ex +++ b/src/chatservice/lib/chatservice_web/components/core_components.ex @@ -1,4 +1,4 @@ -defmodule ChatserviceWeb.CoreComponents do +defmodule ChatServiceWeb.CoreComponents do @moduledoc """ Provides core UI components. @@ -17,7 +17,7 @@ defmodule ChatserviceWeb.CoreComponents do use Phoenix.Component alias Phoenix.LiveView.JS - import ChatserviceWeb.Gettext + import ChatServiceWeb.Gettext @doc """ Renders a modal. @@ -660,9 +660,9 @@ defmodule ChatserviceWeb.CoreComponents do # with our gettext backend as first argument. Translations are # available in the errors.po file (as we use the "errors" domain). if count = opts[:count] do - Gettext.dngettext(ChatserviceWeb.Gettext, "errors", msg, msg, count, opts) + Gettext.dngettext(ChatServiceWeb.Gettext, "errors", msg, msg, count, opts) else - Gettext.dgettext(ChatserviceWeb.Gettext, "errors", msg, opts) + Gettext.dgettext(ChatServiceWeb.Gettext, "errors", msg, opts) end end diff --git a/src/chatservice/lib/chatservice_web/components/layouts.ex b/src/chatservice/lib/chatservice_web/components/layouts.ex index 87dfb6b7e4..ff3ea7196d 100644 --- a/src/chatservice/lib/chatservice_web/components/layouts.ex +++ b/src/chatservice/lib/chatservice_web/components/layouts.ex @@ -1,5 +1,5 @@ -defmodule ChatserviceWeb.Layouts do - use ChatserviceWeb, :html +defmodule ChatServiceWeb.Layouts do + use ChatServiceWeb, :html embed_templates "layouts/*" end diff --git a/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex b/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex index 183a3a46f0..9dc2d7e287 100644 --- a/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex +++ b/src/chatservice/lib/chatservice_web/components/layouts/root.html.heex @@ -5,7 +5,7 @@ <.live_title suffix=" · Phoenix Framework"> - <%= assigns[:page_title] || "Chatservice" %> + <%= assigns[:page_title] || "ChatService" %> +// +// You will need to verify the user token in the "connect/3" function +// in "lib/chatservice_web/channels/user_socket.ex": +// +// def connect(%{"token" => token}, socket, _connect_info) do +// # max_age: 1209600 is equivalent to two weeks in seconds +// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do +// {:ok, user_id} -> +// {:ok, assign(socket, :user, user_id)} +// +// {:error, reason} -> +// :error +// end +// end +// +// Finally, connect to the socket: +socket.connect() + +// Now that you are connected, you can join channels with a topic. +// Let's assume you have a channel with a topic named `room` and the +// subtopic is its id - in this case 42: +let channel = socket.channel("room:42", {}) +channel.join() + .receive("ok", resp => { console.log("Joined successfully", resp) }) + .receive("error", resp => { console.log("Unable to join", resp) }) + +export default socket diff --git a/src/chatservice/lib/chatservice_web/channels/chat_channel.ex b/src/chatservice/lib/chatservice_web/channels/chat_channel.ex new file mode 100644 index 0000000000..f7c62b6d58 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/channels/chat_channel.ex @@ -0,0 +1,32 @@ +defmodule ChatServiceWeb.ChatChannel do + use ChatServiceWeb, :channel + + @impl true + def join("chat:lobby", payload, socket) do + if authorized?(payload) do + {:ok, socket} + else + {:error, %{reason: "unauthorized"}} + end + end + + # Channels can be used in a request/response fashion + # by sending replies to requests from the client + @impl true + def handle_in("ping", payload, socket) do + {:reply, {:ok, payload}, socket} + end + + # It is also common to receive messages from the client and + # broadcast to everyone in the current topic (chat:lobby). + @impl true + def handle_in("shout", payload, socket) do + broadcast(socket, "shout", payload) + {:noreply, socket} + end + + # Add authorization logic here as required. + defp authorized?(_payload) do + true + end +end diff --git a/src/chatservice/lib/chatservice_web/channels/user_socket.ex b/src/chatservice/lib/chatservice_web/channels/user_socket.ex new file mode 100644 index 0000000000..77409668c7 --- /dev/null +++ b/src/chatservice/lib/chatservice_web/channels/user_socket.ex @@ -0,0 +1,44 @@ +defmodule ChatServiceWeb.UserSocket do + use Phoenix.Socket + + # A Socket handler + # + # It's possible to control the websocket connection and + # assign values that can be accessed by your channel topics. + + ## Channels + + channel "chat:*", ChatServiceWeb.ChatChannel + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error` or `{:error, term}`. To control the + # response the client receives in that case, [define an error handler in the + # websocket + # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration). + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + @impl true + def connect(_params, socket, _connect_info) do + {:ok, socket} + end + + # Socket IDs are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # Elixir.ChatServiceWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(_socket), do: nil +end diff --git a/src/chatservice/lib/chatservice_web/endpoint.ex b/src/chatservice/lib/chatservice_web/endpoint.ex index 383019a4b2..5b4f3de821 100644 --- a/src/chatservice/lib/chatservice_web/endpoint.ex +++ b/src/chatservice/lib/chatservice_web/endpoint.ex @@ -11,6 +11,11 @@ defmodule ChatServiceWeb.Endpoint do same_site: "Lax" ] + # add socket created by mix.phx.gen.channel + socket "/socket", ChatServiceWeb.UserSocket, + websocket: true, + longpoll: false + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] diff --git a/src/chatservice/test/chatservice_web/channels/chat_channel_test.exs b/src/chatservice/test/chatservice_web/channels/chat_channel_test.exs new file mode 100644 index 0000000000..1a9eef7f25 --- /dev/null +++ b/src/chatservice/test/chatservice_web/channels/chat_channel_test.exs @@ -0,0 +1,27 @@ +defmodule ChatServiceWeb.ChatChannelTest do + use ChatServiceWeb.ChannelCase + + setup do + {:ok, _, socket} = + ChatServiceWeb.UserSocket + |> socket("user_id", %{some: :assign}) + |> subscribe_and_join(ChatServiceWeb.ChatChannel, "chat:lobby") + + %{socket: socket} + end + + test "ping replies with status ok", %{socket: socket} do + ref = push(socket, "ping", %{"hello" => "there"}) + assert_reply ref, :ok, %{"hello" => "there"} + end + + test "shout broadcasts to chat:lobby", %{socket: socket} do + push(socket, "shout", %{"hello" => "all"}) + assert_broadcast "shout", %{"hello" => "all"} + end + + test "broadcasts are pushed to the client", %{socket: socket} do + broadcast_from!(socket, "broadcast", %{"some" => "data"}) + assert_push "broadcast", %{"some" => "data"} + end +end diff --git a/src/chatservice/test/support/channel_case.ex b/src/chatservice/test/support/channel_case.ex new file mode 100644 index 0000000000..f393e59b4f --- /dev/null +++ b/src/chatservice/test/support/channel_case.ex @@ -0,0 +1,35 @@ +defmodule ChatServiceWeb.ChannelCase do + @moduledoc """ + This module defines the test case to be used by + channel tests. + + Such tests rely on `Phoenix.ChannelTest` 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 ChatServiceWeb.ChannelCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with channels + import Phoenix.ChannelTest + import ChatServiceWeb.ChannelCase + + # The default endpoint for testing + @endpoint ChatServiceWeb.Endpoint + end + end + + setup tags do + ChatService.DataCase.setup_sandbox(tags) + :ok + end +end From 8d39847506e24cca866baf1c6107af69e8c0fe6a Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Mon, 20 May 2024 22:14:28 -0400 Subject: [PATCH 07/26] cleaning up nix shell --- src/chatservice/.gitignore | 1 + src/chatservice/nix/default.nix | 12 -- src/chatservice/nix/sources.json | 14 --- src/chatservice/nix/sources.nix | 198 ------------------------------- src/chatservice/shell.nix | 44 ++++--- 5 files changed, 28 insertions(+), 241 deletions(-) delete mode 100644 src/chatservice/nix/default.nix delete mode 100644 src/chatservice/nix/sources.json delete mode 100644 src/chatservice/nix/sources.nix diff --git a/src/chatservice/.gitignore b/src/chatservice/.gitignore index 7ccee7e1cf..46d0d59dd5 100644 --- a/src/chatservice/.gitignore +++ b/src/chatservice/.gitignore @@ -35,3 +35,4 @@ chatservice-*.tar npm-debug.log /assets/node_modules/ +.nix-* diff --git a/src/chatservice/nix/default.nix b/src/chatservice/nix/default.nix deleted file mode 100644 index 1224bf1cf9..0000000000 --- a/src/chatservice/nix/default.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ sources ? import ./sources.nix -, pkgs ? import sources.nixpkgs { } -}: - -with pkgs; - -buildEnv { - name = "builder"; - paths = [ - elixir - ]; -} diff --git a/src/chatservice/nix/sources.json b/src/chatservice/nix/sources.json deleted file mode 100644 index 711b141b48..0000000000 --- a/src/chatservice/nix/sources.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "nixpkgs": { - "branch": "nixos-unstable", - "description": "Nix Packages collection", - "homepage": null, - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6c43a3495a11e261e5f41e5d7eda2d71dae1b2fe", - "sha256": "16f329z831bq7l3wn1dfvbkh95l2gcggdwn6rk3cisdmv2aa3189", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/6c43a3495a11e261e5f41e5d7eda2d71dae1b2fe.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/src/chatservice/nix/sources.nix b/src/chatservice/nix/sources.nix deleted file mode 100644 index fe3dadf7eb..0000000000 --- a/src/chatservice/nix/sources.nix +++ /dev/null @@ -1,198 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; - - fetch_tarball = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; - - fetch_git = name: spec: - let - ref = - spec.ref or ( - if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" - ); - submodules = spec.submodules or false; - submoduleArg = - let - nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; - emptyArgWithWarning = - if submodules - then - builtins.trace - ( - "The niv input \"${name}\" uses submodules " - + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " - + "does not support them" - ) - { } - else { }; - in - if nixSupportsSubmodules - then { inherit submodules; } - else emptyArgWithWarning; - in - builtins.fetchGit - ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); - - fetch_local = spec: spec.path; - - fetch_builtin-tarball = name: throw - ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=tarball -a builtin=true''; - - fetch_builtin-url = name: throw - ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=file -a builtin=true''; - - # - # Various helpers - # - - # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 - sanitizeName = name: - ( - concatMapStrings (s: if builtins.isList s then "-" else s) - ( - builtins.split "[^[:alnum:]+._?=-]+" - ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) - ) - ); - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: system: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import { } - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs name spec - else if spec.type == "tarball" then fetch_tarball pkgs name spec - else if spec.type == "git" then fetch_git name spec - else if spec.type == "local" then fetch_local spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball name - else if spec.type == "builtin-url" then fetch_builtin-url name - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # If the environment variable NIV_OVERRIDE_${name} is set, then use - # the path directly as opposed to the fetched source. - replace = name: drv: - let - saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; - ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; - in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 - stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 - stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); - concatMapStrings = f: list: concatStrings (map f list); - concatStrings = builtins.concatStringsSep ""; - - # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else { }; - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs - ( - name: spec: - if builtins.hasAttr "outPath" spec - then - abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) - config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) - , system ? builtins.currentSystem - , pkgs ? mkPkgs sources system - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; - -in -mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/src/chatservice/shell.nix b/src/chatservice/shell.nix index 62bbac3a9c..4a4a51d70e 100644 --- a/src/chatservice/shell.nix +++ b/src/chatservice/shell.nix @@ -1,21 +1,31 @@ -{ sources ? import ./nix/sources.nix -, pkgs ? import { } -}: - -with pkgs; +with import {}; let - inherit (lib) optional optionals; -in + basePackages = [ + gnumake + gcc + curl + elixir + inotify-tools + ]; + + inputs = if pkgs.system == "x86_64-darwin" then + basePackages ++ [ pkgs.darwin.apple_skd.frameworks.CoreServices ] + else + basePackages; -mkShell { - buildInputs = [ - (import ./nix/default.nix { inherit pkgs; }) - niv + hooks = '' + mkdir -p .nix-mix + mkdir -p .nix-hex + export MIX_HOME=$PWD/.nix-mix + export HEX_HOME=$PWD/.nix-hex + export PATH=$MIX_HOME/bin:$PATH + export PATH=$HEX_HOME/bin:$PATH + export LANG=en_US.UTF-8 + ''; +in mkShell { + buildInputs = inputs ++ [ otel-desktop-viewer - ] ++ optional stdenv.isLinux inotify-tools - ++ optional stdenv.isDarwin terminal-notifier - ++ optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ - CoreFoundation - CoreServices - ]); + ]; + + shellHook = hooks; } From 265ccfa9277f5b482dd4e45169b8aefdda68426a Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Mon, 20 May 2024 22:15:45 -0400 Subject: [PATCH 08/26] listen on any interface --- src/chatservice/config/dev.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chatservice/config/dev.exs b/src/chatservice/config/dev.exs index ffdef520d8..bcc4a35bdc 100644 --- a/src/chatservice/config/dev.exs +++ b/src/chatservice/config/dev.exs @@ -19,7 +19,7 @@ config :chatservice, ChatService.Repo, config :chatservice, ChatServiceWeb.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], + http: [ip: {0, 0, 0, 0}, port: 4000], check_origin: false, code_reloader: true, debug_errors: true, From 6162218678e50ede22e51f33b713c54b0c533919 Mon Sep 17 00:00:00 2001 From: Joshua Lee Date: Mon, 20 May 2024 22:16:11 -0400 Subject: [PATCH 09/26] working towards user-based chats with session ids genserver for persisting chats simplify skeleton macos nix allow for listing of topics, more idiomatic working POC of frontend with joining different channels, channel list dockerfile for chatservice remove HTML helper cruft --- src/chatservice/Dockerfile | 93 +++ src/chatservice/assets/css/app.css | 5 - src/chatservice/assets/js/app.js | 124 ++-- src/chatservice/assets/js/user_socket.js | 61 +- src/chatservice/assets/tailwind.config.js | 75 -- src/chatservice/config/config.exs | 32 +- src/chatservice/config/dev.exs | 6 +- src/chatservice/config/prod.exs | 6 - src/chatservice/config/runtime.exs | 22 +- src/chatservice/flake.lock | 59 ++ src/chatservice/flake.nix | 38 + .../lib/chatservice/application.ex | 8 +- .../lib/chatservice/chat_server.ex | 46 ++ src/chatservice/lib/chatservice/mailer.ex | 3 - src/chatservice/lib/chatservice_web.ex | 26 - .../chatservice_web/channels/chat_channel.ex | 36 +- .../chatservice_web/channels/user_socket.ex | 18 +- .../components/core_components.ex | 675 ------------------ .../components/layouts/app.html.heex | 33 +- .../chatservice_web/controllers/error_html.ex | 19 - .../chatservice_web/controllers/error_json.ex | 15 - .../controllers/page_controller.ex | 10 +- .../controllers/page_html/home.html.heex | 232 +----- .../lib/chatservice_web/endpoint.ex | 8 - src/chatservice/lib/chatservice_web/router.ex | 29 +- src/chatservice/mix.exs | 13 +- src/chatservice/shell.nix | 31 - .../channels/chat_channel_test.exs | 5 - .../controllers/error_html_test.exs | 14 - .../controllers/error_json_test.exs | 12 - .../controllers/page_controller_test.exs | 2 +- 31 files changed, 379 insertions(+), 1377 deletions(-) create mode 100644 src/chatservice/Dockerfile delete mode 100644 src/chatservice/assets/tailwind.config.js create mode 100644 src/chatservice/flake.lock create mode 100644 src/chatservice/flake.nix create mode 100644 src/chatservice/lib/chatservice/chat_server.ex delete mode 100644 src/chatservice/lib/chatservice/mailer.ex delete mode 100644 src/chatservice/lib/chatservice_web/components/core_components.ex delete mode 100644 src/chatservice/lib/chatservice_web/controllers/error_html.ex delete mode 100644 src/chatservice/lib/chatservice_web/controllers/error_json.ex delete mode 100644 src/chatservice/shell.nix delete mode 100644 src/chatservice/test/chatservice_web/controllers/error_html_test.exs delete mode 100644 src/chatservice/test/chatservice_web/controllers/error_json_test.exs diff --git a/src/chatservice/Dockerfile b/src/chatservice/Dockerfile new file mode 100644 index 0000000000..730f609e4f --- /dev/null +++ b/src/chatservice/Dockerfile @@ -0,0 +1,93 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of +# Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20210902-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.13.3-erlang-25.0-debian-bullseye-20210902-slim +# +ARG ELIXIR_VERSION=1.14.5 +ARG OTP_VERSION=23.0 +ARG DEBIAN_VERSION=bullseye-20210902-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force --verbose +RUN mix local.rebar --force --verbose + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" +ENV SECRET_KEY_BASE="mNhoOKKxgyvBIwbtw0P23waQcvUOmusb2U1moG2I7JQ3Bt6+MlGb5ZTrHwqbqy7j" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/chatservice ./ + +USER nobody + +ENTRYPOINT ["/app/bin/server"] diff --git a/src/chatservice/assets/css/app.css b/src/chatservice/assets/css/app.css index 378c8f9056..e69de29bb2 100644 --- a/src/chatservice/assets/css/app.css +++ b/src/chatservice/assets/css/app.css @@ -1,5 +0,0 @@ -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; - -/* This file is for your main application CSS */ diff --git a/src/chatservice/assets/js/app.js b/src/chatservice/assets/js/app.js index d5e278afe5..1167ab1bd3 100644 --- a/src/chatservice/assets/js/app.js +++ b/src/chatservice/assets/js/app.js @@ -1,44 +1,82 @@ -// 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 csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, { - longPollFallbackMs: 2500, - 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()) - -// 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 +import socket from "./user_socket.js"; + +const form = document.getElementById('login-form'); +const loginContainer = document.getElementById('login-container'); +const chatContainer = document.getElementById('chat-container'); +const chatMessages = document.getElementById('chat-messages'); +const chatInput = document.getElementById('chat-input'); +const nameInput = document.getElementById('name-input'); + +let chatInputEventHandler; +let channelEventHandler; + +const addEventHandlers = (channel) => { + removeEventHandlers(channel); + + chatInputEventHandler = (event) => { + if (event.key === 'Enter') { + const message = chatInput.value.trim(); + if (message) { + sendMessage(channel, nameInput.value, message); + chatInput.value = ''; + } + } + }; + chatInput.addEventListener('keydown', chatInputEventHandler); + + channelEventHandler = (payload) => { + renderMessage(payload); + }; + channel.on('shout', channelEventHandler); +}; + +const removeEventHandlers = (channel) => { + if (chatInputEventHandler) { + chatInput.removeEventListener('keydown', chatInputEventHandler); + chatInputEventHandler = null; + } + + if (channelEventHandler) { + channel.off('shout', channelEventHandler); + channelEventHandler = null; + } +}; + +form.addEventListener('submit', (event) => { + event.preventDefault(); + + const name = nameInput.value; + + // Sanitize the username + const sanitizedName = name.replace(/[^a-zA-Z0-9]/g, ''); + const urlParams = new URLSearchParams(window.location.search); + const channelName = urlParams.get('channel') || sanitizedName; + console.log(channelName); + const channel = socket.channel(`chat:${channelName}`, {}); + channel.join() + .receive("ok", resp => { + console.log("Joined successfully", resp); + + // Hide the login form and show the chat window + loginContainer.style.display = 'none'; + chatContainer.style.display = 'block'; + + addEventHandlers(channel); + }) + .receive("error", resp => { + console.log("Unable to join", resp); + }); +}); + +function sendMessage(channel, name, message) { + channel.push('shout', { + name: name, + message: message, + inserted_at: new Date() + }) +} + +function renderMessage(payload) { + console.log(payload) +} diff --git a/src/chatservice/assets/js/user_socket.js b/src/chatservice/assets/js/user_socket.js index 84676dead0..fb6107724a 100644 --- a/src/chatservice/assets/js/user_socket.js +++ b/src/chatservice/assets/js/user_socket.js @@ -1,64 +1,5 @@ -// NOTE: The contents of this file will only be executed if -// you uncomment its entry in "assets/js/app.js". - -// Bring in Phoenix channels client library: import {Socket} from "phoenix" -// And connect to the path in "lib/chatservice_web/endpoint.ex". We pass the -// token for authentication. Read below how it should be used. -let socket = new Socket("/socket", {params: {token: window.userToken}}) - -// When you connect, you'll often need to authenticate the client. -// For example, imagine you have an authentication plug, `MyAuth`, -// which authenticates the session and assigns a `:current_user`. -// If the current user exists you can assign the user's token in -// the connection for use in the layout. -// -// In your "lib/chatservice_web/router.ex": -// -// pipeline :browser do -// ... -// plug MyAuth -// plug :put_user_token -// end -// -// defp put_user_token(conn, _) do -// if current_user = conn.assigns[:current_user] do -// token = Phoenix.Token.sign(conn, "user socket", current_user.id) -// assign(conn, :user_token, token) -// else -// conn -// end -// end -// -// Now you need to pass this token to JavaScript. You can do so -// inside a script tag in "lib/chatservice_web/templates/layout/app.html.heex": -// -// -// -// You will need to verify the user token in the "connect/3" function -// in "lib/chatservice_web/channels/user_socket.ex": -// -// def connect(%{"token" => token}, socket, _connect_info) do -// # max_age: 1209600 is equivalent to two weeks in seconds -// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do -// {:ok, user_id} -> -// {:ok, assign(socket, :user, user_id)} -// -// {:error, reason} -> -// :error -// end -// end -// -// Finally, connect to the socket: +let socket = new Socket("/socket", {params: {token: window.personToken}}) socket.connect() - -// Now that you are connected, you can join channels with a topic. -// Let's assume you have a channel with a topic named `room` and the -// subtopic is its id - in this case 42: -let channel = socket.channel("room:42", {}) -channel.join() - .receive("ok", resp => { console.log("Joined successfully", resp) }) - .receive("error", resp => { console.log("Unable to join", resp) }) - export default socket diff --git a/src/chatservice/assets/tailwind.config.js b/src/chatservice/assets/tailwind.config.js deleted file mode 100644 index bcc126a1a0..0000000000 --- a/src/chatservice/assets/tailwind.config.js +++ /dev/null @@ -1,75 +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/chatservice_web.ex", - "../lib/chatservice_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-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), - 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/src/chatservice/config/config.exs b/src/chatservice/config/config.exs index 54a8540c09..e2a752f829 100644 --- a/src/chatservice/config/config.exs +++ b/src/chatservice/config/config.exs @@ -15,21 +15,9 @@ config :chatservice, config :chatservice, ChatServiceWeb.Endpoint, url: [host: "localhost"], adapter: Bandit.PhoenixAdapter, - render_errors: [ - formats: [html: ChatServiceWeb.ErrorHTML, json: ChatServiceWeb.ErrorJSON], - layout: false - ], pubsub_server: ChatService.PubSub, - live_view: [signing_salt: "vhPzRN9o"] - -# Configures the mailer -# -# By default it uses the "Local" adapter which stores the emails -# locally. You can see the emails in your browser, at "/dev/mailbox". -# -# For production it's recommended to configure a different adapter -# at the `config/runtime.exs`. -config :chatservice, ChatService.Mailer, adapter: Swoosh.Adapters.Local + live_view: [signing_salt: "vhPzRN9o"], + render_errors: [accepts: ~w(html json), layout: false] # Configure esbuild (the version is required) config :esbuild, @@ -41,18 +29,6 @@ config :esbuild, env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] -# Configure tailwind (the version is required) -config :tailwind, - version: "3.4.0", - chatservice: [ - 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", @@ -66,10 +42,6 @@ config :opentelemetry, span_processor: :batch, traces_exporter: :otlp -config :opentelemetry_exporter, - otlp_protocol: :http_protobuf, - otlp_endpoint: "http://localhost:4318" - # 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/src/chatservice/config/dev.exs b/src/chatservice/config/dev.exs index bcc4a35bdc..90d6c7268b 100644 --- a/src/chatservice/config/dev.exs +++ b/src/chatservice/config/dev.exs @@ -25,8 +25,7 @@ config :chatservice, ChatServiceWeb.Endpoint, debug_errors: true, secret_key_base: "n+w1XD8A1cQBvipOZZpvch1nKpb1enMm4Jhqn/uNPr+ypQ/PA8JxiztV6VPElipy", watchers: [ - esbuild: {Esbuild, :install_and_run, [:chatservice, ~w(--sourcemap=inline --watch)]}, - tailwind: {Tailwind, :install_and_run, [:chatservice, ~w(--watch)]} + esbuild: {Esbuild, :install_and_run, [:chatservice, ~w(--sourcemap=inline --watch)]} ] # ## SSL Support @@ -77,6 +76,3 @@ config :phoenix, :plug_init_mode, :runtime # Include HEEx debug annotations as HTML comments in rendered markup config :phoenix_live_view, :debug_heex_annotations, true - -# Disable swoosh api client as it is only required for production adapters. -config :swoosh, :api_client, false diff --git a/src/chatservice/config/prod.exs b/src/chatservice/config/prod.exs index ba2fab03eb..40789cf3fc 100644 --- a/src/chatservice/config/prod.exs +++ b/src/chatservice/config/prod.exs @@ -8,12 +8,6 @@ import Config config :chatservice, ChatServiceWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" -# Configures Swoosh API Client -config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: ChatService.Finch - -# Disable Swoosh Local Memory Storage -config :swoosh, local: false - # Do not print debug messages in production config :logger, level: :info diff --git a/src/chatservice/config/runtime.exs b/src/chatservice/config/runtime.exs index 3c737c53de..31886e2d9d 100644 --- a/src/chatservice/config/runtime.exs +++ b/src/chatservice/config/runtime.exs @@ -48,8 +48,8 @@ if config_env() == :prod do 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") + host = System.get_env("CHAT_SERVICE_HOST") || "localhost" + port = String.to_integer(System.get_env("CHAT_SERVICE_PORT") || "4000") config :chatservice, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") @@ -96,22 +96,4 @@ if config_env() == :prod do # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. - - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Also, you may need to configure the Swoosh API client of your choice if you - # are not using SMTP. Here is an example of the configuration: - # - # config :chatservice, ChatService.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # For this example you need include a HTTP client required by Swoosh API client. - # Swoosh supports Hackney and Finch out of the box: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Hackney - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/src/chatservice/flake.lock b/src/chatservice/flake.lock new file mode 100644 index 0000000000..11fab0a38f --- /dev/null +++ b/src/chatservice/flake.lock @@ -0,0 +1,59 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1720027103, + "narHash": "sha256-Q92DHQjIvaMLpawMdXnbKQjCkzAWqjhjWJYS5RcKujY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "61684d356e41c97f80087e89659283d00fe032ab", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/src/chatservice/flake.nix b/src/chatservice/flake.nix new file mode 100644 index 0000000000..fbccb36140 --- /dev/null +++ b/src/chatservice/flake.nix @@ -0,0 +1,38 @@ +{ + description = "Development environment"; + + inputs = { + flake-utils = { url = "github:numtide/flake-utils"; }; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + inherit (nixpkgs.lib) optional; + pkgs = import nixpkgs { inherit system; }; + + elixir = pkgs.beam.packages.erlang.elixir; + elixir-ls = pkgs.beam.packages.erlang.elixir_ls; + locales = pkgs.glibcLocales; + + hooks = '' + mkdir -p .nix-mix + mkdir -p .nix-hex + export MIX_HOME=$PWD/.nix-mix + export HEX_HOME=$PWD/.nix-hex + export PATH=$MIX_HOME/bin:$PATH + export PATH=$HEX_HOME/bin:$PATH + export LANG=en_US.UTF-8 + ''; + in + { + devShell = pkgs.mkShell { + buildInputs = [ + elixir + locales + ]; + + shellHook = hooks; + }; + }); +} diff --git a/src/chatservice/lib/chatservice/application.ex b/src/chatservice/lib/chatservice/application.ex index 7ee422aec1..058835cf05 100644 --- a/src/chatservice/lib/chatservice/application.ex +++ b/src/chatservice/lib/chatservice/application.ex @@ -14,13 +14,9 @@ defmodule ChatService.Application do children = [ ChatServiceWeb.Telemetry, ChatService.Repo, - {DNSCluster, query: Application.get_env(:chatservice, :dns_cluster_query) || :ignore}, + # {DNSCluster, query: Application.get_env(:chatservice, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: ChatService.PubSub}, - # Start the Finch HTTP client for sending emails - {Finch, name: ChatService.Finch}, - # Start a worker by calling: ChatService.Worker.start_link(arg) - # {ChatService.Worker, arg}, - # Start to serve requests, typically the last entry + {Registry, keys: :unique, name: ChatService.Registry}, ChatServiceWeb.Endpoint ] diff --git a/src/chatservice/lib/chatservice/chat_server.ex b/src/chatservice/lib/chatservice/chat_server.ex new file mode 100644 index 0000000000..9431afe394 --- /dev/null +++ b/src/chatservice/lib/chatservice/chat_server.ex @@ -0,0 +1,46 @@ +defmodule ChatService.ChatServer do + require OpenTelemetry.Tracer + + def start_chat(topic) do + case Registry.lookup(ChatService.Registry, topic) do + [] -> start_link(topic) + [first | _] -> first + end + end + + def start_link(topic) do + GenServer.start_link(__MODULE__, %{}, name: via_tuple(topic)) + end + + def list_topics() do + ChatService.Registry + |> Registry.select([{{:"$1", :_, :_}, [], [{{:"$1"}}]}]) + |> Enum.map(fn {key} -> key end) + end + + def send_message(topic, message) do + OpenTelemetry.Tracer.with_span :send_message do + GenServer.call(via_tuple(topic), {:send_message, message}) + end + end + + def get_messages(topic) do + GenServer.call(via_tuple(topic), :get_messages) + end + + def init(_) do + {:ok, []} + end + + def handle_call({:send_message, message}, _from, state) do + {:reply, :ok, [message | state]} + end + + def handle_call(:get_messages, _from, state) do + {:reply, Enum.reverse(state), state} + end + + defp via_tuple(topic) do + {:via, Registry, {ChatService.Registry, topic}} + end +end diff --git a/src/chatservice/lib/chatservice/mailer.ex b/src/chatservice/lib/chatservice/mailer.ex deleted file mode 100644 index f631f6069f..0000000000 --- a/src/chatservice/lib/chatservice/mailer.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule ChatService.Mailer do - use Swoosh.Mailer, otp_app: :chatservice -end diff --git a/src/chatservice/lib/chatservice_web.ex b/src/chatservice/lib/chatservice_web.ex index 5889be4cf2..484e91a759 100644 --- a/src/chatservice/lib/chatservice_web.ex +++ b/src/chatservice/lib/chatservice_web.ex @@ -49,23 +49,6 @@ defmodule ChatServiceWeb do end end - def live_view do - quote do - use Phoenix.LiveView, - layout: {ChatServiceWeb.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 @@ -81,15 +64,6 @@ defmodule ChatServiceWeb do defp html_helpers do quote do - # HTML escaping functionality - import Phoenix.HTML - # Core UI components and translation - import ChatServiceWeb.CoreComponents - import ChatServiceWeb.Gettext - - # Shortcut for generating JS commands - alias Phoenix.LiveView.JS - # Routes generation with the ~p sigil unquote(verified_routes()) end diff --git a/src/chatservice/lib/chatservice_web/channels/chat_channel.ex b/src/chatservice/lib/chatservice_web/channels/chat_channel.ex index f7c62b6d58..5a5d02a388 100644 --- a/src/chatservice/lib/chatservice_web/channels/chat_channel.ex +++ b/src/chatservice/lib/chatservice_web/channels/chat_channel.ex @@ -1,32 +1,32 @@ defmodule ChatServiceWeb.ChatChannel do use ChatServiceWeb, :channel + require Logger + require OpenTelemetry.Tracer + alias ChatService.ChatServer @impl true - def join("chat:lobby", payload, socket) do - if authorized?(payload) do - {:ok, socket} - else - {:error, %{reason: "unauthorized"}} + def join(topic, _payload, socket) do + OpenTelemetry.Tracer.with_span :join do + ChatServer.start_chat(topic) + send(self(), :after_join) + {:ok, assign(socket, :topic, topic)} end end - # Channels can be used in a request/response fashion - # by sending replies to requests from the client @impl true - def handle_in("ping", payload, socket) do - {:reply, {:ok, payload}, socket} + def handle_in("shout", payload, socket) do + OpenTelemetry.Tracer.with_span :shout do + ChatServer.send_message(socket.assigns.topic, payload) + broadcast(socket, "shout", payload) + {:noreply, socket} + end end - # It is also common to receive messages from the client and - # broadcast to everyone in the current topic (chat:lobby). @impl true - def handle_in("shout", payload, socket) do - broadcast(socket, "shout", payload) - {:noreply, socket} - end + def handle_info(:after_join, socket) do + ChatServer.get_messages(socket.assigns.topic) + |> Enum.each(fn msg -> push(socket, "shout", msg) end) - # Add authorization logic here as required. - defp authorized?(_payload) do - true + {:noreply, socket} end end diff --git a/src/chatservice/lib/chatservice_web/channels/user_socket.ex b/src/chatservice/lib/chatservice_web/channels/user_socket.ex index 77409668c7..efc11decaa 100644 --- a/src/chatservice/lib/chatservice_web/channels/user_socket.ex +++ b/src/chatservice/lib/chatservice_web/channels/user_socket.ex @@ -1,14 +1,12 @@ defmodule ChatServiceWeb.UserSocket do + require Logger use Phoenix.Socket # A Socket handler - # - # It's possible to control the websocket connection and - # assign values that can be accessed by your channel topics. ## Channels - channel "chat:*", ChatServiceWeb.ChatChannel + channel("chat:*", ChatServiceWeb.ChatChannel) # Socket params are passed from the client and can # be used to verify and authenticate a user. After @@ -25,8 +23,14 @@ defmodule ChatServiceWeb.UserSocket do # See `Phoenix.Token` documentation for examples in # performing token verification on connect. @impl true - def connect(_params, socket, _connect_info) do - {:ok, socket} + def connect(params, socket, _connect_info) do + # In a real application we might want to verify + # this session ID but since this is a demo we will + # trust the front-end. + + # This session ID will provide a way to identify users + # across different chats. + {:ok, assign(socket, :session_id, params["session_id"])} end # Socket IDs are topics that allow you to identify all sockets for a given user: @@ -40,5 +44,5 @@ defmodule ChatServiceWeb.UserSocket do # # Returning `nil` makes this socket anonymous. @impl true - def id(_socket), do: nil + def id(socket), do: "user_socket:#{socket.assigns.session_id}" end diff --git a/src/chatservice/lib/chatservice_web/components/core_components.ex b/src/chatservice/lib/chatservice_web/components/core_components.ex deleted file mode 100644 index 7e2161df67..0000000000 --- a/src/chatservice/lib/chatservice_web/components/core_components.ex +++ /dev/null @@ -1,675 +0,0 @@ -defmodule ChatServiceWeb.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 - import ChatServiceWeb.Gettext - - @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] %> - <%= gettext("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, - 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", - 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 - # When using gettext, we typically pass the strings we want - # to translate as a static argument: - # - # # Translate the number of files with plural rules - # dngettext("errors", "1 file", "%{count} files", count) - # - # However the error messages in our forms and APIs are generated - # dynamically, so we need to translate them by calling Gettext - # with our gettext backend as first argument. Translations are - # available in the errors.po file (as we use the "errors" domain). - if count = opts[:count] do - Gettext.dngettext(ChatServiceWeb.Gettext, "errors", msg, msg, count, opts) - else - Gettext.dgettext(ChatServiceWeb.Gettext, "errors", msg, opts) - 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/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex b/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex index e23bfc81c4..05433985bf 100644 --- a/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex +++ b/src/chatservice/lib/chatservice_web/components/layouts/app.html.heex @@ -1,32 +1 @@ -
-
-
- - - -

- v<%= Application.spec(:phoenix, :vsn) %> -

-
- -
-
-
-
- <.flash_group flash={@flash} /> - <%= @inner_content %> -
-
+<%= @inner_content %> diff --git a/src/chatservice/lib/chatservice_web/controllers/error_html.ex b/src/chatservice/lib/chatservice_web/controllers/error_html.ex deleted file mode 100644 index 5d1d44f187..0000000000 --- a/src/chatservice/lib/chatservice_web/controllers/error_html.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule ChatServiceWeb.ErrorHTML do - use ChatServiceWeb, :html - - # If you want to customize your error pages, - # uncomment the embed_templates/1 call below - # and add pages to the error directory: - # - # * lib/chatservice_web/controllers/error_html/404.html.heex - # * lib/chatservice_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/src/chatservice/lib/chatservice_web/controllers/error_json.ex b/src/chatservice/lib/chatservice_web/controllers/error_json.ex deleted file mode 100644 index 5e04742555..0000000000 --- a/src/chatservice/lib/chatservice_web/controllers/error_json.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule ChatServiceWeb.ErrorJSON do - # 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/src/chatservice/lib/chatservice_web/controllers/page_controller.ex b/src/chatservice/lib/chatservice_web/controllers/page_controller.ex index 03c6def6bc..3ff67fe8fc 100644 --- a/src/chatservice/lib/chatservice_web/controllers/page_controller.ex +++ b/src/chatservice/lib/chatservice_web/controllers/page_controller.ex @@ -2,8 +2,12 @@ defmodule ChatServiceWeb.PageController do use ChatServiceWeb, :controller def home(conn, _params) do - # The home page is often custom made, - # so skip the default app layout. - render(conn, :home, layout: false) + topics = ChatService.ChatServer.list_topics() + + assigns = [ + topics: topics + ] + + render(conn, :home, assigns) end end diff --git a/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex b/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex index dc1820b11e..1f9abf1497 100644 --- a/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex +++ b/src/chatservice/lib/chatservice_web/controllers/page_html/home.html.heex @@ -1,222 +1,14 @@ -<.flash_group flash={@flash} /> -