add links

This commit is contained in:
zack 2024-10-21 13:57:31 -04:00
parent 3842798968
commit e2b802f968
No known key found for this signature in database
GPG key ID: 5F873416BCF59F35
54 changed files with 4984 additions and 6 deletions

View file

@ -20,7 +20,7 @@ config :zoeyscomputer, ZoeyscomputerWeb.Endpoint,
layout: false layout: false
], ],
pubsub_server: Zoeyscomputer.PubSub, pubsub_server: Zoeyscomputer.PubSub,
live_view: [signing_salt: "byg+88ZA"] live_view: [signing_salt: "uUcDyRmg"]
# Configures the mailer # Configures the mailer
# #

View file

@ -23,7 +23,7 @@ config :zoeyscomputer, ZoeyscomputerWeb.Endpoint,
check_origin: false, check_origin: false,
code_reloader: true, code_reloader: true,
debug_errors: true, debug_errors: true,
secret_key_base: "Qe0dIfVihLtZD6q2TbcUJvJZEo4i5SAAAmKfLXfjj8AxZfXgL4+g2ISn9CaOq3ES", secret_key_base: "qP4Ia7HM4vZ854nrMdK3FZvV57X4tDxHM1btfYuMzc2U1QpAAoVGTM+bWh5/ob/I",
watchers: [ watchers: [
esbuild: {Esbuild, :install_and_run, [:zoeyscomputer, ~w(--sourcemap=inline --watch)]}, esbuild: {Esbuild, :install_and_run, [:zoeyscomputer, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:zoeyscomputer, ~w(--watch)]} tailwind: {Tailwind, :install_and_run, [:zoeyscomputer, ~w(--watch)]}

View file

@ -1,5 +1,8 @@
import Config import Config
# Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1
# Configure your database # Configure your database
# #
# The MIX_TEST_PARTITION environment variable can be used # The MIX_TEST_PARTITION environment variable can be used
@ -17,7 +20,7 @@ config :zoeyscomputer, Zoeyscomputer.Repo,
# you can enable the server option below. # you can enable the server option below.
config :zoeyscomputer, ZoeyscomputerWeb.Endpoint, config :zoeyscomputer, ZoeyscomputerWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002], http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "bF1Py67SqufVZ+89ftwi6qMCvOpv01k6Gwm2xKCwOCuGUdDsJ+rYsRtRbS1efk1r", secret_key_base: "EvnzV8Funkmd+/QjtPf1M4nnmdBB+8k1CXRrw3Q+7WDWTudeudvKb4VcUb/lM5K3",
server: false server: false
# In test we don't send emails # In test we don't send emails

727
deps.nix Normal file
View file

@ -0,0 +1,727 @@
{ pkgs, lib, beamPackages, overrides ? (x: y: { }) }:
let
buildMix = lib.makeOverridable beamPackages.buildMix;
buildRebar3 = lib.makeOverridable beamPackages.buildRebar3;
defaultOverrides = (final: prev:
let
apps = {
explorer = [
{
name = "rustlerPrecompiled";
toolchain = {
name = "nightly-2024-07-26";
sha256 = "sha256-5icy5hSaQy6/fUim9L2vz2GeZNC3fX1N5T2MjnkTplc=";
};
}
];
tokenizers = [
{
name = "rustlerPrecompiled";
}
];
};
elixirConfig = pkgs.writeTextDir
"config/config.exs"
''
import Config
config :explorer, Explorer.PolarsBackend.Native,
skip_compilation?: true
config :tokenizers, Tokenizers.Native,
skip_compilation?: true
'';
buildNativeDir = src: "${src}/native/${with builtins; head (attrNames (readDir "${src}/native"))}";
workarounds = {
rustlerPrecompiled = { toolchain ? null, ... }: old:
let
extendedPkgs = pkgs.extend fenixOverlay;
fenixOverlay = import
"${fetchTarball {
url = "https://github.com/nix-community/fenix/archive/43efa7a3a97f290441bd75b18defcd4f7b8df220.tar.gz";
sha256 = "sha256:1b9v45cafixpbj6iqjw3wr0yfpcrh3p11am7v0cjpjq5n8bhs8v3";
}}/overlay.nix";
nativeDir = buildNativeDir old.src;
fenix =
if toolchain == null
then extendedPkgs.fenix.stable
else extendedPkgs.fenix.fromToolchainName toolchain;
native = (extendedPkgs.makeRustPlatform {
inherit (fenix) cargo rustc;
}).buildRustPackage {
pname = "${old.packageName}-native";
version = old.version;
src = nativeDir;
cargoLock = {
lockFile = "${nativeDir}/Cargo.lock";
};
nativeBuildInputs = [ extendedPkgs.cmake ] ++ extendedPkgs.lib.lists.optional extendedPkgs.stdenv.isDarwin extendedPkgs.darwin.IOKit;
doCheck = false;
};
in
{
nativeBuildInputs = [ extendedPkgs.cargo ];
appConfigPath = "${elixirConfig}/config";
env.RUSTLER_PRECOMPILED_FORCE_BUILD_ALL = "true";
env.RUSTLER_PRECOMPILED_GLOBAL_CACHE_PATH = "unused-but-required";
preConfigure = ''
mkdir -p priv/native
for lib in ${native}/lib/*
do
ln -s "$lib" "priv/native/$(basename "$lib")"
done
'';
};
};
applyOverrides = appName: drv:
let
allOverridesForApp = builtins.foldl'
(acc: workaround: acc // (workarounds.${workaround.name} workaround) drv)
{ }
apps.${appName};
in
if builtins.hasAttr appName apps
then
drv.override allOverridesForApp
else
drv;
in
builtins.mapAttrs
applyOverrides
prev);
self = packages // (defaultOverrides self packages) // (overrides self packages);
packages = with beamPackages; with self; {
bandit =
let
version = "1.5.7";
in
buildMix {
inherit version;
name = "bandit";
src = fetchHex {
inherit version;
pkg = "bandit";
sha256 = "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd";
};
beamDeps = [ hpax plug telemetry thousand_island websock ];
};
bcrypt_elixir =
let
version = "3.2.0";
in
buildMix {
inherit version;
name = "bcrypt_elixir";
src = fetchHex {
inherit version;
pkg = "bcrypt_elixir";
sha256 = "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8";
};
beamDeps = [ comeonin elixir_make ];
};
castore =
let
version = "1.0.9";
in
buildMix {
inherit version;
name = "castore";
src = fetchHex {
inherit version;
pkg = "castore";
sha256 = "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7";
};
};
comeonin =
let
version = "5.5.0";
in
buildMix {
inherit version;
name = "comeonin";
src = fetchHex {
inherit version;
pkg = "comeonin";
sha256 = "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb";
};
};
db_connection =
let
version = "2.7.0";
in
buildMix {
inherit version;
name = "db_connection";
src = fetchHex {
inherit version;
pkg = "db_connection";
sha256 = "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff";
};
beamDeps = [ telemetry ];
};
decimal =
let
version = "2.1.1";
in
buildMix {
inherit version;
name = "decimal";
src = fetchHex {
inherit version;
pkg = "decimal";
sha256 = "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc";
};
};
dns_cluster =
let
version = "0.1.3";
in
buildMix {
inherit version;
name = "dns_cluster";
src = fetchHex {
inherit version;
pkg = "dns_cluster";
sha256 = "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33";
};
};
ecto =
let
version = "3.12.4";
in
buildMix {
inherit version;
name = "ecto";
src = fetchHex {
inherit version;
pkg = "ecto";
sha256 = "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747";
};
beamDeps = [ decimal jason telemetry ];
};
ecto_sql =
let
version = "3.12.1";
in
buildMix {
inherit version;
name = "ecto_sql";
src = fetchHex {
inherit version;
pkg = "ecto_sql";
sha256 = "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5";
};
beamDeps = [ db_connection ecto postgrex telemetry ];
};
elixir_make =
let
version = "0.8.4";
in
buildMix {
inherit version;
name = "elixir_make";
src = fetchHex {
inherit version;
pkg = "elixir_make";
sha256 = "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040";
};
beamDeps = [ castore ];
};
esbuild =
let
version = "0.8.2";
in
buildMix {
inherit version;
name = "esbuild";
src = fetchHex {
inherit version;
pkg = "esbuild";
sha256 = "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7";
};
beamDeps = [ castore jason ];
};
expo =
let
version = "1.1.0";
in
buildMix {
inherit version;
name = "expo";
src = fetchHex {
inherit version;
pkg = "expo";
sha256 = "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960";
};
};
finch =
let
version = "0.19.0";
in
buildMix {
inherit version;
name = "finch";
src = fetchHex {
inherit version;
pkg = "finch";
sha256 = "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6";
};
beamDeps = [ mime mint nimble_options nimble_pool telemetry ];
};
gettext =
let
version = "0.26.1";
in
buildMix {
inherit version;
name = "gettext";
src = fetchHex {
inherit version;
pkg = "gettext";
sha256 = "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734";
};
beamDeps = [ expo ];
};
hpax =
let
version = "1.0.0";
in
buildMix {
inherit version;
name = "hpax";
src = fetchHex {
inherit version;
pkg = "hpax";
sha256 = "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601";
};
};
jason =
let
version = "1.4.4";
in
buildMix {
inherit version;
name = "jason";
src = fetchHex {
inherit version;
pkg = "jason";
sha256 = "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b";
};
beamDeps = [ decimal ];
};
mime =
let
version = "2.0.6";
in
buildMix {
inherit version;
name = "mime";
src = fetchHex {
inherit version;
pkg = "mime";
sha256 = "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e";
};
};
mint =
let
version = "1.6.2";
in
buildMix {
inherit version;
name = "mint";
src = fetchHex {
inherit version;
pkg = "mint";
sha256 = "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9";
};
beamDeps = [ castore hpax ];
};
nimble_options =
let
version = "1.1.1";
in
buildMix {
inherit version;
name = "nimble_options";
src = fetchHex {
inherit version;
pkg = "nimble_options";
sha256 = "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44";
};
};
nimble_pool =
let
version = "1.1.0";
in
buildMix {
inherit version;
name = "nimble_pool";
src = fetchHex {
inherit version;
pkg = "nimble_pool";
sha256 = "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a";
};
};
phoenix =
let
version = "1.7.14";
in
buildMix {
inherit version;
name = "phoenix";
src = fetchHex {
inherit version;
pkg = "phoenix";
sha256 = "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a";
};
beamDeps = [ castore jason phoenix_pubsub phoenix_template plug plug_crypto telemetry websock_adapter ];
};
phoenix_ecto =
let
version = "4.6.2";
in
buildMix {
inherit version;
name = "phoenix_ecto";
src = fetchHex {
inherit version;
pkg = "phoenix_ecto";
sha256 = "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59";
};
beamDeps = [ ecto phoenix_html plug postgrex ];
};
phoenix_html =
let
version = "4.1.1";
in
buildMix {
inherit version;
name = "phoenix_html";
src = fetchHex {
inherit version;
pkg = "phoenix_html";
sha256 = "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f";
};
};
phoenix_live_dashboard =
let
version = "0.8.4";
in
buildMix {
inherit version;
name = "phoenix_live_dashboard";
src = fetchHex {
inherit version;
pkg = "phoenix_live_dashboard";
sha256 = "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59";
};
beamDeps = [ ecto mime phoenix_live_view telemetry_metrics ];
};
phoenix_live_view =
let
version = "1.0.0-rc.7";
in
buildMix {
inherit version;
name = "phoenix_live_view";
src = fetchHex {
inherit version;
pkg = "phoenix_live_view";
sha256 = "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01";
};
beamDeps = [ jason phoenix phoenix_html phoenix_template plug telemetry ];
};
phoenix_pubsub =
let
version = "2.1.3";
in
buildMix {
inherit version;
name = "phoenix_pubsub";
src = fetchHex {
inherit version;
pkg = "phoenix_pubsub";
sha256 = "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502";
};
};
phoenix_template =
let
version = "1.0.4";
in
buildMix {
inherit version;
name = "phoenix_template";
src = fetchHex {
inherit version;
pkg = "phoenix_template";
sha256 = "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206";
};
beamDeps = [ phoenix_html ];
};
plug =
let
version = "1.16.1";
in
buildMix {
inherit version;
name = "plug";
src = fetchHex {
inherit version;
pkg = "plug";
sha256 = "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc";
};
beamDeps = [ mime plug_crypto telemetry ];
};
plug_crypto =
let
version = "2.1.0";
in
buildMix {
inherit version;
name = "plug_crypto";
src = fetchHex {
inherit version;
pkg = "plug_crypto";
sha256 = "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa";
};
};
postgrex =
let
version = "0.19.1";
in
buildMix {
inherit version;
name = "postgrex";
src = fetchHex {
inherit version;
pkg = "postgrex";
sha256 = "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8";
};
beamDeps = [ db_connection decimal jason ];
};
swoosh =
let
version = "1.17.2";
in
buildMix {
inherit version;
name = "swoosh";
src = fetchHex {
inherit version;
pkg = "swoosh";
sha256 = "de914359f0ddc134dc0d7735e28922d49d0503f31e4bd66b44e26039c2226d39";
};
beamDeps = [ bandit finch jason mime plug telemetry ];
};
tailwind =
let
version = "0.2.4";
in
buildMix {
inherit version;
name = "tailwind";
src = fetchHex {
inherit version;
pkg = "tailwind";
sha256 = "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463";
};
beamDeps = [ castore ];
};
telemetry =
let
version = "1.3.0";
in
buildRebar3 {
inherit version;
name = "telemetry";
src = fetchHex {
inherit version;
pkg = "telemetry";
sha256 = "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6";
};
};
telemetry_metrics =
let
version = "1.0.0";
in
buildMix {
inherit version;
name = "telemetry_metrics";
src = fetchHex {
inherit version;
pkg = "telemetry_metrics";
sha256 = "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d";
};
beamDeps = [ telemetry ];
};
telemetry_poller =
let
version = "1.1.0";
in
buildRebar3 {
inherit version;
name = "telemetry_poller";
src = fetchHex {
inherit version;
pkg = "telemetry_poller";
sha256 = "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f";
};
beamDeps = [ telemetry ];
};
thousand_island =
let
version = "1.3.5";
in
buildMix {
inherit version;
name = "thousand_island";
src = fetchHex {
inherit version;
pkg = "thousand_island";
sha256 = "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180";
};
beamDeps = [ telemetry ];
};
websock =
let
version = "0.5.3";
in
buildMix {
inherit version;
name = "websock";
src = fetchHex {
inherit version;
pkg = "websock";
sha256 = "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453";
};
};
websock_adapter =
let
version = "0.5.7";
in
buildMix {
inherit version;
name = "websock_adapter";
src = fetchHex {
inherit version;
pkg = "websock_adapter";
sha256 = "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1";
};
beamDeps = [ bandit plug websock ];
};
};
in
self

166
flake.lock generated Normal file
View file

@ -0,0 +1,166 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1727826117,
"narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1719994518,
"narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"lexical": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs",
"systems": "systems"
},
"locked": {
"lastModified": 1727126932,
"narHash": "sha256-JvLiDxwkKD1u/DmTVUpmymQoCfYdoTHAOSs9fYCLfcw=",
"owner": "lexical-lsp",
"repo": "lexical",
"rev": "48fb3fa4f25fc490f297758761c5593eb45a2026",
"type": "github"
},
"original": {
"owner": "lexical-lsp",
"repo": "lexical",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1719931832,
"narHash": "sha256-0LD+KePCKKEb4CcPsTBOwf019wDtZJanjoKm1S8q3Do=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0aeab749216e4c073cece5d34bc01b79e717c3e0",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1727825735,
"narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
}
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1719876945,
"narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 0,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"path": "/nix/store/riqkpszjqk02bi1wppfg8ip5xvh102qd-source",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"process-compose-flake": {
"locked": {
"lastModified": 1729388329,
"narHash": "sha256-ziJLH/BuBlcfSdFZH+FbITNzwkIZZwdVzuAYUKoSoWg=",
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"rev": "00f00687efba844f356aca8bfb44105f93756c26",
"type": "github"
},
"original": {
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"lexical": "lexical",
"nixpkgs": "nixpkgs_2",
"process-compose-flake": "process-compose-flake",
"systems": "systems_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

124
flake.nix Normal file
View file

@ -0,0 +1,124 @@
{
inputs = {
# nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
flake-parts.url = "github:hercules-ci/flake-parts";
# Lexical is an alternative LSP server. There are several options for Elixir
# LSP. See https://gist.github.com/Nezteb/dc63f1d5ad9d88907dd103da2ca000b1
lexical.url = "github:lexical-lsp/lexical";
# Use process-compose to manage background processes during development
process-compose-flake.url = "github:Platonic-Systems/process-compose-flake";
};
outputs = {
self,
nixpkgs,
systems,
flake-parts,
...
} @ inputs: let
# Set the Erlang version
erlangVersion = "erlang_27";
# Set the Elixir version
elixirVersion = "elixir_1_17";
in
flake-parts.lib.mkFlake {inherit inputs;} {
systems = import systems;
imports = [inputs.process-compose-flake.flakeModule];
perSystem = {
# self',
config,
system,
pkgs,
lib,
...
}: let
heroicons = pkgs.fetchFromGitHub {
owner = "tailwindlabs";
repo = "heroicons";
rev = "v2.1.1";
hash = "sha256-y/kY8HPJmzB2e7ErgkUdQijU7oUhfS3fI093Rsvyvqs=";
sparseCheckout = ["optimized"];
};
in {
# Define a consistent package set for development, testing, and
# production.
_module.args.pkgs = import nixpkgs {
inherit system;
overlays = [
(
final: _: let
erlang = final.beam.interpreters.${erlangVersion};
beamPackages = final.beam.packages.${erlangVersion};
elixir = beamPackages.${elixirVersion};
in {
inherit erlang elixir;
# Hex is not used in the devShell.
# inherit (beamPackages) hex;
}
)
];
};
# You can build your Elixir application using mixRelease.
packages.default = pkgs.beamPackages.mixRelease {
pname = "zoeys-computer";
version = "0.1.0";
src = ./.;
removeCookie = false;
mixNixDeps = with pkgs;
import ./deps.nix {
inherit pkgs lib beamPackages;
};
buildInputs = with pkgs; [nodejs];
MIX_ENV = "prod";
};
# Add dependencies to develop your application using Mix.
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; (
[
erlang
elixir
# You are likely to need Node.js if you develop a Phoenix
# application.
nodejs
mix2nix
# Add the language server of your choice.
inputs.lexical.packages.${system}.default
# I once added Hex via a Nix development shell, but now I install
# and upgrade it using Mix. Hex installed using Nix can cause an
# issue if you manage Elixir dependencies using Mix.
]
# Add a dependency for a file watcher if you develop a Phoenix
# application.
++ lib.optional stdenv.isLinux inotify-tools
++ (lib.optionals stdenv.isDarwin (
with darwin.apple_sdk.frameworks; [
CoreFoundation
CoreServices
]
))
);
};
# You can define background processes in Nix using
# process-compose-flake.
process-compose.example = {
settings = {
processes = {
ponysay.command = ''
while true; do
${lib.getExe pkgs.ponysay} "Enjoy our ponysay demo!"
sleep 2
done
'';
};
};
};
};
};
}

107
lib/zoeyscomputer/links.ex Normal file
View file

@ -0,0 +1,107 @@
defmodule Zoeyscomputer.Links do
@moduledoc """
The Links context.
"""
import Ecto.Query, warn: false
alias Zoeyscomputer.Repo
alias Zoeyscomputer.Links.Link
@doc """
Returns the list of links.
## Examples
iex> list_links()
[%Link{}, ...]
"""
def list_links(user_id) do
Repo.all(
from l in Link,
where: l.user_id == ^user_id
)
end
@doc """
Gets a single link.
Raises `Ecto.NoResultsError` if the Link does not exist.
## Examples
iex> get_link!(123)
%Link{}
iex> get_link!(456)
** (Ecto.NoResultsError)
"""
def get_link!(id), do: Repo.get!(Link, id)
@doc """
Creates a link.
## Examples
iex> create_link(%{field: value})
{:ok, %Link{}}
iex> create_link(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_link(attrs \\ %{}) do
%Link{}
|> Link.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a link.
## Examples
iex> update_link(link, %{field: new_value})
{:ok, %Link{}}
iex> update_link(link, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_link(%Link{} = link, attrs) do
link
|> Link.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a link.
## Examples
iex> delete_link(link)
{:ok, %Link{}}
iex> delete_link(link)
{:error, %Ecto.Changeset{}}
"""
def delete_link(%Link{} = link) do
Repo.delete(link)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking link changes.
## Examples
iex> change_link(link)
%Ecto.Changeset{data: %Link{}}
"""
def change_link(%Link{} = link, attrs \\ %{}) do
Link.changeset(link, attrs)
end
end

View file

@ -0,0 +1,19 @@
defmodule Zoeyscomputer.Links.Link do
use Ecto.Schema
import Ecto.Changeset
schema "links" do
field :url, :string
belongs_to :user, Zoeyscomputer.Users.User
timestamps(type: :utc_datetime)
end
@doc false
def changeset(link, attrs \\ %{}) do
link
|> cast(attrs, [:url, :user_id])
|> validate_required([:url])
end
end

View file

@ -0,0 +1,28 @@
defmodule Zoeyscomputer.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :zoeyscomputer
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end

353
lib/zoeyscomputer/users.ex Normal file
View file

@ -0,0 +1,353 @@
defmodule Zoeyscomputer.Users do
@moduledoc """
The Users context.
"""
import Ecto.Query, warn: false
alias Zoeyscomputer.Repo
alias Zoeyscomputer.Users.{User, UserToken, UserNotifier}
## Database getters
@doc """
Gets a user by email.
## Examples
iex> get_user_by_email("foo@example.com")
%User{}
iex> get_user_by_email("unknown@example.com")
nil
"""
def get_user_by_email(email) when is_binary(email) do
Repo.get_by(User, email: email)
end
@doc """
Gets a user by email and password.
## Examples
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
%User{}
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
nil
"""
def get_user_by_email_and_password(email, password)
when is_binary(email) and is_binary(password) do
user = Repo.get_by(User, email: email)
if User.valid_password?(user, password), do: user
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id)
## User registration
@doc """
Registers a user.
## Examples
iex> register_user(%{field: value})
{:ok, %User{}}
iex> register_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user_registration(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_registration(%User{} = user, attrs \\ %{}) do
User.registration_changeset(user, attrs, hash_password: false, validate_email: false)
end
## Settings
@doc """
Returns an `%Ecto.Changeset{}` for changing the user email.
## Examples
iex> change_user_email(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_email(user, attrs \\ %{}) do
User.email_changeset(user, attrs, validate_email: false)
end
@doc """
Emulates that the email will change without actually changing
it in the database.
## Examples
iex> apply_user_email(user, "valid password", %{email: ...})
{:ok, %User{}}
iex> apply_user_email(user, "invalid password", %{email: ...})
{:error, %Ecto.Changeset{}}
"""
def apply_user_email(user, password, attrs) do
user
|> User.email_changeset(attrs)
|> User.validate_current_password(password)
|> Ecto.Changeset.apply_action(:update)
end
@doc """
Updates the user email using the given token.
If the token matches, the user email is updated and the token is deleted.
The confirmed_at date is also updated to the current time.
"""
def update_user_email(user, token) do
context = "change:#{user.email}"
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
%UserToken{sent_to: email} <- Repo.one(query),
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
:ok
else
_ -> :error
end
end
defp user_email_multi(user, email, context) do
changeset =
user
|> User.email_changeset(%{email: email})
|> User.confirm_changeset()
Ecto.Multi.new()
|> Ecto.Multi.update(:user, changeset)
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, [context]))
end
@doc ~S"""
Delivers the update email instructions to the given user.
## Examples
iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm_email/#{&1}"))
{:ok, %{to: ..., body: ...}}
"""
def deliver_user_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
when is_function(update_email_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
Repo.insert!(user_token)
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
end
@doc """
Returns an `%Ecto.Changeset{}` for changing the user password.
## Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_password(user, attrs \\ %{}) do
User.password_changeset(user, attrs, hash_password: false)
end
@doc """
Updates the user password.
## Examples
iex> update_user_password(user, "valid password", %{password: ...})
{:ok, %User{}}
iex> update_user_password(user, "invalid password", %{password: ...})
{:error, %Ecto.Changeset{}}
"""
def update_user_password(user, password, attrs) do
changeset =
user
|> User.password_changeset(attrs)
|> User.validate_current_password(password)
Ecto.Multi.new()
|> Ecto.Multi.update(:user, changeset)
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
|> Repo.transaction()
|> case do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
end
end
## Session
@doc """
Generates a session token.
"""
def generate_user_session_token(user) do
{token, user_token} = UserToken.build_session_token(user)
Repo.insert!(user_token)
token
end
@doc """
Gets the user with the given signed token.
"""
def get_user_by_session_token(token) do
{:ok, query} = UserToken.verify_session_token_query(token)
Repo.one(query)
end
@doc """
Deletes the signed token with the given context.
"""
def delete_user_session_token(token) do
Repo.delete_all(UserToken.by_token_and_context_query(token, "session"))
:ok
end
## Confirmation
@doc ~S"""
Delivers the confirmation email instructions to the given user.
## Examples
iex> deliver_user_confirmation_instructions(user, &url(~p"/users/confirm/#{&1}"))
{:ok, %{to: ..., body: ...}}
iex> deliver_user_confirmation_instructions(confirmed_user, &url(~p"/users/confirm/#{&1}"))
{:error, :already_confirmed}
"""
def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
when is_function(confirmation_url_fun, 1) do
if user.confirmed_at do
{:error, :already_confirmed}
else
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
Repo.insert!(user_token)
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
end
end
@doc """
Confirms a user by the given token.
If the token matches, the user account is marked as confirmed
and the token is deleted.
"""
def confirm_user(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
%User{} = user <- Repo.one(query),
{:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
{:ok, user}
else
_ -> :error
end
end
defp confirm_user_multi(user) do
Ecto.Multi.new()
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, ["confirm"]))
end
## Reset password
@doc ~S"""
Delivers the reset password email to the given user.
## Examples
iex> deliver_user_reset_password_instructions(user, &url(~p"/users/reset_password/#{&1}"))
{:ok, %{to: ..., body: ...}}
"""
def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
when is_function(reset_password_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
Repo.insert!(user_token)
UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
end
@doc """
Gets the user by reset password token.
## Examples
iex> get_user_by_reset_password_token("validtoken")
%User{}
iex> get_user_by_reset_password_token("invalidtoken")
nil
"""
def get_user_by_reset_password_token(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
%User{} = user <- Repo.one(query) do
user
else
_ -> nil
end
end
@doc """
Resets the user password.
## Examples
iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
{:ok, %User{}}
iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
{:error, %Ecto.Changeset{}}
"""
def reset_user_password(user, attrs) do
Ecto.Multi.new()
|> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
|> Repo.transaction()
|> case do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
end
end
end

View file

@ -0,0 +1,161 @@
defmodule Zoeyscomputer.Users.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :current_password, :string, virtual: true, redact: true
field :confirmed_at, :utc_datetime
timestamps(type: :utc_datetime)
end
@doc """
A user changeset for registration.
It is important to validate the length of both email and password.
Otherwise databases may truncate the email without warnings, which
could lead to unpredictable or insecure behaviour. Long passwords may
also be very expensive to hash for certain algorithms.
## Options
* `:hash_password` - Hashes the password so it can be stored securely
in the database and ensures the password field is cleared to prevent
leaks in the logs. If password hashing is not needed and clearing the
password field is not desired (like when using this changeset for
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
* `:validate_email` - Validates the uniqueness of the email, in case
you don't want to validate the uniqueness of the email (like when
using this changeset for validations on a LiveView form before
submitting the form), this option can be set to `false`.
Defaults to `true`.
"""
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password])
|> validate_email(opts)
|> validate_password(opts)
end
defp validate_email(changeset, opts) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> maybe_validate_unique_email(opts)
end
defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
# Examples of additional password validation:
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end
defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)
if hash_password? && password && changeset.valid? do
changeset
# If using Bcrypt, then further validate it is at most 72 bytes long
|> validate_length(:password, max: 72, count: :bytes)
# Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
# would keep the database transaction open longer and hurt performance.
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
defp maybe_validate_unique_email(changeset, opts) do
if Keyword.get(opts, :validate_email, true) do
changeset
|> unsafe_validate_unique(:email, Zoeyscomputer.Repo)
|> unique_constraint(:email)
else
changeset
end
end
@doc """
A user changeset for changing the email.
It requires the email to change otherwise an error is added.
"""
def email_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email])
|> validate_email(opts)
|> case do
%{changes: %{email: _}} = changeset -> changeset
%{} = changeset -> add_error(changeset, :email, "did not change")
end
end
@doc """
A user changeset for changing the password.
## Options
* `:hash_password` - Hashes the password so it can be stored securely
in the database and ensures the password field is cleared to prevent
leaks in the logs. If password hashing is not needed and clearing the
password field is not desired (like when using this changeset for
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
"""
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password")
|> validate_password(opts)
end
@doc """
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(user) do
now = DateTime.utc_now() |> DateTime.truncate(:second)
change(user, confirmed_at: now)
end
@doc """
Verifies the password.
If there is no user or the user doesn't have a password, we call
`Bcrypt.no_user_verify/0` to avoid timing attacks.
"""
def valid_password?(%Zoeyscomputer.Users.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password)
end
def valid_password?(_, _) do
Bcrypt.no_user_verify()
false
end
@doc """
Validates the current password otherwise adds an error to the changeset.
"""
def validate_current_password(changeset, password) do
changeset = cast(changeset, %{current_password: password}, [:current_password])
if valid_password?(changeset.data, password) do
changeset
else
add_error(changeset, :current_password, "is not valid")
end
end
end

View file

@ -0,0 +1,79 @@
defmodule Zoeyscomputer.Users.UserNotifier do
import Swoosh.Email
alias Zoeyscomputer.Mailer
# Delivers the email using the application mailer.
defp deliver(recipient, subject, body) do
email =
new()
|> to(recipient)
|> from({"Zoeyscomputer", "contact@example.com"})
|> subject(subject)
|> text_body(body)
with {:ok, _metadata} <- Mailer.deliver(email) do
{:ok, email}
end
end
@doc """
Deliver instructions to confirm account.
"""
def deliver_confirmation_instructions(user, url) do
deliver(user.email, "Confirmation instructions", """
==============================
Hi #{user.email},
You can confirm your account by visiting the URL below:
#{url}
If you didn't create an account with us, please ignore this.
==============================
""")
end
@doc """
Deliver instructions to reset a user password.
"""
def deliver_reset_password_instructions(user, url) do
deliver(user.email, "Reset password instructions", """
==============================
Hi #{user.email},
You can reset your password by visiting the URL below:
#{url}
If you didn't request this change, please ignore this.
==============================
""")
end
@doc """
Deliver instructions to update a user email.
"""
def deliver_update_email_instructions(user, url) do
deliver(user.email, "Update email instructions", """
==============================
Hi #{user.email},
You can change your email by visiting the URL below:
#{url}
If you didn't request this change, please ignore this.
==============================
""")
end
end

View file

@ -0,0 +1,179 @@
defmodule Zoeyscomputer.Users.UserToken do
use Ecto.Schema
import Ecto.Query
alias Zoeyscomputer.Users.UserToken
@hash_algorithm :sha256
@rand_size 32
# It is very important to keep the reset password token expiry short,
# since someone with access to the email may take over the account.
@reset_password_validity_in_days 1
@confirm_validity_in_days 7
@change_email_validity_in_days 7
@session_validity_in_days 60
schema "users_tokens" do
field :token, :binary
field :context, :string
field :sent_to, :string
belongs_to :user, Zoeyscomputer.Users.User
timestamps(type: :utc_datetime, updated_at: false)
end
@doc """
Generates a token that will be stored in a signed place,
such as session or cookie. As they are signed, those
tokens do not need to be hashed.
The reason why we store session tokens in the database, even
though Phoenix already provides a session cookie, is because
Phoenix' default session cookies are not persisted, they are
simply signed and potentially encrypted. This means they are
valid indefinitely, unless you change the signing/encryption
salt.
Therefore, storing them allows individual user
sessions to be expired. The token system can also be extended
to store additional data, such as the device used for logging in.
You could then use this information to display all valid sessions
and devices in the UI and allow users to explicitly expire any
session they deem invalid.
"""
def build_session_token(user) do
token = :crypto.strong_rand_bytes(@rand_size)
{token, %UserToken{token: token, context: "session", user_id: user.id}}
end
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any.
The token is valid if it matches the value in the database and it has
not expired (after @session_validity_in_days).
"""
def verify_session_token_query(token) do
query =
from token in by_token_and_context_query(token, "session"),
join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: user
{:ok, query}
end
@doc """
Builds a token and its hash to be delivered to the user's email.
The non-hashed token is sent to the user email while the
hashed part is stored in the database. The original token cannot be reconstructed,
which means anyone with read-only access to the database cannot directly use
the token in the application to gain access. Furthermore, if the user changes
their email in the system, the tokens sent to the previous email are no longer
valid.
Users can easily adapt the existing code to provide other types of delivery methods,
for example, by phone numbers.
"""
def build_email_token(user, context) do
build_hashed_token(user, context, user.email)
end
defp build_hashed_token(user, context, sent_to) do
token = :crypto.strong_rand_bytes(@rand_size)
hashed_token = :crypto.hash(@hash_algorithm, token)
{Base.url_encode64(token, padding: false),
%UserToken{
token: hashed_token,
context: context,
sent_to: sent_to,
user_id: user.id
}}
end
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any.
The given token is valid if it matches its hashed counterpart in the
database and the user email has not changed. This function also checks
if the token is being used within a certain period, depending on the
context. The default contexts supported by this function are either
"confirm", for account confirmation emails, and "reset_password",
for resetting the password. For verifying requests to change the email,
see `verify_change_email_token_query/2`.
"""
def verify_email_token_query(token, context) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
days = days_for_context(context)
query =
from token in by_token_and_context_query(hashed_token, context),
join: user in assoc(token, :user),
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
select: user
{:ok, query}
:error ->
:error
end
end
defp days_for_context("confirm"), do: @confirm_validity_in_days
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any.
This is used to validate requests to change the user
email. It is different from `verify_email_token_query/2` precisely because
`verify_email_token_query/2` validates the email has not changed, which is
the starting point by this function.
The given token is valid if it matches its hashed counterpart in the
database and if it has not expired (after @change_email_validity_in_days).
The context must always start with "change:".
"""
def verify_change_email_token_query(token, "change:" <> _ = context) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
from token in by_token_and_context_query(hashed_token, context),
where: token.inserted_at > ago(@change_email_validity_in_days, "day")
{:ok, query}
:error ->
:error
end
end
@doc """
Returns the token struct for the given token value and context.
"""
def by_token_and_context_query(token, context) do
from UserToken, where: [token: ^token, context: ^context]
end
@doc """
Gets all tokens for the given user for the given contexts.
"""
def by_user_and_contexts_query(user, :all) do
from t in UserToken, where: t.user_id == ^user.id
end
def by_user_and_contexts_query(user, [_ | _] = contexts) do
from t in UserToken, where: t.user_id == ^user.id and t.context in ^contexts
end
end

View file

@ -12,6 +12,47 @@
</script> </script>
</head> </head>
<body class="bg-white"> <body class="bg-white">
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 text-zinc-900">
<%= @current_user.email %>
</li>
<li>
<.link
href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Settings
</.link>
</li>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/users/register"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Register
</.link>
</li>
<li>
<.link
href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
</.link>
</li>
<% end %>
</ul>
<%= @inner_content %> <%= @inner_content %>
</body> </body>
</html> </html>

View file

@ -0,0 +1,42 @@
defmodule ZoeyscomputerWeb.UserSessionController do
use ZoeyscomputerWeb, :controller
alias Zoeyscomputer.Users
alias ZoeyscomputerWeb.UserAuth
def create(conn, %{"_action" => "registered"} = params) do
create(conn, params, "Account created successfully!")
end
def create(conn, %{"_action" => "password_updated"} = params) do
conn
|> put_session(:user_return_to, ~p"/users/settings")
|> create(params, "Password updated successfully!")
end
def create(conn, params) do
create(conn, params, "Welcome back!")
end
defp create(conn, %{"user" => user_params}, info) do
%{"email" => email, "password" => password} = user_params
if user = Users.get_user_by_email_and_password(email, password) do
conn
|> put_flash(:info, info)
|> UserAuth.log_in_user(user, user_params)
else
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:email, String.slice(email, 0, 160))
|> redirect(to: ~p"/users/log_in")
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
end

View file

@ -7,7 +7,7 @@ defmodule ZoeyscomputerWeb.Endpoint do
@session_options [ @session_options [
store: :cookie, store: :cookie,
key: "_zoeyscomputer_key", key: "_zoeyscomputer_key",
signing_salt: "G7dIA3Vb", signing_salt: "kGKSTrbw",
same_site: "Lax" same_site: "Lax"
] ]

View file

@ -0,0 +1,82 @@
defmodule ZoeyscomputerWeb.LinkLive.FormComponent do
use ZoeyscomputerWeb, :live_component
alias Zoeyscomputer.Links
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage link records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="link-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:url]} type="text" label="Url" />
<:actions>
<.button phx-disable-with="Saving...">Save Link</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{link: link} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Links.change_link(link))
end)}
end
@impl true
def handle_event("validate", %{"link" => link_params}, socket) do
changeset = Links.change_link(socket.assigns.link, link_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"link" => link_params}, socket) do
save_link(socket, socket.assigns.action, link_params)
end
defp save_link(socket, :edit, link_params) do
case Links.update_link(socket.assigns.link, link_params) do
{:ok, link} ->
notify_parent({:saved, link})
{:noreply,
socket
|> put_flash(:info, "Link updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_link(socket, :new, link_params) do
case Links.create_link(link_params) do
{:ok, link} ->
notify_parent({:saved, link})
{:noreply,
socket
|> put_flash(:info, "Link created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,39 @@
defmodule ZoeyscomputerWeb.LinkLive.Index do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Links
def mount(_params, _session, socket) do
user_id = socket.assigns.current_user.id
changeset = Links.Link.changeset(%Links.Link{})
socket =
socket
|> assign(:links, Links.list_links(user_id))
|> assign(:form, to_form(changeset))
{:ok, socket}
end
def handle_event("submit", %{"link" => link_params}, socket) do
params =
link_params
|> Map.put("user_id", socket.assigns.current_user.id)
case Links.create_link(params) do
{:ok, link} ->
socket =
socket
|> assign(:links, [link | socket.assigns.links])
{:noreply, socket}
{:error, changeset} ->
socket
|> assign(:form, to_form(changeset))
{:noreply, socket}
end
end
end

View file

@ -0,0 +1,30 @@
<div class="flex gap-2">
<h1 class="text-2xl grow font-bold">Links</h1>
<.link
navigate={~p"/links/new"}
class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md"
>
Add Link
</.link>
</div>
<div class="divide-y">
<div :for={link <- @links}>
<div>
<div class="font-bold"><%= link.url %></div>
<div class="text-sm"><%= link.inserted_at %></div>
</div>
</div>
</div>
<.form for={@form} phx-submit="submit">
<div class="flex gap-2 items-end">
<div class="grow">
<.input field={@form[:url]} type="text" label="url" />
</div>
<button class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md">
Create
</button>
</div>
</.form>

View file

@ -0,0 +1,37 @@
defmodule ZoeyscomputerWeb.LinkLive.New do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Links
def mount(_params, _session, socket) do
changeset = Links.Link.changeset(%Links.Link{})
socket =
socket
|> assign(:form, to_form(changeset))
{:ok, socket}
end
def handle_event("submit", %{"link" => link_params}, socket) do
params =
link_params
|> Map.put("user_id", socket.assigns.current_user.id)
case Links.create_link(params) do
{:ok, _link} ->
socket =
socket
|> put_flash(:info, "Link created successfully")
|> push_navigate(to: ~p"/links")
{:noreply, socket}
{:error, changeset} ->
socket
|> assign(:form, to_form(changeset))
{:noreply, socket}
end
end
end

View file

@ -0,0 +1,12 @@
<h1 class="text-2xl grow font-bold mb-6">Create a new link</h1>
<.form for={@form} phx-submit="submit">
<div class="flex gap-2 items-end">
<div class="grow">
<.input field={@form[:url]} type="text" label="url" />
</div>
<button class="bg-black border border-black hover:bg-gray-700 text-white font-bold py-2 px-3 rounded-md">
Create
</button>
</div>
</.form>

View file

@ -0,0 +1,51 @@
defmodule ZoeyscomputerWeb.UserConfirmationInstructionsLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">
No confirmation instructions received?
<:subtitle>We'll send a new confirmation link to your inbox</:subtitle>
</.header>
<.simple_form for={@form} id="resend_confirmation_form" phx-submit="send_instructions">
<.input field={@form[:email]} type="email" placeholder="Email" required />
<:actions>
<.button phx-disable-with="Sending..." class="w-full">
Resend confirmation instructions
</.button>
</:actions>
</.simple_form>
<p class="text-center mt-4">
<.link href={~p"/users/register"}>Register</.link>
| <.link href={~p"/users/log_in"}>Log in</.link>
</p>
</div>
"""
end
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{}, as: "user"))}
end
def handle_event("send_instructions", %{"user" => %{"email" => email}}, socket) do
if user = Users.get_user_by_email(email) do
Users.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)
end
info =
"If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
{:noreply,
socket
|> put_flash(:info, info)
|> redirect(to: ~p"/")}
end
end

View file

@ -0,0 +1,58 @@
defmodule ZoeyscomputerWeb.UserConfirmationLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
def render(%{live_action: :edit} = assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">Confirm Account</.header>
<.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
<:actions>
<.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
</:actions>
</.simple_form>
<p class="text-center mt-4">
<.link href={~p"/users/register"}>Register</.link>
| <.link href={~p"/users/log_in"}>Log in</.link>
</p>
</div>
"""
end
def mount(%{"token" => token}, _session, socket) do
form = to_form(%{"token" => token}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
end
# Do not log in the user after confirmation to avoid a
# leaked token giving the user access to the account.
def handle_event("confirm_account", %{"user" => %{"token" => token}}, socket) do
case Users.confirm_user(token) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "User confirmed successfully.")
|> redirect(to: ~p"/")}
:error ->
# If there is a current user and the account was already confirmed,
# then odds are that the confirmation link was already visited, either
# by some automation or by the user themselves, so we redirect without
# a warning message.
case socket.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
{:noreply, redirect(socket, to: ~p"/")}
%{} ->
{:noreply,
socket
|> put_flash(:error, "User confirmation link is invalid or it has expired.")
|> redirect(to: ~p"/")}
end
end
end
end

View file

@ -0,0 +1,50 @@
defmodule ZoeyscomputerWeb.UserForgotPasswordLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">
Forgot your password?
<:subtitle>We'll send a password reset link to your inbox</:subtitle>
</.header>
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
<.input field={@form[:email]} type="email" placeholder="Email" required />
<:actions>
<.button phx-disable-with="Sending..." class="w-full">
Send password reset instructions
</.button>
</:actions>
</.simple_form>
<p class="text-center text-sm mt-4">
<.link href={~p"/users/register"}>Register</.link>
| <.link href={~p"/users/log_in"}>Log in</.link>
</p>
</div>
"""
end
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{}, as: "user"))}
end
def handle_event("send_email", %{"user" => %{"email" => email}}, socket) do
if user = Users.get_user_by_email(email) do
Users.deliver_user_reset_password_instructions(
user,
&url(~p"/users/reset_password/#{&1}")
)
end
info =
"If your email is in our system, you will receive instructions to reset your password shortly."
{:noreply,
socket
|> put_flash(:info, info)
|> redirect(to: ~p"/")}
end
end

View file

@ -0,0 +1,43 @@
defmodule ZoeyscomputerWeb.UserLoginLive do
use ZoeyscomputerWeb, :live_view
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">
Log in to account
<:subtitle>
Don't have an account?
<.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline">
Sign up
</.link>
for an account now.
</:subtitle>
</.header>
<.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore">
<.input field={@form[:email]} type="email" label="Email" required />
<.input field={@form[:password]} type="password" label="Password" required />
<:actions>
<.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
<.link href={~p"/users/reset_password"} class="text-sm font-semibold">
Forgot your password?
</.link>
</:actions>
<:actions>
<.button phx-disable-with="Logging in..." class="w-full">
Log in <span aria-hidden="true"></span>
</.button>
</:actions>
</.simple_form>
</div>
"""
end
def mount(_params, _session, socket) do
email = Phoenix.Flash.get(socket.assigns.flash, :email)
form = to_form(%{"email" => email}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
end

View file

@ -0,0 +1,87 @@
defmodule ZoeyscomputerWeb.UserRegistrationLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
alias Zoeyscomputer.Users.User
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">
Register for an account
<:subtitle>
Already registered?
<.link navigate={~p"/users/log_in"} class="font-semibold text-brand hover:underline">
Log in
</.link>
to your account now.
</:subtitle>
</.header>
<.simple_form
for={@form}
id="registration_form"
phx-submit="save"
phx-change="validate"
phx-trigger-action={@trigger_submit}
action={~p"/users/log_in?_action=registered"}
method="post"
>
<.error :if={@check_errors}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={@form[:email]} type="email" label="Email" required />
<.input field={@form[:password]} type="password" label="Password" required />
<:actions>
<.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
</:actions>
</.simple_form>
</div>
"""
end
def mount(_params, _session, socket) do
changeset = Users.change_user_registration(%User{})
socket =
socket
|> assign(trigger_submit: false, check_errors: false)
|> assign_form(changeset)
{:ok, socket, temporary_assigns: [form: nil]}
end
def handle_event("save", %{"user" => user_params}, socket) do
case Users.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Users.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)
changeset = Users.change_user_registration(user)
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
end
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = Users.change_user_registration(%User{}, user_params)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
form = to_form(changeset, as: "user")
if changeset.valid? do
assign(socket, form: form, check_errors: false)
else
assign(socket, form: form)
end
end
end

View file

@ -0,0 +1,89 @@
defmodule ZoeyscomputerWeb.UserResetPasswordLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<.header class="text-center">Reset Password</.header>
<.simple_form
for={@form}
id="reset_password_form"
phx-submit="reset_password"
phx-change="validate"
>
<.error :if={@form.errors != []}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={@form[:password]} type="password" label="New password" required />
<.input
field={@form[:password_confirmation]}
type="password"
label="Confirm new password"
required
/>
<:actions>
<.button phx-disable-with="Resetting..." class="w-full">Reset Password</.button>
</:actions>
</.simple_form>
<p class="text-center text-sm mt-4">
<.link href={~p"/users/register"}>Register</.link>
| <.link href={~p"/users/log_in"}>Log in</.link>
</p>
</div>
"""
end
def mount(params, _session, socket) do
socket = assign_user_and_token(socket, params)
form_source =
case socket.assigns do
%{user: user} ->
Users.change_user_password(user)
_ ->
%{}
end
{:ok, assign_form(socket, form_source), temporary_assigns: [form: nil]}
end
# Do not log in the user after reset password to avoid a
# leaked token giving the user access to the account.
def handle_event("reset_password", %{"user" => user_params}, socket) do
case Users.reset_user_password(socket.assigns.user, user_params) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Password reset successfully.")
|> redirect(to: ~p"/users/log_in")}
{:error, changeset} ->
{:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
end
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = Users.change_user_password(socket.assigns.user, user_params)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end
defp assign_user_and_token(socket, %{"token" => token}) do
if user = Users.get_user_by_reset_password_token(token) do
assign(socket, user: user, token: token)
else
socket
|> put_flash(:error, "Reset password link is invalid or it has expired.")
|> redirect(to: ~p"/")
end
end
defp assign_form(socket, %{} = source) do
assign(socket, :form, to_form(source, as: "user"))
end
end

View file

@ -0,0 +1,167 @@
defmodule ZoeyscomputerWeb.UserSettingsLive do
use ZoeyscomputerWeb, :live_view
alias Zoeyscomputer.Users
def render(assigns) do
~H"""
<.header class="text-center">
Account Settings
<:subtitle>Manage your account email address and password settings</:subtitle>
</.header>
<div class="space-y-12 divide-y">
<div>
<.simple_form
for={@email_form}
id="email_form"
phx-submit="update_email"
phx-change="validate_email"
>
<.input field={@email_form[:email]} type="email" label="Email" required />
<.input
field={@email_form[:current_password]}
name="current_password"
id="current_password_for_email"
type="password"
label="Current password"
value={@email_form_current_password}
required
/>
<:actions>
<.button phx-disable-with="Changing...">Change Email</.button>
</:actions>
</.simple_form>
</div>
<div>
<.simple_form
for={@password_form}
id="password_form"
action={~p"/users/log_in?_action=password_updated"}
method="post"
phx-change="validate_password"
phx-submit="update_password"
phx-trigger-action={@trigger_submit}
>
<input
name={@password_form[:email].name}
type="hidden"
id="hidden_user_email"
value={@current_email}
/>
<.input field={@password_form[:password]} type="password" label="New password" required />
<.input
field={@password_form[:password_confirmation]}
type="password"
label="Confirm new password"
/>
<.input
field={@password_form[:current_password]}
name="current_password"
type="password"
label="Current password"
id="current_password_for_password"
value={@current_password}
required
/>
<:actions>
<.button phx-disable-with="Changing...">Change Password</.button>
</:actions>
</.simple_form>
</div>
</div>
"""
end
def mount(%{"token" => token}, _session, socket) do
socket =
case Users.update_user_email(socket.assigns.current_user, token) do
:ok ->
put_flash(socket, :info, "Email changed successfully.")
:error ->
put_flash(socket, :error, "Email change link is invalid or it has expired.")
end
{:ok, push_navigate(socket, to: ~p"/users/settings")}
end
def mount(_params, _session, socket) do
user = socket.assigns.current_user
email_changeset = Users.change_user_email(user)
password_changeset = Users.change_user_password(user)
socket =
socket
|> assign(:current_password, nil)
|> assign(:email_form_current_password, nil)
|> assign(:current_email, user.email)
|> assign(:email_form, to_form(email_changeset))
|> assign(:password_form, to_form(password_changeset))
|> assign(:trigger_submit, false)
{:ok, socket}
end
def handle_event("validate_email", params, socket) do
%{"current_password" => password, "user" => user_params} = params
email_form =
socket.assigns.current_user
|> Users.change_user_email(user_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, email_form: email_form, email_form_current_password: password)}
end
def handle_event("update_email", params, socket) do
%{"current_password" => password, "user" => user_params} = params
user = socket.assigns.current_user
case Users.apply_user_email(user, password, user_params) do
{:ok, applied_user} ->
Users.deliver_user_update_email_instructions(
applied_user,
user.email,
&url(~p"/users/settings/confirm_email/#{&1}")
)
info = "A link to confirm your email change has been sent to the new address."
{:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
{:error, changeset} ->
{:noreply, assign(socket, :email_form, to_form(Map.put(changeset, :action, :insert)))}
end
end
def handle_event("validate_password", params, socket) do
%{"current_password" => password, "user" => user_params} = params
password_form =
socket.assigns.current_user
|> Users.change_user_password(user_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, password_form: password_form, current_password: password)}
end
def handle_event("update_password", params, socket) do
%{"current_password" => password, "user" => user_params} = params
user = socket.assigns.current_user
case Users.update_user_password(user, password, user_params) do
{:ok, user} ->
password_form =
user
|> Users.change_user_password(user_params)
|> to_form()
{:noreply, assign(socket, trigger_submit: true, password_form: password_form)}
{:error, changeset} ->
{:noreply, assign(socket, password_form: to_form(changeset))}
end
end
end

View file

@ -1,6 +1,8 @@
defmodule ZoeyscomputerWeb.Router do defmodule ZoeyscomputerWeb.Router do
use ZoeyscomputerWeb, :router use ZoeyscomputerWeb, :router
import ZoeyscomputerWeb.UserAuth
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
plug :fetch_session plug :fetch_session
@ -8,6 +10,7 @@ defmodule ZoeyscomputerWeb.Router do
plug :put_root_layout, html: {ZoeyscomputerWeb.Layouts, :root} plug :put_root_layout, html: {ZoeyscomputerWeb.Layouts, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
plug :fetch_current_user
end end
pipeline :api do pipeline :api do
@ -41,4 +44,44 @@ defmodule ZoeyscomputerWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview forward "/mailbox", Plug.Swoosh.MailboxPreview
end end
end end
## Authentication routes
scope "/", ZoeyscomputerWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
live_session :redirect_if_user_is_authenticated,
on_mount: [{ZoeyscomputerWeb.UserAuth, :redirect_if_user_is_authenticated}] do
live "/users/register", UserRegistrationLive, :new
live "/users/log_in", UserLoginLive, :new
live "/users/reset_password", UserForgotPasswordLive, :new
live "/users/reset_password/:token", UserResetPasswordLive, :edit
end
post "/users/log_in", UserSessionController, :create
end
scope "/", ZoeyscomputerWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{ZoeyscomputerWeb.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/links", LinkLive.Index
live "/links/new", LinkLive.New
end
end
scope "/", ZoeyscomputerWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
live_session :current_user,
on_mount: [{ZoeyscomputerWeb.UserAuth, :mount_current_user}] do
live "/users/confirm/:token", UserConfirmationLive, :edit
live "/users/confirm", UserConfirmationInstructionsLive, :new
end
end
end end

View file

@ -0,0 +1,229 @@
defmodule ZoeyscomputerWeb.UserAuth do
use ZoeyscomputerWeb, :verified_routes
import Plug.Conn
import Phoenix.Controller
alias Zoeyscomputer.Users
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_zoeyscomputer_web_user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
@doc """
Logs the user in.
It renews the session ID and clears the whole session
to avoid fixation attacks. See the renew_session
function to customize this behaviour.
It also sets a `:live_socket_id` key in the session,
so LiveView sessions are identified and automatically
disconnected on log out. The line can be safely removed
if you are not using LiveView.
"""
def log_in_user(conn, user, params \\ %{}) do
token = Users.generate_user_session_token(user)
user_return_to = get_session(conn, :user_return_to)
conn
|> renew_session()
|> put_token_in_session(token)
|> maybe_write_remember_me_cookie(token, params)
|> redirect(to: user_return_to || signed_in_path(conn))
end
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
defp maybe_write_remember_me_cookie(conn, _token, _params) do
conn
end
# This function renews the session ID and erases the whole
# session to avoid fixation attacks. If there is any data
# in the session you may want to preserve after log in/log out,
# you must explicitly fetch the session data before clearing
# and then immediately set it after clearing, for example:
#
# defp renew_session(conn) do
# preferred_locale = get_session(conn, :preferred_locale)
#
# conn
# |> configure_session(renew: true)
# |> clear_session()
# |> put_session(:preferred_locale, preferred_locale)
# end
#
defp renew_session(conn) do
delete_csrf_token()
conn
|> configure_session(renew: true)
|> clear_session()
end
@doc """
Logs the user out.
It clears all session data for safety. See renew_session.
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Users.delete_user_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
ZoeyscomputerWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: ~p"/")
end
@doc """
Authenticates the user by looking into the session
and remember me token.
"""
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Users.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
end
defp ensure_user_token(conn) do
if token = get_session(conn, :user_token) do
{token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
if token = conn.cookies[@remember_me_cookie] do
{token, put_token_in_session(conn, token)}
else
{nil, conn}
end
end
end
@doc """
Handles mounting and authenticating the current_user in LiveViews.
## `on_mount` arguments
* `:mount_current_user` - Assigns current_user
to socket assigns based on user_token, or nil if
there's no user_token or no matching user.
* `:ensure_authenticated` - Authenticates the user from the session,
and assigns the current_user to socket assigns based
on user_token.
Redirects to login page if there's no logged user.
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
Redirects to signed_in_path if there's a logged user.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:
defmodule ZoeyscomputerWeb.PageLive do
use ZoeyscomputerWeb, :live_view
on_mount {ZoeyscomputerWeb.UserAuth, :mount_current_user}
...
end
Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{ZoeyscomputerWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, mount_current_user(socket, session)}
end
def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_user(socket, session)
if socket.assigns.current_user do
{:cont, socket}
else
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
{:halt, socket}
end
end
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
socket = mount_current_user(socket, session)
if socket.assigns.current_user do
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
else
{:cont, socket}
end
end
defp mount_current_user(socket, session) do
Phoenix.Component.assign_new(socket, :current_user, fn ->
if user_token = session["user_token"] do
Users.get_user_by_session_token(user_token)
end
end)
end
@doc """
Used for routes that require the user to not be authenticated.
"""
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
else
conn
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/users/log_in")
|> halt()
end
end
defp put_token_in_session(conn, token) do
conn
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: ~p"/"
end

View file

@ -32,6 +32,7 @@ defmodule Zoeyscomputer.MixProject do
# Type `mix help deps` for examples and options. # Type `mix help deps` for examples and options.
defp deps do defp deps do
[ [
{:bcrypt_elixir, "~> 3.0"},
{:phoenix, "~> 1.7.14"}, {:phoenix, "~> 1.7.14"},
{:phoenix_ecto, "~> 4.5"}, {:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.10"}, {:ecto_sql, "~> 3.10"},
@ -54,10 +55,10 @@ defmodule Zoeyscomputer.MixProject do
{:swoosh, "~> 1.5"}, {:swoosh, "~> 1.5"},
{:finch, "~> 0.13"}, {:finch, "~> 0.13"},
{:telemetry_metrics, "~> 1.0"}, {:telemetry_metrics, "~> 1.0"},
{:salad_ui, "~> 0.9.0", only: [:dev]},
{:telemetry_poller, "~> 1.0"}, {:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.20"}, {:gettext, "~> 0.20"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:deps_nix, "~> 0.0", only: :dev},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"} {:bandit, "~> 1.5"}
] ]
@ -71,6 +72,8 @@ defmodule Zoeyscomputer.MixProject do
# See the documentation for `Mix` for more info on aliases. # See the documentation for `Mix` for more info on aliases.
defp aliases do defp aliases do
[ [
"deps.get": ["deps.get", "deps.nix"],
"deps.update": ["deps.update", "deps.nix"],
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.reset": ["ecto.drop", "ecto.setup"],

View file

@ -1,18 +1,22 @@
%{ %{
"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"}, "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.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"},
"castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
"comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "db_connection": {:hex, :db_connection, "2.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"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deps_nix": {:hex, :deps_nix, "0.6.0", "4f642620696f389a8826bcbb6cda9a043f82c84a36acfe594842d7a722fea990", [:mix], [], "hexpm", "61cd72c63b7ce58ae86a012b8c96a65018b6201ba9c0a6b55249f246c7252c80"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [: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", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [: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", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"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.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, "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"}, "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"]}, "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"}, "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"}, "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"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},

View file

@ -0,0 +1,29 @@
defmodule Zoeyscomputer.Repo.Migrations.CreateUsersAuthTables do
use Ecto.Migration
def change do
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
create table(:users) do
add :email, :citext, null: false
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
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

View file

@ -0,0 +1,11 @@
defmodule Zoeyscomputer.Repo.Migrations.CreateLinks do
use Ecto.Migration
def change do
create table(:links) do
add :url, :text
timestamps(type: :utc_datetime)
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Zoeyscomputer.Repo.Migrations.AddUserToLinks do
use Ecto.Migration
def change do
alter table(:links) do
add :user_id, references(:users, on_delete: :nothing)
end
end
end

5
rel/overlays/bin/migrate Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
exec ./zoeyscomputer eval Zoeyscomputer.Release.migrate

1
rel/overlays/bin/migrate.bat Executable file
View file

@ -0,0 +1 @@
call "%~dp0\zoeyscomputer" eval Zoeyscomputer.Release.migrate

5
rel/overlays/bin/server Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
PHX_SERVER=true exec ./zoeyscomputer start

2
rel/overlays/bin/server.bat Executable file
View file

@ -0,0 +1,2 @@
set PHX_SERVER=true
call "%~dp0\zoeyscomputer" start

View file

@ -35,4 +35,30 @@ defmodule ZoeyscomputerWeb.ConnCase do
Zoeyscomputer.DataCase.setup_sandbox(tags) Zoeyscomputer.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()} {:ok, conn: Phoenix.ConnTest.build_conn()}
end 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 = Zoeyscomputer.UsersFixtures.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 = Zoeyscomputer.Users.generate_user_session_token(user)
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:user_token, token)
end
end end

View file

@ -0,0 +1,20 @@
defmodule Zoeyscomputer.LinksFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Zoeyscomputer.Links` context.
"""
@doc """
Generate a link.
"""
def link_fixture(attrs \\ %{}) do
{:ok, link} =
attrs
|> Enum.into(%{
url: "some url"
})
|> Zoeyscomputer.Links.create_link()
link
end
end

View file

@ -0,0 +1,31 @@
defmodule Zoeyscomputer.UsersFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Zoeyscomputer.Users` 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()
|> Zoeyscomputer.Users.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

View file

@ -0,0 +1,59 @@
defmodule Zoeyscomputer.LinksTest do
use Zoeyscomputer.DataCase
alias Zoeyscomputer.Links
describe "links" do
alias Zoeyscomputer.Links.Link
import Zoeyscomputer.LinksFixtures
@invalid_attrs %{url: nil}
test "list_links/0 returns all links" do
link = link_fixture()
assert Links.list_links() == [link]
end
test "get_link!/1 returns the link with given id" do
link = link_fixture()
assert Links.get_link!(link.id) == link
end
test "create_link/1 with valid data creates a link" do
valid_attrs = %{url: "some url"}
assert {:ok, %Link{} = link} = Links.create_link(valid_attrs)
assert link.url == "some url"
end
test "create_link/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Links.create_link(@invalid_attrs)
end
test "update_link/2 with valid data updates the link" do
link = link_fixture()
update_attrs = %{url: "some updated url"}
assert {:ok, %Link{} = link} = Links.update_link(link, update_attrs)
assert link.url == "some updated url"
end
test "update_link/2 with invalid data returns error changeset" do
link = link_fixture()
assert {:error, %Ecto.Changeset{}} = Links.update_link(link, @invalid_attrs)
assert link == Links.get_link!(link.id)
end
test "delete_link/1 deletes the link" do
link = link_fixture()
assert {:ok, %Link{}} = Links.delete_link(link)
assert_raise Ecto.NoResultsError, fn -> Links.get_link!(link.id) end
end
test "change_link/1 returns a link changeset" do
link = link_fixture()
assert %Ecto.Changeset{} = Links.change_link(link)
end
end
end

View file

@ -0,0 +1,508 @@
defmodule Zoeyscomputer.UsersTest do
use Zoeyscomputer.DataCase
alias Zoeyscomputer.Users
import Zoeyscomputer.UsersFixtures
alias Zoeyscomputer.Users.{User, UserToken}
describe "get_user_by_email/1" do
test "does not return the user if the email does not exist" do
refute Users.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} = Users.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 Users.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 Users.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} =
Users.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 ->
Users.get_user!(-1)
end
end
test "returns the user with the given id" do
%{id: id} = user = user_fixture()
assert %User{id: ^id} = Users.get_user!(user.id)
end
end
describe "register_user/1" do
test "requires email and password to be set" do
{:error, changeset} = Users.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} = Users.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} = Users.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} = Users.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} = Users.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} = Users.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 = Users.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 =
Users.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 = Users.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} = Users.apply_user_email(user, valid_user_password(), %{})
assert %{email: ["did not change"]} = errors_on(changeset)
end
test "validates email", %{user: user} do
{:error, changeset} =
Users.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} =
Users.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} = Users.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} =
Users.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} = Users.apply_user_email(user, valid_user_password(), %{email: email})
assert user.email == email
assert Users.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 ->
Users.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 ->
Users.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 Users.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 Users.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 Users.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 Users.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 = Users.change_user_password(%User{})
assert changeset.required == [:password]
end
test "allows fields to be set" do
changeset =
Users.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} =
Users.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} =
Users.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} =
Users.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} =
Users.update_user_password(user, valid_user_password(), %{
password: "new valid password"
})
assert is_nil(user.password)
assert Users.get_user_by_email_and_password(user.email, "new valid password")
end
test "deletes all tokens for the given user", %{user: user} do
_ = Users.generate_user_session_token(user)
{:ok, _} =
Users.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 = Users.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 = Users.generate_user_session_token(user)
%{user: user, token: token}
end
test "returns user by token", %{user: user, token: token} do
assert session_user = Users.get_user_by_session_token(token)
assert session_user.id == user.id
end
test "does not return user for invalid token" do
refute Users.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 Users.get_user_by_session_token(token)
end
end
describe "delete_user_session_token/1" do
test "deletes the token" do
user = user_fixture()
token = Users.generate_user_session_token(user)
assert Users.delete_user_session_token(token) == :ok
refute Users.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 ->
Users.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 ->
Users.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} = Users.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 Users.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 Users.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 ->
Users.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 ->
Users.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} = Users.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 Users.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 Users.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} =
Users.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} = Users.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} = Users.reset_user_password(user, %{password: "new valid password"})
assert is_nil(updated_user.password)
assert Users.get_user_by_email_and_password(user.email, "new valid password")
end
test "deletes all tokens for the given user", %{user: user} do
_ = Users.generate_user_session_token(user)
{:ok, _} = Users.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

View file

@ -0,0 +1,113 @@
defmodule ZoeyscomputerWeb.UserSessionControllerTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Zoeyscomputer.UsersFixtures
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["_zoeyscomputer_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

View file

@ -0,0 +1,113 @@
defmodule ZoeyscomputerWeb.LinkLiveTest do
use ZoeyscomputerWeb.ConnCase
import Phoenix.LiveViewTest
import Zoeyscomputer.LinksFixtures
@create_attrs %{url: "some url"}
@update_attrs %{url: "some updated url"}
@invalid_attrs %{url: nil}
defp create_link(_) do
link = link_fixture()
%{link: link}
end
describe "Index" do
setup [:create_link]
test "lists all links", %{conn: conn, link: link} do
{:ok, _index_live, html} = live(conn, ~p"/links")
assert html =~ "Listing Links"
assert html =~ link.url
end
test "saves new link", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/links")
assert index_live |> element("a", "New Link") |> render_click() =~
"New Link"
assert_patch(index_live, ~p"/links/new")
assert index_live
|> form("#link-form", link: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#link-form", link: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/links")
html = render(index_live)
assert html =~ "Link created successfully"
assert html =~ "some url"
end
test "updates link in listing", %{conn: conn, link: link} do
{:ok, index_live, _html} = live(conn, ~p"/links")
assert index_live |> element("#links-#{link.id} a", "Edit") |> render_click() =~
"Edit Link"
assert_patch(index_live, ~p"/links/#{link}/edit")
assert index_live
|> form("#link-form", link: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#link-form", link: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/links")
html = render(index_live)
assert html =~ "Link updated successfully"
assert html =~ "some updated url"
end
test "deletes link in listing", %{conn: conn, link: link} do
{:ok, index_live, _html} = live(conn, ~p"/links")
assert index_live |> element("#links-#{link.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#links-#{link.id}")
end
end
describe "Show" do
setup [:create_link]
test "displays link", %{conn: conn, link: link} do
{:ok, _show_live, html} = live(conn, ~p"/links/#{link}")
assert html =~ "Show Link"
assert html =~ link.url
end
test "updates link within modal", %{conn: conn, link: link} do
{:ok, show_live, _html} = live(conn, ~p"/links/#{link}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Link"
assert_patch(show_live, ~p"/links/#{link}/show/edit")
assert show_live
|> form("#link-form", link: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#link-form", link: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/links/#{link}")
html = render(show_live)
assert html =~ "Link updated successfully"
assert html =~ "some updated url"
end
end
end

View file

@ -0,0 +1,67 @@
defmodule ZoeyscomputerWeb.UserConfirmationInstructionsLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
alias Zoeyscomputer.Users
alias Zoeyscomputer.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!(Users.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!(Users.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(Users.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(Users.UserToken) == []
end
end
end

View file

@ -0,0 +1,89 @@
defmodule ZoeyscomputerWeb.UserConfirmationLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
alias Zoeyscomputer.Users
alias Zoeyscomputer.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 ->
Users.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 Users.get_user!(user.id).confirmed_at
refute get_session(conn, :user_token)
assert Repo.all(Users.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 Users.get_user!(user.id).confirmed_at
end
end
end

View file

@ -0,0 +1,63 @@
defmodule ZoeyscomputerWeb.UserForgotPasswordLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
alias Zoeyscomputer.Users
alias Zoeyscomputer.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!(Users.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(Users.UserToken) == []
end
end
end

View file

@ -0,0 +1,87 @@
defmodule ZoeyscomputerWeb.UserLoginLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
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

View file

@ -0,0 +1,87 @@
defmodule ZoeyscomputerWeb.UserRegistrationLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
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

View file

@ -0,0 +1,118 @@
defmodule ZoeyscomputerWeb.UserResetPasswordLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
alias Zoeyscomputer.Users
setup do
user = user_fixture()
token =
extract_user_token(fn url ->
Users.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 Users.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

View file

@ -0,0 +1,210 @@
defmodule ZoeyscomputerWeb.UserSettingsLiveTest do
use ZoeyscomputerWeb.ConnCase, async: true
alias Zoeyscomputer.Users
import Phoenix.LiveViewTest
import Zoeyscomputer.UsersFixtures
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 Users.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 Users.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 ->
Users.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 Users.get_user_by_email(user.email)
assert Users.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 Users.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

View file

@ -0,0 +1,272 @@
defmodule ZoeyscomputerWeb.UserAuthTest do
use ZoeyscomputerWeb.ConnCase, async: true
alias Phoenix.LiveView
alias Zoeyscomputer.Users
alias ZoeyscomputerWeb.UserAuth
import Zoeyscomputer.UsersFixtures
@remember_me_cookie "_zoeyscomputer_web_user_remember_me"
setup %{conn: conn} do
conn =
conn
|> Map.replace!(:secret_key_base, ZoeyscomputerWeb.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 Users.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 = Users.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 Users.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"
ZoeyscomputerWeb.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 = Users.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
_ = Users.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 = Users.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 = Users.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: ZoeyscomputerWeb.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: ZoeyscomputerWeb.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 = Users.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