Init: Notes app phx project
This commit is contained in:
parent
a60a05bab4
commit
f47aedfb67
38 changed files with 2772 additions and 0 deletions
6
Elixir/notes_app/.formatter.exs
Normal file
6
Elixir/notes_app/.formatter.exs
Normal file
|
@ -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"]
|
||||||
|
]
|
41
Elixir/notes_app/.gitignore
vendored
Normal file
41
Elixir/notes_app/.gitignore
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# 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").
|
||||||
|
notes_app-*.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/
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.db-*
|
||||||
|
|
18
Elixir/notes_app/README.md
Normal file
18
Elixir/notes_app/README.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# NotesApp
|
||||||
|
|
||||||
|
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
|
5
Elixir/notes_app/assets/css/app.css
Normal file
5
Elixir/notes_app/assets/css/app.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
/* This file is for your main application CSS */
|
44
Elixir/notes_app/assets/js/app.js
Normal file
44
Elixir/notes_app/assets/js/app.js
Normal file
|
@ -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
|
||||||
|
|
74
Elixir/notes_app/assets/tailwind.config.js
Normal file
74
Elixir/notes_app/assets/tailwind.config.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// See the Tailwind configuration guide for advanced usage
|
||||||
|
// https://tailwindcss.com/docs/configuration
|
||||||
|
|
||||||
|
const plugin = require("tailwindcss/plugin")
|
||||||
|
const fs = require("fs")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./js/**/*.js",
|
||||||
|
"../lib/notes_app_web.ex",
|
||||||
|
"../lib/notes_app_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:
|
||||||
|
//
|
||||||
|
// <div class="phx-click-loading:animate-ping">
|
||||||
|
//
|
||||||
|
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})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
165
Elixir/notes_app/assets/vendor/topbar.js
vendored
Normal file
165
Elixir/notes_app/assets/vendor/topbar.js
vendored
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
* topbar 2.0.0, 2023-02-04
|
||||||
|
* https://buunguyen.github.io/topbar
|
||||||
|
* Copyright (c) 2021 Buu Nguyen
|
||||||
|
*/
|
||||||
|
(function (window, document) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// https://gist.github.com/paulirish/1579671
|
||||||
|
(function () {
|
||||||
|
var lastTime = 0;
|
||||||
|
var vendors = ["ms", "moz", "webkit", "o"];
|
||||||
|
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||||
|
window.requestAnimationFrame =
|
||||||
|
window[vendors[x] + "RequestAnimationFrame"];
|
||||||
|
window.cancelAnimationFrame =
|
||||||
|
window[vendors[x] + "CancelAnimationFrame"] ||
|
||||||
|
window[vendors[x] + "CancelRequestAnimationFrame"];
|
||||||
|
}
|
||||||
|
if (!window.requestAnimationFrame)
|
||||||
|
window.requestAnimationFrame = function (callback, element) {
|
||||||
|
var currTime = new Date().getTime();
|
||||||
|
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||||||
|
var id = window.setTimeout(function () {
|
||||||
|
callback(currTime + timeToCall);
|
||||||
|
}, timeToCall);
|
||||||
|
lastTime = currTime + timeToCall;
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
if (!window.cancelAnimationFrame)
|
||||||
|
window.cancelAnimationFrame = function (id) {
|
||||||
|
clearTimeout(id);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
var canvas,
|
||||||
|
currentProgress,
|
||||||
|
showing,
|
||||||
|
progressTimerId = null,
|
||||||
|
fadeTimerId = null,
|
||||||
|
delayTimerId = null,
|
||||||
|
addEvent = function (elem, type, handler) {
|
||||||
|
if (elem.addEventListener) elem.addEventListener(type, handler, false);
|
||||||
|
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
|
||||||
|
else elem["on" + type] = handler;
|
||||||
|
},
|
||||||
|
options = {
|
||||||
|
autoRun: true,
|
||||||
|
barThickness: 3,
|
||||||
|
barColors: {
|
||||||
|
0: "rgba(26, 188, 156, .9)",
|
||||||
|
".25": "rgba(52, 152, 219, .9)",
|
||||||
|
".50": "rgba(241, 196, 15, .9)",
|
||||||
|
".75": "rgba(230, 126, 34, .9)",
|
||||||
|
"1.0": "rgba(211, 84, 0, .9)",
|
||||||
|
},
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: "rgba(0, 0, 0, .6)",
|
||||||
|
className: null,
|
||||||
|
},
|
||||||
|
repaint = function () {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = options.barThickness * 5; // need space for shadow
|
||||||
|
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
ctx.shadowBlur = options.shadowBlur;
|
||||||
|
ctx.shadowColor = options.shadowColor;
|
||||||
|
|
||||||
|
var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
|
for (var stop in options.barColors)
|
||||||
|
lineGradient.addColorStop(stop, options.barColors[stop]);
|
||||||
|
ctx.lineWidth = options.barThickness;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, options.barThickness / 2);
|
||||||
|
ctx.lineTo(
|
||||||
|
Math.ceil(currentProgress * canvas.width),
|
||||||
|
options.barThickness / 2
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = lineGradient;
|
||||||
|
ctx.stroke();
|
||||||
|
},
|
||||||
|
createCanvas = function () {
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
|
var style = canvas.style;
|
||||||
|
style.position = "fixed";
|
||||||
|
style.top = style.left = style.right = style.margin = style.padding = 0;
|
||||||
|
style.zIndex = 100001;
|
||||||
|
style.display = "none";
|
||||||
|
if (options.className) canvas.classList.add(options.className);
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
addEvent(window, "resize", repaint);
|
||||||
|
},
|
||||||
|
topbar = {
|
||||||
|
config: function (opts) {
|
||||||
|
for (var key in opts)
|
||||||
|
if (options.hasOwnProperty(key)) options[key] = opts[key];
|
||||||
|
},
|
||||||
|
show: function (delay) {
|
||||||
|
if (showing) return;
|
||||||
|
if (delay) {
|
||||||
|
if (delayTimerId) return;
|
||||||
|
delayTimerId = setTimeout(() => topbar.show(), delay);
|
||||||
|
} else {
|
||||||
|
showing = true;
|
||||||
|
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
|
||||||
|
if (!canvas) createCanvas();
|
||||||
|
canvas.style.opacity = 1;
|
||||||
|
canvas.style.display = "block";
|
||||||
|
topbar.progress(0);
|
||||||
|
if (options.autoRun) {
|
||||||
|
(function loop() {
|
||||||
|
progressTimerId = window.requestAnimationFrame(loop);
|
||||||
|
topbar.progress(
|
||||||
|
"+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progress: function (to) {
|
||||||
|
if (typeof to === "undefined") return currentProgress;
|
||||||
|
if (typeof to === "string") {
|
||||||
|
to =
|
||||||
|
(to.indexOf("+") >= 0 || to.indexOf("-") >= 0
|
||||||
|
? currentProgress
|
||||||
|
: 0) + parseFloat(to);
|
||||||
|
}
|
||||||
|
currentProgress = to > 1 ? 1 : to;
|
||||||
|
repaint();
|
||||||
|
return currentProgress;
|
||||||
|
},
|
||||||
|
hide: function () {
|
||||||
|
clearTimeout(delayTimerId);
|
||||||
|
delayTimerId = null;
|
||||||
|
if (!showing) return;
|
||||||
|
showing = false;
|
||||||
|
if (progressTimerId != null) {
|
||||||
|
window.cancelAnimationFrame(progressTimerId);
|
||||||
|
progressTimerId = null;
|
||||||
|
}
|
||||||
|
(function loop() {
|
||||||
|
if (topbar.progress("+.1") >= 1) {
|
||||||
|
canvas.style.opacity -= 0.05;
|
||||||
|
if (canvas.style.opacity <= 0.05) {
|
||||||
|
canvas.style.display = "none";
|
||||||
|
fadeTimerId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fadeTimerId = window.requestAnimationFrame(loop);
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof module === "object" && typeof module.exports === "object") {
|
||||||
|
module.exports = topbar;
|
||||||
|
} else if (typeof define === "function" && define.amd) {
|
||||||
|
define(function () {
|
||||||
|
return topbar;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.topbar = topbar;
|
||||||
|
}
|
||||||
|
}.call(this, window, document));
|
66
Elixir/notes_app/config/config.exs
Normal file
66
Elixir/notes_app/config/config.exs
Normal file
|
@ -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 :notes_app,
|
||||||
|
ecto_repos: [NotesApp.Repo],
|
||||||
|
generators: [timestamp_type: :utc_datetime]
|
||||||
|
|
||||||
|
# Configures the endpoint
|
||||||
|
config :notes_app, NotesAppWeb.Endpoint,
|
||||||
|
url: [host: "localhost"],
|
||||||
|
adapter: Bandit.PhoenixAdapter,
|
||||||
|
render_errors: [
|
||||||
|
formats: [html: NotesAppWeb.ErrorHTML, json: NotesAppWeb.ErrorJSON],
|
||||||
|
layout: false
|
||||||
|
],
|
||||||
|
pubsub_server: NotesApp.PubSub,
|
||||||
|
live_view: [signing_salt: "He9WNGLS"]
|
||||||
|
|
||||||
|
# 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 :notes_app, NotesApp.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
|
# Configure esbuild (the version is required)
|
||||||
|
config :esbuild,
|
||||||
|
version: "0.17.11",
|
||||||
|
notes_app: [
|
||||||
|
args:
|
||||||
|
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||||
|
cd: Path.expand("../assets", __DIR__),
|
||||||
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Configure tailwind (the version is required)
|
||||||
|
config :tailwind,
|
||||||
|
version: "3.4.3",
|
||||||
|
notes_app: [
|
||||||
|
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"
|
82
Elixir/notes_app/config/dev.exs
Normal file
82
Elixir/notes_app/config/dev.exs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
# Configure your database
|
||||||
|
config :notes_app, NotesApp.Repo,
|
||||||
|
database: Path.expand("../notes_app_dev.db", __DIR__),
|
||||||
|
pool_size: 5,
|
||||||
|
stacktrace: true,
|
||||||
|
show_sensitive_data_on_connection_error: true
|
||||||
|
|
||||||
|
# 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 :notes_app, NotesAppWeb.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: "mQh9OiutrVG3ttXYKwX0owXUInJ+SI16fMmOmYzJ+/e9/zq5wgPFRAPDCncLs9sC",
|
||||||
|
watchers: [
|
||||||
|
esbuild: {Esbuild, :install_and_run, [:notes_app, ~w(--sourcemap=inline --watch)]},
|
||||||
|
tailwind: {Tailwind, :install_and_run, [:notes_app, ~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 :notes_app, NotesAppWeb.Endpoint,
|
||||||
|
live_reload: [
|
||||||
|
patterns: [
|
||||||
|
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||||
|
~r"priv/gettext/.*(po)$",
|
||||||
|
~r"lib/notes_app_web/(controllers|live|components)/.*(ex|heex)$"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Enable dev routes for dashboard and mailbox
|
||||||
|
config :notes_app, dev_routes: true
|
||||||
|
|
||||||
|
# Do not include metadata nor timestamps in development logs
|
||||||
|
config :logger, :console, format: "[$level] $message\n"
|
||||||
|
|
||||||
|
# Set a higher stacktrace during development. Avoid configuring such
|
||||||
|
# in production as building large stacktraces may be expensive.
|
||||||
|
config :phoenix, :stacktrace_depth, 20
|
||||||
|
|
||||||
|
# Initialize plugs at runtime for faster development compilation
|
||||||
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|
||||||
|
config :phoenix_live_view,
|
||||||
|
# Include HEEx debug annotations as HTML comments in rendered markup
|
||||||
|
debug_heex_annotations: true,
|
||||||
|
# Enable helpful, but potentially expensive runtime checks
|
||||||
|
enable_expensive_runtime_checks: true
|
||||||
|
|
||||||
|
# Disable swoosh api client as it is only required for production adapters.
|
||||||
|
config :swoosh, :api_client, false
|
20
Elixir/notes_app/config/prod.exs
Normal file
20
Elixir/notes_app/config/prod.exs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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 :notes_app, NotesAppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
|
||||||
|
|
||||||
|
# Configures Swoosh API Client
|
||||||
|
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: NotesApp.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.
|
113
Elixir/notes_app/config/runtime.exs
Normal file
113
Elixir/notes_app/config/runtime.exs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
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/notes_app 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 :notes_app, NotesAppWeb.Endpoint, server: true
|
||||||
|
end
|
||||||
|
|
||||||
|
if config_env() == :prod do
|
||||||
|
database_path =
|
||||||
|
System.get_env("DATABASE_PATH") ||
|
||||||
|
raise """
|
||||||
|
environment variable DATABASE_PATH is missing.
|
||||||
|
For example: /etc/notes_app/notes_app.db
|
||||||
|
"""
|
||||||
|
|
||||||
|
config :notes_app, NotesApp.Repo,
|
||||||
|
database: database_path,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
|
||||||
|
|
||||||
|
# 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 :notes_app, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||||
|
|
||||||
|
config :notes_app, NotesAppWeb.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 :notes_app, NotesAppWeb.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 :notes_app, NotesAppWeb.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 :notes_app, NotesApp.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
|
37
Elixir/notes_app/config/test.exs
Normal file
37
Elixir/notes_app/config/test.exs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import Config
|
||||||
|
|
||||||
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
|
||||||
|
# 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 :notes_app, NotesApp.Repo,
|
||||||
|
database: Path.expand("../notes_app_test.db", __DIR__),
|
||||||
|
pool_size: 5,
|
||||||
|
pool: Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
|
# We don't run a server during test. If one is required,
|
||||||
|
# you can enable the server option below.
|
||||||
|
config :notes_app, NotesAppWeb.Endpoint,
|
||||||
|
http: [ip: {127, 0, 0, 1}, port: 4002],
|
||||||
|
secret_key_base: "YKtYPCCPpdbg9Lh7QLm0wHREqg/j4LCe/cd1TrngAaLMSwl/pN+eVN/TYLl0nzk1",
|
||||||
|
server: false
|
||||||
|
|
||||||
|
# In test we don't send emails
|
||||||
|
config :notes_app, NotesApp.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
|
||||||
|
|
||||||
|
# Enable helpful, but potentially expensive runtime checks
|
||||||
|
config :phoenix_live_view,
|
||||||
|
enable_expensive_runtime_checks: true
|
87
Elixir/notes_app/mix.exs
Normal file
87
Elixir/notes_app/mix.exs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
defmodule NotesApp.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :notes_app,
|
||||||
|
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: {NotesApp.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
|
||||||
|
[
|
||||||
|
{:bcrypt_elixir, "~> 3.0"},
|
||||||
|
{:phoenix, "~> 1.7.14"},
|
||||||
|
{:phoenix_ecto, "~> 4.5"},
|
||||||
|
{:ecto_sql, "~> 3.10"},
|
||||||
|
{:ecto_sqlite3, ">= 0.0.0"},
|
||||||
|
{:phoenix_html, "~> 4.1"},
|
||||||
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
|
# TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
|
||||||
|
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
|
||||||
|
{:floki, ">= 0.30.0", only: :test},
|
||||||
|
{: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, "~> 1.0"},
|
||||||
|
{:telemetry_poller, "~> 1.0"},
|
||||||
|
{:gettext, "~> 0.20"},
|
||||||
|
{:jason, "~> 1.2"},
|
||||||
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
|
{:bandit, "~> 1.5"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Aliases are shortcuts or tasks specific to the current project.
|
||||||
|
# For example, to install project dependencies and perform other setup tasks, run:
|
||||||
|
#
|
||||||
|
# $ mix setup
|
||||||
|
#
|
||||||
|
# See the documentation for `Mix` for more info on aliases.
|
||||||
|
defp aliases do
|
||||||
|
[
|
||||||
|
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
||||||
|
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||||
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||||
|
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||||
|
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
|
||||||
|
"assets.build": ["tailwind notes_app", "esbuild notes_app"],
|
||||||
|
"assets.deploy": [
|
||||||
|
"tailwind notes_app --minify",
|
||||||
|
"esbuild notes_app --minify",
|
||||||
|
"phx.digest"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
46
Elixir/notes_app/mix.lock
Normal file
46
Elixir/notes_app/mix.lock
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
%{
|
||||||
|
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
|
||||||
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||||
|
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
|
||||||
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||||
|
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||||
|
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||||
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
|
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
|
||||||
|
"ecto": {:hex, :ecto, "3.12.2", "bae2094f038e9664ce5f089e5f3b6132a535d8b018bd280a485c2f33df5c0ce1", [: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", "492e67c70f3a71c6afe80d946d3ced52ecc57c53c9829791bfff1830ff5a1f0c"},
|
||||||
|
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
||||||
|
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.1", "96d08639aa1fb07a087daa2220ebc68d9f4f5dbb8aed930e771d848d8d472d32", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "dbd6a68b5364fe6e9ce5d70336709d62edaebbb4cb4acb5f13ec825f64fa48cc"},
|
||||||
|
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||||
|
"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, "1.0.1", "f9e2f984f5b8d195815d52d0ba264798c12c8d2f2606f76fa4c60e8ebe39474d", [:mix], [], "hexpm", "f250b33274e3e56513644858c116f255d35c767c2b8e96a512fe7839ef9306a1"},
|
||||||
|
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
|
||||||
|
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
||||||
|
"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.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
|
||||||
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
||||||
|
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
|
||||||
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
|
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||||
|
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||||
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
|
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
||||||
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
|
||||||
|
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
|
||||||
|
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [: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", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
|
||||||
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
|
||||||
|
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.6", "47d2669995ea326e5c71f5c1bc9177109cebf211385c638faa7b5862a401e516", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56e4f1642a0b20edc2488cab30e5439595e0d8b5b259f76ef98b1c4e2e5b527"},
|
||||||
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||||
|
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||||
|
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||||
|
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||||
|
"swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [: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.2.3", [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.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"},
|
||||||
|
"tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
|
||||||
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
|
||||||
|
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||||
|
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
|
||||||
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
|
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
|
||||||
|
}
|
112
Elixir/notes_app/priv/gettext/en/LC_MESSAGES/errors.po
Normal file
112
Elixir/notes_app/priv/gettext/en/LC_MESSAGES/errors.po
Normal file
|
@ -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 ""
|
4
Elixir/notes_app/priv/repo/migrations/.formatter.exs
Normal file
4
Elixir/notes_app/priv/repo/migrations/.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
import_deps: [:ecto_sql],
|
||||||
|
inputs: ["*.exs"]
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule NotesApp.Repo.Migrations.CreateUsersAuthTables do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:users) do
|
||||||
|
add :email, :string, null: false, collate: :nocase
|
||||||
|
add :hashed_password, :string, null: false
|
||||||
|
add :confirmed_at, :utc_datetime
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:users, [:email])
|
||||||
|
|
||||||
|
create table(:users_tokens) do
|
||||||
|
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||||
|
add :token, :binary, null: false, size: 32
|
||||||
|
add :context, :string, null: false
|
||||||
|
add :sent_to, :string
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime, updated_at: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:users_tokens, [:user_id])
|
||||||
|
create unique_index(:users_tokens, [:context, :token])
|
||||||
|
end
|
||||||
|
end
|
11
Elixir/notes_app/priv/repo/seeds.exs
Normal file
11
Elixir/notes_app/priv/repo/seeds.exs
Normal file
|
@ -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:
|
||||||
|
#
|
||||||
|
# NotesApp.Repo.insert!(%NotesApp.SomeSchema{})
|
||||||
|
#
|
||||||
|
# We recommend using the bang functions (`insert!`, `update!`
|
||||||
|
# and so on) as they will fail if something goes wrong.
|
BIN
Elixir/notes_app/priv/static/favicon.ico
Normal file
BIN
Elixir/notes_app/priv/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 152 B |
6
Elixir/notes_app/priv/static/images/logo.svg
Normal file
6
Elixir/notes_app/priv/static/images/logo.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
|
||||||
|
fill="#FD4F00"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3 KiB |
5
Elixir/notes_app/priv/static/robots.txt
Normal file
5
Elixir/notes_app/priv/static/robots.txt
Normal file
|
@ -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: /
|
508
Elixir/notes_app/test/notes_app/accounts_test.exs
Normal file
508
Elixir/notes_app/test/notes_app/accounts_test.exs
Normal file
|
@ -0,0 +1,508 @@
|
||||||
|
defmodule NotesApp.AccountsTest do
|
||||||
|
use NotesApp.DataCase
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
alias NotesApp.Accounts.{User, UserToken}
|
||||||
|
|
||||||
|
describe "get_user_by_email/1" do
|
||||||
|
test "does not return the user if the email does not exist" do
|
||||||
|
refute Accounts.get_user_by_email("unknown@example.com")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the user if the email exists" do
|
||||||
|
%{id: id} = user = user_fixture()
|
||||||
|
assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_user_by_email_and_password/2" do
|
||||||
|
test "does not return the user if the email does not exist" do
|
||||||
|
refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return the user if the password is not valid" do
|
||||||
|
user = user_fixture()
|
||||||
|
refute Accounts.get_user_by_email_and_password(user.email, "invalid")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the user if the email and password are valid" do
|
||||||
|
%{id: id} = user = user_fixture()
|
||||||
|
|
||||||
|
assert %User{id: ^id} =
|
||||||
|
Accounts.get_user_by_email_and_password(user.email, valid_user_password())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_user!/1" do
|
||||||
|
test "raises if id is invalid" do
|
||||||
|
assert_raise Ecto.NoResultsError, fn ->
|
||||||
|
Accounts.get_user!(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the user with the given id" do
|
||||||
|
%{id: id} = user = user_fixture()
|
||||||
|
assert %User{id: ^id} = Accounts.get_user!(user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "register_user/1" do
|
||||||
|
test "requires email and password to be set" do
|
||||||
|
{:error, changeset} = Accounts.register_user(%{})
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
password: ["can't be blank"],
|
||||||
|
email: ["can't be blank"]
|
||||||
|
} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates email and password when given" do
|
||||||
|
{:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not valid"})
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
email: ["must have the @ sign and no spaces"],
|
||||||
|
password: ["should be at least 12 character(s)"]
|
||||||
|
} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates maximum values for email and password for security" do
|
||||||
|
too_long = String.duplicate("db", 100)
|
||||||
|
{:error, changeset} = Accounts.register_user(%{email: too_long, password: too_long})
|
||||||
|
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
||||||
|
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates email uniqueness" do
|
||||||
|
%{email: email} = user_fixture()
|
||||||
|
{:error, changeset} = Accounts.register_user(%{email: email})
|
||||||
|
assert "has already been taken" in errors_on(changeset).email
|
||||||
|
|
||||||
|
# Now try with the upper cased email too, to check that email case is ignored.
|
||||||
|
{:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
|
||||||
|
assert "has already been taken" in errors_on(changeset).email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "registers users with a hashed password" do
|
||||||
|
email = unique_user_email()
|
||||||
|
{:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
|
||||||
|
assert user.email == email
|
||||||
|
assert is_binary(user.hashed_password)
|
||||||
|
assert is_nil(user.confirmed_at)
|
||||||
|
assert is_nil(user.password)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "change_user_registration/2" do
|
||||||
|
test "returns a changeset" do
|
||||||
|
assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{})
|
||||||
|
assert changeset.required == [:password, :email]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows fields to be set" do
|
||||||
|
email = unique_user_email()
|
||||||
|
password = valid_user_password()
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
Accounts.change_user_registration(
|
||||||
|
%User{},
|
||||||
|
valid_user_attributes(email: email, password: password)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert changeset.valid?
|
||||||
|
assert get_change(changeset, :email) == email
|
||||||
|
assert get_change(changeset, :password) == password
|
||||||
|
assert is_nil(get_change(changeset, :hashed_password))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "change_user_email/2" do
|
||||||
|
test "returns a user changeset" do
|
||||||
|
assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
|
||||||
|
assert changeset.required == [:email]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "apply_user_email/3" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "requires email to change", %{user: user} do
|
||||||
|
{:error, changeset} = Accounts.apply_user_email(user, valid_user_password(), %{})
|
||||||
|
assert %{email: ["did not change"]} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates email", %{user: user} do
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.apply_user_email(user, valid_user_password(), %{email: "not valid"})
|
||||||
|
|
||||||
|
assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates maximum value for email for security", %{user: user} do
|
||||||
|
too_long = String.duplicate("db", 100)
|
||||||
|
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.apply_user_email(user, valid_user_password(), %{email: too_long})
|
||||||
|
|
||||||
|
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates email uniqueness", %{user: user} do
|
||||||
|
%{email: email} = user_fixture()
|
||||||
|
password = valid_user_password()
|
||||||
|
|
||||||
|
{:error, changeset} = Accounts.apply_user_email(user, password, %{email: email})
|
||||||
|
|
||||||
|
assert "has already been taken" in errors_on(changeset).email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates current password", %{user: user} do
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()})
|
||||||
|
|
||||||
|
assert %{current_password: ["is not valid"]} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "applies the email without persisting it", %{user: user} do
|
||||||
|
email = unique_user_email()
|
||||||
|
{:ok, user} = Accounts.apply_user_email(user, valid_user_password(), %{email: email})
|
||||||
|
assert user.email == email
|
||||||
|
assert Accounts.get_user!(user.id).email != email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "deliver_user_update_email_instructions/3" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends token through notification", %{user: user} do
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, token} = Base.url_decode64(token, padding: false)
|
||||||
|
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
||||||
|
assert user_token.user_id == user.id
|
||||||
|
assert user_token.sent_to == user.email
|
||||||
|
assert user_token.context == "change:current@example.com"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_user_email/2" do
|
||||||
|
setup do
|
||||||
|
user = user_fixture()
|
||||||
|
email = unique_user_email()
|
||||||
|
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{user: user, token: token, email: email}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the email with a valid token", %{user: user, token: token, email: email} do
|
||||||
|
assert Accounts.update_user_email(user, token) == :ok
|
||||||
|
changed_user = Repo.get!(User, user.id)
|
||||||
|
assert changed_user.email != user.email
|
||||||
|
assert changed_user.email == email
|
||||||
|
assert changed_user.confirmed_at
|
||||||
|
assert changed_user.confirmed_at != user.confirmed_at
|
||||||
|
refute Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update email with invalid token", %{user: user} do
|
||||||
|
assert Accounts.update_user_email(user, "oops") == :error
|
||||||
|
assert Repo.get!(User, user.id).email == user.email
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update email if user email changed", %{user: user, token: token} do
|
||||||
|
assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) == :error
|
||||||
|
assert Repo.get!(User, user.id).email == user.email
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update email if token expired", %{user: user, token: token} do
|
||||||
|
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||||
|
assert Accounts.update_user_email(user, token) == :error
|
||||||
|
assert Repo.get!(User, user.id).email == user.email
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "change_user_password/2" do
|
||||||
|
test "returns a user changeset" do
|
||||||
|
assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
|
||||||
|
assert changeset.required == [:password]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows fields to be set" do
|
||||||
|
changeset =
|
||||||
|
Accounts.change_user_password(%User{}, %{
|
||||||
|
"password" => "new valid password"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert changeset.valid?
|
||||||
|
assert get_change(changeset, :password) == "new valid password"
|
||||||
|
assert is_nil(get_change(changeset, :hashed_password))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_user_password/3" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates password", %{user: user} do
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.update_user_password(user, valid_user_password(), %{
|
||||||
|
password: "not valid",
|
||||||
|
password_confirmation: "another"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
password: ["should be at least 12 character(s)"],
|
||||||
|
password_confirmation: ["does not match password"]
|
||||||
|
} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates maximum values for password for security", %{user: user} do
|
||||||
|
too_long = String.duplicate("db", 100)
|
||||||
|
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.update_user_password(user, valid_user_password(), %{password: too_long})
|
||||||
|
|
||||||
|
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates current password", %{user: user} do
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.update_user_password(user, "invalid", %{password: valid_user_password()})
|
||||||
|
|
||||||
|
assert %{current_password: ["is not valid"]} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the password", %{user: user} do
|
||||||
|
{:ok, user} =
|
||||||
|
Accounts.update_user_password(user, valid_user_password(), %{
|
||||||
|
password: "new valid password"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert is_nil(user.password)
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes all tokens for the given user", %{user: user} do
|
||||||
|
_ = Accounts.generate_user_session_token(user)
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
Accounts.update_user_password(user, valid_user_password(), %{
|
||||||
|
password: "new valid password"
|
||||||
|
})
|
||||||
|
|
||||||
|
refute Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "generate_user_session_token/1" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates a token", %{user: user} do
|
||||||
|
token = Accounts.generate_user_session_token(user)
|
||||||
|
assert user_token = Repo.get_by(UserToken, token: token)
|
||||||
|
assert user_token.context == "session"
|
||||||
|
|
||||||
|
# Creating the same token for another user should fail
|
||||||
|
assert_raise Ecto.ConstraintError, fn ->
|
||||||
|
Repo.insert!(%UserToken{
|
||||||
|
token: user_token.token,
|
||||||
|
user_id: user_fixture().id,
|
||||||
|
context: "session"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_user_by_session_token/1" do
|
||||||
|
setup do
|
||||||
|
user = user_fixture()
|
||||||
|
token = Accounts.generate_user_session_token(user)
|
||||||
|
%{user: user, token: token}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns user by token", %{user: user, token: token} do
|
||||||
|
assert session_user = Accounts.get_user_by_session_token(token)
|
||||||
|
assert session_user.id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return user for invalid token" do
|
||||||
|
refute Accounts.get_user_by_session_token("oops")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return user for expired token", %{token: token} do
|
||||||
|
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||||
|
refute Accounts.get_user_by_session_token(token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete_user_session_token/1" do
|
||||||
|
test "deletes the token" do
|
||||||
|
user = user_fixture()
|
||||||
|
token = Accounts.generate_user_session_token(user)
|
||||||
|
assert Accounts.delete_user_session_token(token) == :ok
|
||||||
|
refute Accounts.get_user_by_session_token(token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "deliver_user_confirmation_instructions/2" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends token through notification", %{user: user} do
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_confirmation_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, token} = Base.url_decode64(token, padding: false)
|
||||||
|
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
||||||
|
assert user_token.user_id == user.id
|
||||||
|
assert user_token.sent_to == user.email
|
||||||
|
assert user_token.context == "confirm"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "confirm_user/1" do
|
||||||
|
setup do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_confirmation_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{user: user, token: token}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "confirms the email with a valid token", %{user: user, token: token} do
|
||||||
|
assert {:ok, confirmed_user} = Accounts.confirm_user(token)
|
||||||
|
assert confirmed_user.confirmed_at
|
||||||
|
assert confirmed_user.confirmed_at != user.confirmed_at
|
||||||
|
assert Repo.get!(User, user.id).confirmed_at
|
||||||
|
refute Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not confirm with invalid token", %{user: user} do
|
||||||
|
assert Accounts.confirm_user("oops") == :error
|
||||||
|
refute Repo.get!(User, user.id).confirmed_at
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not confirm email if token expired", %{user: user, token: token} do
|
||||||
|
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||||
|
assert Accounts.confirm_user(token) == :error
|
||||||
|
refute Repo.get!(User, user.id).confirmed_at
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "deliver_user_reset_password_instructions/2" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends token through notification", %{user: user} do
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_reset_password_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, token} = Base.url_decode64(token, padding: false)
|
||||||
|
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
||||||
|
assert user_token.user_id == user.id
|
||||||
|
assert user_token.sent_to == user.email
|
||||||
|
assert user_token.context == "reset_password"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_user_by_reset_password_token/1" do
|
||||||
|
setup do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_reset_password_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{user: user, token: token}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the user with valid token", %{user: %{id: id}, token: token} do
|
||||||
|
assert %User{id: ^id} = Accounts.get_user_by_reset_password_token(token)
|
||||||
|
assert Repo.get_by(UserToken, user_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return the user with invalid token", %{user: user} do
|
||||||
|
refute Accounts.get_user_by_reset_password_token("oops")
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return the user if token expired", %{user: user, token: token} do
|
||||||
|
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||||
|
refute Accounts.get_user_by_reset_password_token(token)
|
||||||
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "reset_user_password/2" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates password", %{user: user} do
|
||||||
|
{:error, changeset} =
|
||||||
|
Accounts.reset_user_password(user, %{
|
||||||
|
password: "not valid",
|
||||||
|
password_confirmation: "another"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
password: ["should be at least 12 character(s)"],
|
||||||
|
password_confirmation: ["does not match password"]
|
||||||
|
} = errors_on(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates maximum values for password for security", %{user: user} do
|
||||||
|
too_long = String.duplicate("db", 100)
|
||||||
|
{:error, changeset} = Accounts.reset_user_password(user, %{password: too_long})
|
||||||
|
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the password", %{user: user} do
|
||||||
|
{:ok, updated_user} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
||||||
|
assert is_nil(updated_user.password)
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes all tokens for the given user", %{user: user} do
|
||||||
|
_ = Accounts.generate_user_session_token(user)
|
||||||
|
{:ok, _} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
||||||
|
refute Repo.get_by(UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "inspect/2 for the User module" do
|
||||||
|
test "does not include password" do
|
||||||
|
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule NotesAppWeb.ErrorHTMLTest do
|
||||||
|
use NotesAppWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
# Bring render_to_string/4 for testing custom views
|
||||||
|
import Phoenix.Template
|
||||||
|
|
||||||
|
test "renders 404.html" do
|
||||||
|
assert render_to_string(NotesAppWeb.ErrorHTML, "404", "html", []) == "Not Found"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders 500.html" do
|
||||||
|
assert render_to_string(NotesAppWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule NotesAppWeb.ErrorJSONTest do
|
||||||
|
use NotesAppWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
test "renders 404" do
|
||||||
|
assert NotesAppWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders 500" do
|
||||||
|
assert NotesAppWeb.ErrorJSON.render("500.json", %{}) ==
|
||||||
|
%{errors: %{detail: "Internal Server Error"}}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
defmodule NotesAppWeb.PageControllerTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
test "GET /", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/")
|
||||||
|
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,113 @@
|
||||||
|
defmodule NotesAppWeb.UserSessionControllerTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /users/log_in" do
|
||||||
|
test "logs the user in", %{conn: conn, user: user} do
|
||||||
|
conn =
|
||||||
|
post(conn, ~p"/users/log_in", %{
|
||||||
|
"user" => %{"email" => user.email, "password" => valid_user_password()}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert get_session(conn, :user_token)
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
|
||||||
|
# Now do a logged in request and assert on the menu
|
||||||
|
conn = get(conn, ~p"/")
|
||||||
|
response = html_response(conn, 200)
|
||||||
|
assert response =~ user.email
|
||||||
|
assert response =~ ~p"/users/settings"
|
||||||
|
assert response =~ ~p"/users/log_out"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs the user in with remember me", %{conn: conn, user: user} do
|
||||||
|
conn =
|
||||||
|
post(conn, ~p"/users/log_in", %{
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => valid_user_password(),
|
||||||
|
"remember_me" => "true"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert conn.resp_cookies["_notes_app_web_user_remember_me"]
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs the user in with return to", %{conn: conn, user: user} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> init_test_session(user_return_to: "/foo/bar")
|
||||||
|
|> post(~p"/users/log_in", %{
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => valid_user_password()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert redirected_to(conn) == "/foo/bar"
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "login following registration", %{conn: conn, user: user} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post(~p"/users/log_in", %{
|
||||||
|
"_action" => "registered",
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => valid_user_password()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Account created successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "login following password update", %{conn: conn, user: user} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> post(~p"/users/log_in", %{
|
||||||
|
"_action" => "password_updated",
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => valid_user_password()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert redirected_to(conn) == ~p"/users/settings"
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password updated successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to login page with invalid credentials", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
post(conn, ~p"/users/log_in", %{
|
||||||
|
"user" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
||||||
|
assert redirected_to(conn) == ~p"/users/log_in"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE /users/log_out" do
|
||||||
|
test "logs the user out", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> log_in_user(user) |> delete(~p"/users/log_out")
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "succeeds even if the user is not logged in", %{conn: conn} do
|
||||||
|
conn = delete(conn, ~p"/users/log_out")
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,67 @@
|
||||||
|
defmodule NotesAppWeb.UserConfirmationInstructionsLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
alias NotesApp.Repo
|
||||||
|
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Resend confirmation" do
|
||||||
|
test "renders the resend confirmation page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/confirm")
|
||||||
|
assert html =~ "Resend confirmation instructions"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends a new confirmation token", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#resend_confirmation_form", user: %{email: user.email})
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, ~p"/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||||
|
"If your email is in our system"
|
||||||
|
|
||||||
|
assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not send confirmation token if user is confirmed", %{conn: conn, user: user} do
|
||||||
|
Repo.update!(Accounts.User.confirm_changeset(user))
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#resend_confirmation_form", user: %{email: user.email})
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, ~p"/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||||
|
"If your email is in our system"
|
||||||
|
|
||||||
|
refute Repo.get_by(Accounts.UserToken, user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not send confirmation token if email is invalid", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#resend_confirmation_form", user: %{email: "unknown@example.com"})
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, ~p"/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||||
|
"If your email is in our system"
|
||||||
|
|
||||||
|
assert Repo.all(Accounts.UserToken) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
defmodule NotesAppWeb.UserConfirmationLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
alias NotesApp.Repo
|
||||||
|
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Confirm user" do
|
||||||
|
test "renders confirmation page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/confirm/some-token")
|
||||||
|
assert html =~ "Confirm Account"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "confirms the given token once", %{conn: conn, user: user} do
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_confirmation_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#confirmation_form")
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert {:ok, conn} = result
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||||
|
"User confirmed successfully"
|
||||||
|
|
||||||
|
assert Accounts.get_user!(user.id).confirmed_at
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
assert Repo.all(Accounts.UserToken) == []
|
||||||
|
|
||||||
|
# when not logged in
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#confirmation_form")
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert {:ok, conn} = result
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
||||||
|
"User confirmation link is invalid or it has expired"
|
||||||
|
|
||||||
|
# when logged in
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> log_in_user(user)
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#confirmation_form")
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert {:ok, conn} = result
|
||||||
|
refute Phoenix.Flash.get(conn.assigns.flash, :error)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not confirm email with invalid token", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/confirm/invalid-token")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#confirmation_form")
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, ~p"/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
||||||
|
"User confirmation link is invalid or it has expired"
|
||||||
|
|
||||||
|
refute Accounts.get_user!(user.id).confirmed_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,63 @@
|
||||||
|
defmodule NotesAppWeb.UserForgotPasswordLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
alias NotesApp.Repo
|
||||||
|
|
||||||
|
describe "Forgot password page" do
|
||||||
|
test "renders email page", %{conn: conn} do
|
||||||
|
{:ok, lv, html} = live(conn, ~p"/users/reset_password")
|
||||||
|
|
||||||
|
assert html =~ "Forgot your password?"
|
||||||
|
assert has_element?(lv, ~s|a[href="#{~p"/users/register"}"]|, "Register")
|
||||||
|
assert has_element?(lv, ~s|a[href="#{~p"/users/log_in"}"]|, "Log in")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if already logged in", %{conn: conn} do
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> log_in_user(user_fixture())
|
||||||
|
|> live(~p"/users/reset_password")
|
||||||
|
|> follow_redirect(conn, ~p"/")
|
||||||
|
|
||||||
|
assert {:ok, _conn} = result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Reset link" do
|
||||||
|
setup do
|
||||||
|
%{user: user_fixture()}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends a new reset password token", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#reset_password_form", user: %{"email" => user.email})
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
|
||||||
|
|
||||||
|
assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context ==
|
||||||
|
"reset_password"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not send reset password token if email is invalid", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#reset_password_form", user: %{"email" => "unknown@example.com"})
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
|
||||||
|
assert Repo.all(Accounts.UserToken) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
defmodule NotesAppWeb.UserLoginLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
describe "Log in page" do
|
||||||
|
test "renders log in page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
assert html =~ "Log in"
|
||||||
|
assert html =~ "Register"
|
||||||
|
assert html =~ "Forgot your password?"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if already logged in", %{conn: conn} do
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> log_in_user(user_fixture())
|
||||||
|
|> live(~p"/users/log_in")
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert {:ok, _conn} = result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "user login" do
|
||||||
|
test "redirects if user login with valid credentials", %{conn: conn} do
|
||||||
|
password = "123456789abcd"
|
||||||
|
user = user_fixture(%{password: password})
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
form =
|
||||||
|
form(lv, "#login_form", user: %{email: user.email, password: password, remember_me: true})
|
||||||
|
|
||||||
|
conn = submit_form(form, conn)
|
||||||
|
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to login page with a flash error if there are no valid credentials", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
form =
|
||||||
|
form(lv, "#login_form",
|
||||||
|
user: %{email: "test@email.com", password: "123456", remember_me: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = submit_form(form, conn)
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
||||||
|
|
||||||
|
assert redirected_to(conn) == "/users/log_in"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "login navigation" do
|
||||||
|
test "redirects to registration page when the Register button is clicked", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
{:ok, _login_live, login_html} =
|
||||||
|
lv
|
||||||
|
|> element(~s|main a:fl-contains("Sign up")|)
|
||||||
|
|> render_click()
|
||||||
|
|> follow_redirect(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
assert login_html =~ "Register"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to forgot password page when the Forgot Password button is clicked", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> element(~s|main a:fl-contains("Forgot your password?")|)
|
||||||
|
|> render_click()
|
||||||
|
|> follow_redirect(conn, ~p"/users/reset_password")
|
||||||
|
|
||||||
|
assert conn.resp_body =~ "Forgot your password?"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
defmodule NotesAppWeb.UserRegistrationLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
describe "Registration page" do
|
||||||
|
test "renders registration page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
assert html =~ "Register"
|
||||||
|
assert html =~ "Log in"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if already logged in", %{conn: conn} do
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> log_in_user(user_fixture())
|
||||||
|
|> live(~p"/users/register")
|
||||||
|
|> follow_redirect(conn, "/")
|
||||||
|
|
||||||
|
assert {:ok, _conn} = result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors for invalid data", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#registration_form")
|
||||||
|
|> render_change(user: %{"email" => "with spaces", "password" => "too short"})
|
||||||
|
|
||||||
|
assert result =~ "Register"
|
||||||
|
assert result =~ "must have the @ sign and no spaces"
|
||||||
|
assert result =~ "should be at least 12 character"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "register user" do
|
||||||
|
test "creates account and logs the user in", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
email = unique_user_email()
|
||||||
|
form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
|
||||||
|
render_submit(form)
|
||||||
|
conn = follow_trigger_action(form, conn)
|
||||||
|
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
|
||||||
|
# Now do a logged in request and assert on the menu
|
||||||
|
conn = get(conn, "/")
|
||||||
|
response = html_response(conn, 200)
|
||||||
|
assert response =~ email
|
||||||
|
assert response =~ "Settings"
|
||||||
|
assert response =~ "Log out"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors for duplicated email", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
user = user_fixture(%{email: "test@email.com"})
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#registration_form",
|
||||||
|
user: %{"email" => user.email, "password" => "valid_password"}
|
||||||
|
)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "has already been taken"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "registration navigation" do
|
||||||
|
test "redirects to login page when the Log in button is clicked", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
{:ok, _login_live, login_html} =
|
||||||
|
lv
|
||||||
|
|> element(~s|main a:fl-contains("Log in")|)
|
||||||
|
|> render_click()
|
||||||
|
|> follow_redirect(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
assert login_html =~ "Log in"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,118 @@
|
||||||
|
defmodule NotesAppWeb.UserResetPasswordLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
|
||||||
|
setup do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_reset_password_instructions(user, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{token: token, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Reset password page" do
|
||||||
|
test "renders reset password with valid token", %{conn: conn, token: token} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
assert html =~ "Reset Password"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not render reset password with invalid token", %{conn: conn} do
|
||||||
|
{:error, {:redirect, to}} = live(conn, ~p"/users/reset_password/invalid")
|
||||||
|
|
||||||
|
assert to == %{
|
||||||
|
flash: %{"error" => "Reset password link is invalid or it has expired."},
|
||||||
|
to: ~p"/"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors for invalid data", %{conn: conn, token: token} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#reset_password_form")
|
||||||
|
|> render_change(
|
||||||
|
user: %{"password" => "secret12", "password_confirmation" => "secret123456"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "should be at least 12 character"
|
||||||
|
assert result =~ "does not match password"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Reset Password" do
|
||||||
|
test "resets password once", %{conn: conn, token: token, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> form("#reset_password_form",
|
||||||
|
user: %{
|
||||||
|
"password" => "new valid password",
|
||||||
|
"password_confirmation" => "new valid password"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> render_submit()
|
||||||
|
|> follow_redirect(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not reset password on invalid data", %{conn: conn, token: token} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#reset_password_form",
|
||||||
|
user: %{
|
||||||
|
"password" => "too short",
|
||||||
|
"password_confirmation" => "does not match"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "Reset Password"
|
||||||
|
assert result =~ "should be at least 12 character(s)"
|
||||||
|
assert result =~ "does not match password"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Reset password navigation" do
|
||||||
|
test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> element(~s|main a:fl-contains("Log in")|)
|
||||||
|
|> render_click()
|
||||||
|
|> follow_redirect(conn, ~p"/users/log_in")
|
||||||
|
|
||||||
|
assert conn.resp_body =~ "Log in"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to registration page when the Register button is clicked", %{
|
||||||
|
conn: conn,
|
||||||
|
token: token
|
||||||
|
} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
||||||
|
|
||||||
|
{:ok, conn} =
|
||||||
|
lv
|
||||||
|
|> element(~s|main a:fl-contains("Register")|)
|
||||||
|
|> render_click()
|
||||||
|
|> follow_redirect(conn, ~p"/users/register")
|
||||||
|
|
||||||
|
assert conn.resp_body =~ "Register"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,210 @@
|
||||||
|
defmodule NotesAppWeb.UserSettingsLiveTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
describe "Settings page" do
|
||||||
|
test "renders settings page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} =
|
||||||
|
conn
|
||||||
|
|> log_in_user(user_fixture())
|
||||||
|
|> live(~p"/users/settings")
|
||||||
|
|
||||||
|
assert html =~ "Change Email"
|
||||||
|
assert html =~ "Change Password"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if user is not logged in", %{conn: conn} do
|
||||||
|
assert {:error, redirect} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/log_in"
|
||||||
|
assert %{"error" => "You must log in to access this page."} = flash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update email form" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
password = valid_user_password()
|
||||||
|
user = user_fixture(%{password: password})
|
||||||
|
%{conn: log_in_user(conn, user), user: user, password: password}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user email", %{conn: conn, password: password, user: user} do
|
||||||
|
new_email = unique_user_email()
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#email_form", %{
|
||||||
|
"current_password" => password,
|
||||||
|
"user" => %{"email" => new_email}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "A link to confirm your email"
|
||||||
|
assert Accounts.get_user_by_email(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#email_form")
|
||||||
|
|> render_change(%{
|
||||||
|
"action" => "update_email",
|
||||||
|
"current_password" => "invalid",
|
||||||
|
"user" => %{"email" => "with spaces"}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result =~ "Change Email"
|
||||||
|
assert result =~ "must have the @ sign and no spaces"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#email_form", %{
|
||||||
|
"current_password" => "invalid",
|
||||||
|
"user" => %{"email" => user.email}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "Change Email"
|
||||||
|
assert result =~ "did not change"
|
||||||
|
assert result =~ "is not valid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update password form" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
password = valid_user_password()
|
||||||
|
user = user_fixture(%{password: password})
|
||||||
|
%{conn: log_in_user(conn, user), user: user, password: password}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user password", %{conn: conn, user: user, password: password} do
|
||||||
|
new_password = valid_user_password()
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
form =
|
||||||
|
form(lv, "#password_form", %{
|
||||||
|
"current_password" => password,
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => new_password,
|
||||||
|
"password_confirmation" => new_password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
render_submit(form)
|
||||||
|
|
||||||
|
new_password_conn = follow_trigger_action(form, conn)
|
||||||
|
|
||||||
|
assert redirected_to(new_password_conn) == ~p"/users/settings"
|
||||||
|
|
||||||
|
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
||||||
|
"Password updated successfully"
|
||||||
|
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, new_password)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#password_form")
|
||||||
|
|> render_change(%{
|
||||||
|
"current_password" => "invalid",
|
||||||
|
"user" => %{
|
||||||
|
"password" => "too short",
|
||||||
|
"password_confirmation" => "does not match"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result =~ "Change Password"
|
||||||
|
assert result =~ "should be at least 12 character(s)"
|
||||||
|
assert result =~ "does not match password"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-submit)", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#password_form", %{
|
||||||
|
"current_password" => "invalid",
|
||||||
|
"user" => %{
|
||||||
|
"password" => "too short",
|
||||||
|
"password_confirmation" => "does not match"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "Change Password"
|
||||||
|
assert result =~ "should be at least 12 character(s)"
|
||||||
|
assert result =~ "does not match password"
|
||||||
|
assert result =~ "is not valid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "confirm email" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
email = unique_user_email()
|
||||||
|
|
||||||
|
token =
|
||||||
|
extract_user_token(fn url ->
|
||||||
|
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{conn: log_in_user(conn, user), token: token, email: email, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
||||||
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
||||||
|
|
||||||
|
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/settings"
|
||||||
|
assert %{"info" => message} = flash
|
||||||
|
assert message == "Email changed successfully."
|
||||||
|
refute Accounts.get_user_by_email(user.email)
|
||||||
|
assert Accounts.get_user_by_email(email)
|
||||||
|
|
||||||
|
# use confirm token again
|
||||||
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
||||||
|
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/settings"
|
||||||
|
assert %{"error" => message} = flash
|
||||||
|
assert message == "Email change link is invalid or it has expired."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not update email with invalid token", %{conn: conn, user: user} do
|
||||||
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/oops")
|
||||||
|
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/settings"
|
||||||
|
assert %{"error" => message} = flash
|
||||||
|
assert message == "Email change link is invalid or it has expired."
|
||||||
|
assert Accounts.get_user_by_email(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if user is not logged in", %{token: token} do
|
||||||
|
conn = build_conn()
|
||||||
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
||||||
|
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/log_in"
|
||||||
|
assert %{"error" => message} = flash
|
||||||
|
assert message == "You must log in to access this page."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
272
Elixir/notes_app/test/notes_app_web/user_auth_test.exs
Normal file
272
Elixir/notes_app/test/notes_app_web/user_auth_test.exs
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
defmodule NotesAppWeb.UserAuthTest do
|
||||||
|
use NotesAppWeb.ConnCase
|
||||||
|
|
||||||
|
alias Phoenix.LiveView
|
||||||
|
alias NotesApp.Accounts
|
||||||
|
alias NotesAppWeb.UserAuth
|
||||||
|
import NotesApp.AccountsFixtures
|
||||||
|
|
||||||
|
@remember_me_cookie "_notes_app_web_user_remember_me"
|
||||||
|
|
||||||
|
setup %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> Map.replace!(:secret_key_base, NotesAppWeb.Endpoint.config(:secret_key_base))
|
||||||
|
|> init_test_session(%{})
|
||||||
|
|
||||||
|
%{user: user_fixture(), conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "log_in_user/3" do
|
||||||
|
test "stores the user token in the session", %{conn: conn, user: user} do
|
||||||
|
conn = UserAuth.log_in_user(conn, user)
|
||||||
|
assert token = get_session(conn, :user_token)
|
||||||
|
assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
assert Accounts.get_user_by_session_token(token)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clears everything previously stored in the session", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
|
||||||
|
refute get_session(conn, :to_be_removed)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to the configured path", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
|
||||||
|
assert redirected_to(conn) == "/hello"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||||
|
assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
|
||||||
|
|
||||||
|
assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
|
||||||
|
assert signed_token != get_session(conn, :user_token)
|
||||||
|
assert max_age == 5_184_000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "logout_user/1" do
|
||||||
|
test "erases session and cookies", %{conn: conn, user: user} do
|
||||||
|
user_token = Accounts.generate_user_session_token(user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_session(:user_token, user_token)
|
||||||
|
|> put_req_cookie(@remember_me_cookie, user_token)
|
||||||
|
|> fetch_cookies()
|
||||||
|
|> UserAuth.log_out_user()
|
||||||
|
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
refute conn.cookies[@remember_me_cookie]
|
||||||
|
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
refute Accounts.get_user_by_session_token(user_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "broadcasts to the given live_socket_id", %{conn: conn} do
|
||||||
|
live_socket_id = "users_sessions:abcdef-token"
|
||||||
|
NotesAppWeb.Endpoint.subscribe(live_socket_id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_session(:live_socket_id, live_socket_id)
|
||||||
|
|> UserAuth.log_out_user()
|
||||||
|
|
||||||
|
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "works even if user is already logged out", %{conn: conn} do
|
||||||
|
conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch_current_user/2" do
|
||||||
|
test "authenticates user from session", %{conn: conn, user: user} do
|
||||||
|
user_token = Accounts.generate_user_session_token(user)
|
||||||
|
conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([])
|
||||||
|
assert conn.assigns.current_user.id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "authenticates user from cookies", %{conn: conn, user: user} do
|
||||||
|
logged_in_conn =
|
||||||
|
conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||||
|
|
||||||
|
user_token = logged_in_conn.cookies[@remember_me_cookie]
|
||||||
|
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_cookie(@remember_me_cookie, signed_token)
|
||||||
|
|> UserAuth.fetch_current_user([])
|
||||||
|
|
||||||
|
assert conn.assigns.current_user.id == user.id
|
||||||
|
assert get_session(conn, :user_token) == user_token
|
||||||
|
|
||||||
|
assert get_session(conn, :live_socket_id) ==
|
||||||
|
"users_sessions:#{Base.url_encode64(user_token)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not authenticate if data is missing", %{conn: conn, user: user} do
|
||||||
|
_ = Accounts.generate_user_session_token(user)
|
||||||
|
conn = UserAuth.fetch_current_user(conn, [])
|
||||||
|
refute get_session(conn, :user_token)
|
||||||
|
refute conn.assigns.current_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "on_mount :mount_current_user" do
|
||||||
|
test "assigns current_user based on a valid user_token", %{conn: conn, user: user} do
|
||||||
|
user_token = Accounts.generate_user_session_token(user)
|
||||||
|
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||||
|
|
||||||
|
{:cont, updated_socket} =
|
||||||
|
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
|
||||||
|
|
||||||
|
assert updated_socket.assigns.current_user.id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "assigns nil to current_user assign if there isn't a valid user_token", %{conn: conn} do
|
||||||
|
user_token = "invalid_token"
|
||||||
|
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||||
|
|
||||||
|
{:cont, updated_socket} =
|
||||||
|
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
|
||||||
|
|
||||||
|
assert updated_socket.assigns.current_user == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "assigns nil to current_user assign if there isn't a user_token", %{conn: conn} do
|
||||||
|
session = conn |> get_session()
|
||||||
|
|
||||||
|
{:cont, updated_socket} =
|
||||||
|
UserAuth.on_mount(:mount_current_user, %{}, session, %LiveView.Socket{})
|
||||||
|
|
||||||
|
assert updated_socket.assigns.current_user == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "on_mount :ensure_authenticated" do
|
||||||
|
test "authenticates current_user based on a valid user_token", %{conn: conn, user: user} do
|
||||||
|
user_token = Accounts.generate_user_session_token(user)
|
||||||
|
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||||
|
|
||||||
|
{:cont, updated_socket} =
|
||||||
|
UserAuth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})
|
||||||
|
|
||||||
|
assert updated_socket.assigns.current_user.id == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
|
||||||
|
user_token = "invalid_token"
|
||||||
|
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||||
|
|
||||||
|
socket = %LiveView.Socket{
|
||||||
|
endpoint: NotesAppWeb.Endpoint,
|
||||||
|
assigns: %{__changed__: %{}, flash: %{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
|
||||||
|
assert updated_socket.assigns.current_user == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to login page if there isn't a user_token", %{conn: conn} do
|
||||||
|
session = conn |> get_session()
|
||||||
|
|
||||||
|
socket = %LiveView.Socket{
|
||||||
|
endpoint: NotesAppWeb.Endpoint,
|
||||||
|
assigns: %{__changed__: %{}, flash: %{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:halt, updated_socket} = UserAuth.on_mount(:ensure_authenticated, %{}, session, socket)
|
||||||
|
assert updated_socket.assigns.current_user == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "on_mount :redirect_if_user_is_authenticated" do
|
||||||
|
test "redirects if there is an authenticated user ", %{conn: conn, user: user} do
|
||||||
|
user_token = Accounts.generate_user_session_token(user)
|
||||||
|
session = conn |> put_session(:user_token, user_token) |> get_session()
|
||||||
|
|
||||||
|
assert {:halt, _updated_socket} =
|
||||||
|
UserAuth.on_mount(
|
||||||
|
:redirect_if_user_is_authenticated,
|
||||||
|
%{},
|
||||||
|
session,
|
||||||
|
%LiveView.Socket{}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't redirect if there is no authenticated user", %{conn: conn} do
|
||||||
|
session = conn |> get_session()
|
||||||
|
|
||||||
|
assert {:cont, _updated_socket} =
|
||||||
|
UserAuth.on_mount(
|
||||||
|
:redirect_if_user_is_authenticated,
|
||||||
|
%{},
|
||||||
|
session,
|
||||||
|
%LiveView.Socket{}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "redirect_if_user_is_authenticated/2" do
|
||||||
|
test "redirects if user is authenticated", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([])
|
||||||
|
assert conn.halted
|
||||||
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not redirect if user is not authenticated", %{conn: conn} do
|
||||||
|
conn = UserAuth.redirect_if_user_is_authenticated(conn, [])
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "require_authenticated_user/2" do
|
||||||
|
test "redirects if user is not authenticated", %{conn: conn} do
|
||||||
|
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
|
||||||
|
assert conn.halted
|
||||||
|
|
||||||
|
assert redirected_to(conn) == ~p"/users/log_in"
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||||
|
"You must log in to access this page."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stores the path to redirect to on GET", %{conn: conn} do
|
||||||
|
halted_conn =
|
||||||
|
%{conn | path_info: ["foo"], query_string: ""}
|
||||||
|
|> fetch_flash()
|
||||||
|
|> UserAuth.require_authenticated_user([])
|
||||||
|
|
||||||
|
assert halted_conn.halted
|
||||||
|
assert get_session(halted_conn, :user_return_to) == "/foo"
|
||||||
|
|
||||||
|
halted_conn =
|
||||||
|
%{conn | path_info: ["foo"], query_string: "bar=baz"}
|
||||||
|
|> fetch_flash()
|
||||||
|
|> UserAuth.require_authenticated_user([])
|
||||||
|
|
||||||
|
assert halted_conn.halted
|
||||||
|
assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
|
||||||
|
|
||||||
|
halted_conn =
|
||||||
|
%{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
|
||||||
|
|> fetch_flash()
|
||||||
|
|> UserAuth.require_authenticated_user([])
|
||||||
|
|
||||||
|
assert halted_conn.halted
|
||||||
|
refute get_session(halted_conn, :user_return_to)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not redirect if user is authenticated", %{conn: conn, user: user} do
|
||||||
|
conn = conn |> assign(:current_user, user) |> UserAuth.require_authenticated_user([])
|
||||||
|
refute conn.halted
|
||||||
|
refute conn.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
64
Elixir/notes_app/test/support/conn_case.ex
Normal file
64
Elixir/notes_app/test/support/conn_case.ex
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule NotesAppWeb.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 NotesAppWeb.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 NotesAppWeb.Endpoint
|
||||||
|
|
||||||
|
use NotesAppWeb, :verified_routes
|
||||||
|
|
||||||
|
# Import conveniences for testing with connections
|
||||||
|
import Plug.Conn
|
||||||
|
import Phoenix.ConnTest
|
||||||
|
import NotesAppWeb.ConnCase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup tags do
|
||||||
|
NotesApp.DataCase.setup_sandbox(tags)
|
||||||
|
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Setup helper that registers and logs in users.
|
||||||
|
|
||||||
|
setup :register_and_log_in_user
|
||||||
|
|
||||||
|
It stores an updated connection and a registered user in the
|
||||||
|
test context.
|
||||||
|
"""
|
||||||
|
def register_and_log_in_user(%{conn: conn}) do
|
||||||
|
user = NotesApp.AccountsFixtures.user_fixture()
|
||||||
|
%{conn: log_in_user(conn, user), user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Logs the given `user` into the `conn`.
|
||||||
|
|
||||||
|
It returns an updated `conn`.
|
||||||
|
"""
|
||||||
|
def log_in_user(conn, user) do
|
||||||
|
token = NotesApp.Accounts.generate_user_session_token(user)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> Phoenix.ConnTest.init_test_session(%{})
|
||||||
|
|> Plug.Conn.put_session(:user_token, token)
|
||||||
|
end
|
||||||
|
end
|
58
Elixir/notes_app/test/support/data_case.ex
Normal file
58
Elixir/notes_app/test/support/data_case.ex
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
defmodule NotesApp.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 NotesApp.DataCase, async: true`, although
|
||||||
|
this option is not recommended for other databases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use ExUnit.CaseTemplate
|
||||||
|
|
||||||
|
using do
|
||||||
|
quote do
|
||||||
|
alias NotesApp.Repo
|
||||||
|
|
||||||
|
import Ecto
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query
|
||||||
|
import NotesApp.DataCase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup tags do
|
||||||
|
NotesApp.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!(NotesApp.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
|
31
Elixir/notes_app/test/support/fixtures/accounts_fixtures.ex
Normal file
31
Elixir/notes_app/test/support/fixtures/accounts_fixtures.ex
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule NotesApp.AccountsFixtures do
|
||||||
|
@moduledoc """
|
||||||
|
This module defines test helpers for creating
|
||||||
|
entities via the `NotesApp.Accounts` context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||||
|
def valid_user_password, do: "hello world!"
|
||||||
|
|
||||||
|
def valid_user_attributes(attrs \\ %{}) do
|
||||||
|
Enum.into(attrs, %{
|
||||||
|
email: unique_user_email(),
|
||||||
|
password: valid_user_password()
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_fixture(attrs \\ %{}) do
|
||||||
|
{:ok, user} =
|
||||||
|
attrs
|
||||||
|
|> valid_user_attributes()
|
||||||
|
|> NotesApp.Accounts.register_user()
|
||||||
|
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_user_token(fun) do
|
||||||
|
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
||||||
|
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
|
||||||
|
token
|
||||||
|
end
|
||||||
|
end
|
2
Elixir/notes_app/test/test_helper.exs
Normal file
2
Elixir/notes_app/test/test_helper.exs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(NotesApp.Repo, :manual)
|
Loading…
Reference in a new issue