run phx.new and add phx.gen.auth
This commit is contained in:
		
							
								
								
									
										5
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | [ | ||||||
|  |   import_deps: [:ecto, :phoenix], | ||||||
|  |   inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], | ||||||
|  |   subdirectories: ["priv/*/migrations"] | ||||||
|  | ] | ||||||
							
								
								
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | # The directory Mix will write compiled artifacts to. | ||||||
|  | /_build/ | ||||||
|  |  | ||||||
|  | # If you run "mix test --cover", coverage assets end up here. | ||||||
|  | /cover/ | ||||||
|  |  | ||||||
|  | # The directory Mix downloads your dependencies sources to. | ||||||
|  | /deps/ | ||||||
|  |  | ||||||
|  | # Where 3rd-party dependencies like ExDoc output generated docs. | ||||||
|  | /doc/ | ||||||
|  |  | ||||||
|  | # Ignore .fetch files in case you like to edit your project deps locally. | ||||||
|  | /.fetch | ||||||
|  |  | ||||||
|  | # If the VM crashes, it generates a dump, let's ignore it too. | ||||||
|  | erl_crash.dump | ||||||
|  |  | ||||||
|  | # Also ignore archive artifacts (built via "mix archive.build"). | ||||||
|  | *.ez | ||||||
|  |  | ||||||
|  | # Ignore package tarball (built via "mix hex.build"). | ||||||
|  | lokal-*.tar | ||||||
|  |  | ||||||
|  | # If NPM crashes, it generates a log, let's ignore it too. | ||||||
|  | npm-debug.log | ||||||
|  |  | ||||||
|  | # The directory NPM downloads your dependencies sources to. | ||||||
|  | /assets/node_modules/ | ||||||
|  |  | ||||||
|  | # Since we are building assets from assets/, | ||||||
|  | # we ignore priv/static. You may want to comment | ||||||
|  | # this depending on your deployment strategy. | ||||||
|  | /priv/static/ | ||||||
|  |  | ||||||
|  | .elixir_ls/ | ||||||
							
								
								
									
										3
									
								
								.tool-versions
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.tool-versions
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | elixir 1.12.2-otp-23 | ||||||
|  | erlang 23.3 | ||||||
|  | nodejs 14.16.0 | ||||||
							
								
								
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | # Lokal | ||||||
|  |  | ||||||
|  | Lokal is a local business aggregation site helping you to shop locally by providing a one-stop-shop for your local community. Set your profile and start shopping today! | ||||||
							
								
								
									
										5
									
								
								assets/.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								assets/.babelrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | { | ||||||
|  |     "presets": [ | ||||||
|  |         "@babel/preset-env" | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								assets/css/app.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								assets/css/app.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | @import "tailwindcss/base"; | ||||||
|  | @import "tailwindcss/components"; | ||||||
|  | @import "tailwindcss/utilities"; | ||||||
|  |  | ||||||
|  | @import "components"; | ||||||
|  |  | ||||||
|  | /* LiveView specific classes for your customizations */ | ||||||
|  | .phx-no-feedback.invalid-feedback, | ||||||
|  | .phx-no-feedback .invalid-feedback { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-click-loading { | ||||||
|  |   opacity: 0.5; | ||||||
|  |   transition: opacity 1s ease-out; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-disconnected{ | ||||||
|  |   cursor: wait; | ||||||
|  | } | ||||||
|  | .phx-disconnected *{ | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-modal { | ||||||
|  |   opacity: 1!important; | ||||||
|  |   position: fixed; | ||||||
|  |   z-index: 1; | ||||||
|  |   left: 0; | ||||||
|  |   top: 0; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   overflow: auto; | ||||||
|  |   background-color: rgb(0,0,0); | ||||||
|  |   background-color: rgba(0,0,0,0.4); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-modal-content { | ||||||
|  |   background-color: #fefefe; | ||||||
|  |   margin: 15% auto; | ||||||
|  |   padding: 20px; | ||||||
|  |   border: 1px solid #888; | ||||||
|  |   width: 80%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-modal-close { | ||||||
|  |   color: #aaa; | ||||||
|  |   float: right; | ||||||
|  |   font-size: 28px; | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .phx-modal-close:hover, | ||||||
|  | .phx-modal-close:focus { | ||||||
|  |   color: black; | ||||||
|  |   text-decoration: none; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* Alerts and form errors */ | ||||||
|  | .alert { | ||||||
|  |   padding: 15px; | ||||||
|  |   margin-bottom: 20px; | ||||||
|  |   border: 1px solid transparent; | ||||||
|  |   border-radius: 4px; | ||||||
|  | } | ||||||
|  | .alert-info { | ||||||
|  |   color: #31708f; | ||||||
|  |   background-color: #d9edf7; | ||||||
|  |   border-color: #bce8f1; | ||||||
|  | } | ||||||
|  | .alert-warning { | ||||||
|  |   color: #8a6d3b; | ||||||
|  |   background-color: #fcf8e3; | ||||||
|  |   border-color: #faebcc; | ||||||
|  | } | ||||||
|  | .alert-danger { | ||||||
|  |   color: #a94442; | ||||||
|  |   background-color: #f2dede; | ||||||
|  |   border-color: #ebccd1; | ||||||
|  | } | ||||||
|  | .alert p { | ||||||
|  |   margin-bottom: 0; | ||||||
|  | } | ||||||
|  | .alert:empty { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | .invalid-feedback { | ||||||
|  |   color: #a94442; | ||||||
|  |   display: block; | ||||||
|  |   margin: -1rem 0 2rem; | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								assets/css/components.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								assets/css/components.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | @layer components { | ||||||
|  |   .input { | ||||||
|  |     @apply rounded-lg px-4 py-2; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								assets/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								assets/js/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | // We need to import the CSS so that webpack will load it. | ||||||
|  | // The MiniCssExtractPlugin is used to separate it out into | ||||||
|  | // its own CSS file. | ||||||
|  | import "../css/app.scss" | ||||||
|  |  | ||||||
|  | // webpack automatically bundles all modules in your | ||||||
|  | // entry points. Those entry points can be configured | ||||||
|  | // in "webpack.config.js". | ||||||
|  | // | ||||||
|  | // Import deps with the dep name or local files with a relative path, for example: | ||||||
|  | // | ||||||
|  | //     import {Socket} from "phoenix" | ||||||
|  | //     import socket from "./socket" | ||||||
|  | // | ||||||
|  | import "phoenix_html" | ||||||
|  | import {Socket} from "phoenix" | ||||||
|  | import topbar from "topbar" | ||||||
|  | import {LiveSocket} from "phoenix_live_view" | ||||||
|  |  | ||||||
|  | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") | ||||||
|  | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) | ||||||
|  |  | ||||||
|  | // Show progress bar on live navigation and form submits | ||||||
|  | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) | ||||||
|  | window.addEventListener("phx:page-loading-start", info => topbar.show()) | ||||||
|  | window.addEventListener("phx:page-loading-stop", info => topbar.hide()) | ||||||
|  |  | ||||||
|  | // connect if there are any LiveViews on the page | ||||||
|  | liveSocket.connect() | ||||||
|  |  | ||||||
|  | // expose liveSocket on window for web console debug logs and latency simulation: | ||||||
|  | // >> liveSocket.enableDebug() | ||||||
|  | // >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session | ||||||
|  | // >> liveSocket.disableLatencySim() | ||||||
|  | window.liveSocket = liveSocket | ||||||
|  |  | ||||||
							
								
								
									
										63
									
								
								assets/js/socket.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								assets/js/socket.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | // NOTE: The contents of this file will only be executed if | ||||||
|  | // you uncomment its entry in "assets/js/app.js". | ||||||
|  |  | ||||||
|  | // To use Phoenix channels, the first step is to import Socket, | ||||||
|  | // and connect at the socket path in "lib/web/endpoint.ex". | ||||||
|  | // | ||||||
|  | // Pass the token on params as below. Or remove it | ||||||
|  | // from the params if you are not using authentication. | ||||||
|  | import {Socket} from "phoenix" | ||||||
|  |  | ||||||
|  | let socket = new Socket("/socket", {params: {token: window.userToken}}) | ||||||
|  |  | ||||||
|  | // When you connect, you'll often need to authenticate the client. | ||||||
|  | // For example, imagine you have an authentication plug, `MyAuth`, | ||||||
|  | // which authenticates the session and assigns a `:current_user`. | ||||||
|  | // If the current user exists you can assign the user's token in | ||||||
|  | // the connection for use in the layout. | ||||||
|  | // | ||||||
|  | // In your "lib/web/router.ex": | ||||||
|  | // | ||||||
|  | //     pipeline :browser do | ||||||
|  | //       ... | ||||||
|  | //       plug MyAuth | ||||||
|  | //       plug :put_user_token | ||||||
|  | //     end | ||||||
|  | // | ||||||
|  | //     defp put_user_token(conn, _) do | ||||||
|  | //       if current_user = conn.assigns[:current_user] do | ||||||
|  | //         token = Phoenix.Token.sign(conn, "user socket", current_user.id) | ||||||
|  | //         assign(conn, :user_token, token) | ||||||
|  | //       else | ||||||
|  | //         conn | ||||||
|  | //       end | ||||||
|  | //     end | ||||||
|  | // | ||||||
|  | // Now you need to pass this token to JavaScript. You can do so | ||||||
|  | // inside a script tag in "lib/web/templates/layout/app.html.eex": | ||||||
|  | // | ||||||
|  | //     <script>window.userToken = "<%= assigns[:user_token] %>";</script> | ||||||
|  | // | ||||||
|  | // You will need to verify the user token in the "connect/3" function | ||||||
|  | // in "lib/web/channels/user_socket.ex": | ||||||
|  | // | ||||||
|  | //     def connect(%{"token" => token}, socket, _connect_info) do | ||||||
|  | //       # max_age: 1209600 is equivalent to two weeks in seconds | ||||||
|  | //       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do | ||||||
|  | //         {:ok, user_id} -> | ||||||
|  | //           {:ok, assign(socket, :user, user_id)} | ||||||
|  | //         {:error, reason} -> | ||||||
|  | //           :error | ||||||
|  | //       end | ||||||
|  | //     end | ||||||
|  | // | ||||||
|  | // Finally, connect to the socket: | ||||||
|  | socket.connect() | ||||||
|  |  | ||||||
|  | // Now that you are connected, you can join channels with a topic: | ||||||
|  | let channel = socket.channel("topic:subtopic", {}) | ||||||
|  | channel.join() | ||||||
|  |   .receive("ok", resp => { console.log("Joined successfully", resp) }) | ||||||
|  |   .receive("error", resp => { console.log("Unable to join", resp) }) | ||||||
|  |  | ||||||
|  | export default socket | ||||||
							
								
								
									
										10140
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10140
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										38
									
								
								assets/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								assets/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | { | ||||||
|  |   "repository": {}, | ||||||
|  |   "description": " ", | ||||||
|  |   "license": "MIT", | ||||||
|  |   "scripts": { | ||||||
|  |     "deploy": "NODE_ENV=production webpack --mode production", | ||||||
|  |     "watch": "webpack --mode development --watch --watch-options-stdin" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "phoenix": "file:../deps/phoenix", | ||||||
|  |     "phoenix_html": "file:../deps/phoenix_html", | ||||||
|  |     "phoenix_live_view": "file:../deps/phoenix_live_view", | ||||||
|  |     "topbar": "^0.1.4" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@babel/core": "^7.15.0", | ||||||
|  |     "@babel/preset-env": "^7.15.0", | ||||||
|  |     "autoprefixer": "^10.2.6", | ||||||
|  |     "babel-loader": "^8.2.2", | ||||||
|  |     "copy-webpack-plugin": "^9.0.0", | ||||||
|  |     "css-loader": "^5.2.7", | ||||||
|  |     "css-minimizer-webpack-plugin": "^3.0.1", | ||||||
|  |     "hard-source-webpack-plugin": "^0.13.1", | ||||||
|  |     "mini-css-extract-plugin": "^1.6.0", | ||||||
|  |     "node-sass": "^6.0.0", | ||||||
|  |     "postcss": "^8.3.6", | ||||||
|  |     "postcss-import": "^14.0.2", | ||||||
|  |     "postcss-loader": "^6.1.1", | ||||||
|  |     "postcss-preset-env": "^6.7.0", | ||||||
|  |     "sass-loader": "^12.1.0", | ||||||
|  |     "style-loader": "^3.2.1", | ||||||
|  |     "tailwindcss": "^2.2.7", | ||||||
|  |     "terser-webpack-plugin": "^5.1.3", | ||||||
|  |     "webpack": "^5.50.0", | ||||||
|  |     "webpack-cli": "^4.8.0", | ||||||
|  |     "webpack-dev-server": "^3.11.2" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								assets/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								assets/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | module.exports = { | ||||||
|  | 	plugins: { | ||||||
|  | 		"postcss-import": {}, | ||||||
|  | 		tailwindcss: {}, | ||||||
|  | 		autoprefixer: {}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/static/images/phoenix.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/static/images/phoenix.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										5
									
								
								assets/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								assets/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file | ||||||
|  | # | ||||||
|  | # To ban all spiders from the entire site uncomment the next two lines: | ||||||
|  | # User-agent: * | ||||||
|  | # Disallow: / | ||||||
							
								
								
									
										31
									
								
								assets/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								assets/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | const colors = require('tailwindcss/colors') | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   purge: [ | ||||||
|  |     '../lib/**/*.ex', | ||||||
|  |     '../lib/**/*.leex', | ||||||
|  |     '../lib/**/*.eex', | ||||||
|  |     './js/**/*.js' | ||||||
|  |   ], | ||||||
|  |   darkMode: "media", | ||||||
|  |   theme: { | ||||||
|  |     colors: { | ||||||
|  |       transparent: 'transparent', | ||||||
|  |       current: 'currentColor', | ||||||
|  |        | ||||||
|  |       primary: colors.indigo, | ||||||
|  |        | ||||||
|  |       black: colors.black, | ||||||
|  |       white: colors.white, | ||||||
|  |       gray: colors.trueGray, | ||||||
|  |       indigo: colors.indigo, | ||||||
|  |       red: colors.rose, | ||||||
|  |       yellow: colors.amber, | ||||||
|  |     }, | ||||||
|  |     extend: {}, | ||||||
|  |   }, | ||||||
|  |   variants: { | ||||||
|  |     extend: {}, | ||||||
|  |   }, | ||||||
|  |   plugins: [], | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								assets/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								assets/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const glob = require("glob"); | ||||||
|  | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | ||||||
|  | const TerserPlugin = require("terser-webpack-plugin"); | ||||||
|  | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); | ||||||
|  | const CopyWebpackPlugin = require("copy-webpack-plugin"); | ||||||
|  |  | ||||||
|  | module.exports = (env, options) => { | ||||||
|  |   const devMode = options.mode !== "production"; | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     optimization: { | ||||||
|  |       minimizer: [ | ||||||
|  |         new TerserPlugin({ parallel: true, extractComments: true }), | ||||||
|  |         new CssMinimizerPlugin({}) | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     entry: { | ||||||
|  |       app: glob.sync("./vendor/**/*.js").concat(["./js/app.js"]) | ||||||
|  |     }, | ||||||
|  |     output: { | ||||||
|  |       filename: "[name].js", | ||||||
|  |       path: path.resolve(__dirname, "../priv/static/js"), | ||||||
|  |       publicPath: "/js/" | ||||||
|  |     }, | ||||||
|  |     devtool: devMode ? "eval-cheap-module-source-map" : undefined, | ||||||
|  |     module: { | ||||||
|  |       rules: [ | ||||||
|  |         { | ||||||
|  |           test: /\.js$/, | ||||||
|  |           exclude: /node_modules/, | ||||||
|  |           use: { | ||||||
|  |             loader: "babel-loader" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           test: /\.s?css$/, | ||||||
|  |           use: [ | ||||||
|  |             MiniCssExtractPlugin.loader, | ||||||
|  |             "css-loader", | ||||||
|  |             "postcss-loader", | ||||||
|  |             "sass-loader", | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     plugins: [ | ||||||
|  |       new MiniCssExtractPlugin({ filename: "../css/app.css" }), | ||||||
|  |       new CopyWebpackPlugin({ | ||||||
|  |         patterns: [{ from: "static/", to: "../" }] | ||||||
|  |       }) | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										4587
									
								
								assets/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4587
									
								
								assets/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										31
									
								
								config/config.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								config/config.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | # This file is responsible for configuring your application | ||||||
|  | # and its dependencies with the aid of the Mix.Config module. | ||||||
|  | # | ||||||
|  | # This configuration file is loaded before any dependency and | ||||||
|  | # is restricted to this project. | ||||||
|  |  | ||||||
|  | # General application configuration | ||||||
|  | use Mix.Config | ||||||
|  |  | ||||||
|  | config :lokal, | ||||||
|  |   ecto_repos: [Lokal.Repo] | ||||||
|  |  | ||||||
|  | # Configures the endpoint | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   url: [host: "localhost"], | ||||||
|  |   secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I", | ||||||
|  |   render_errors: [view: LokalWeb.ErrorView, accepts: ~w(html json), layout: false], | ||||||
|  |   pubsub_server: Lokal.PubSub, | ||||||
|  |   live_view: [signing_salt: "zOLgd3lr"] | ||||||
|  |  | ||||||
|  | # Configures Elixir's Logger | ||||||
|  | config :logger, :console, | ||||||
|  |   format: "$time $metadata[$level] $message\n", | ||||||
|  |   metadata: [:request_id] | ||||||
|  |  | ||||||
|  | # Use Jason for JSON parsing in Phoenix | ||||||
|  | config :phoenix, :json_library, Jason | ||||||
|  |  | ||||||
|  | # Import environment specific config. This must remain at the bottom | ||||||
|  | # of this file so it overrides the configuration defined above. | ||||||
|  | import_config "#{Mix.env()}.exs" | ||||||
							
								
								
									
										77
									
								
								config/dev.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								config/dev.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | use Mix.Config | ||||||
|  |  | ||||||
|  | # Configure your database | ||||||
|  | config :lokal, Lokal.Repo, | ||||||
|  |   username: "postgres", | ||||||
|  |   password: "postgres", | ||||||
|  |   database: "lokal_dev", | ||||||
|  |   hostname: "localhost", | ||||||
|  |   show_sensitive_data_on_connection_error: true, | ||||||
|  |   pool_size: 10 | ||||||
|  |  | ||||||
|  | # For development, we disable any cache and enable | ||||||
|  | # debugging and code reloading. | ||||||
|  | # | ||||||
|  | # The watchers configuration can be used to run external | ||||||
|  | # watchers to your application. For example, we use it | ||||||
|  | # with webpack to recompile .js and .css sources. | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   http: [port: 4000], | ||||||
|  |   debug_errors: true, | ||||||
|  |   code_reloader: true, | ||||||
|  |   check_origin: false, | ||||||
|  |   watchers: [ | ||||||
|  |     node: [ | ||||||
|  |       "node_modules/webpack/bin/webpack.js", | ||||||
|  |       "--mode", | ||||||
|  |       "development", | ||||||
|  |       "--watch", | ||||||
|  |       "--watch-options-stdin", | ||||||
|  |       cd: Path.expand("../assets", __DIR__) | ||||||
|  |     ] | ||||||
|  |   ] | ||||||
|  |  | ||||||
|  | # ## SSL Support | ||||||
|  | # | ||||||
|  | # In order to use HTTPS in development, a self-signed | ||||||
|  | # certificate can be generated by running the following | ||||||
|  | # Mix task: | ||||||
|  | # | ||||||
|  | #     mix phx.gen.cert | ||||||
|  | # | ||||||
|  | # Note that this task requires Erlang/OTP 20 or later. | ||||||
|  | # Run `mix help phx.gen.cert` for more information. | ||||||
|  | # | ||||||
|  | # The `http:` config above can be replaced with: | ||||||
|  | # | ||||||
|  | #     https: [ | ||||||
|  | #       port: 4001, | ||||||
|  | #       cipher_suite: :strong, | ||||||
|  | #       keyfile: "priv/cert/selfsigned_key.pem", | ||||||
|  | #       certfile: "priv/cert/selfsigned.pem" | ||||||
|  | #     ], | ||||||
|  | # | ||||||
|  | # If desired, both `http:` and `https:` keys can be | ||||||
|  | # configured to run both http and https servers on | ||||||
|  | # different ports. | ||||||
|  |  | ||||||
|  | # Watch static and templates for browser reloading. | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   live_reload: [ | ||||||
|  |     patterns: [ | ||||||
|  |       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", | ||||||
|  |       ~r"priv/gettext/.*(po)$", | ||||||
|  |       ~r"lib/lokal_web/.*(live|views)/.*(ex|leex)$", | ||||||
|  |       ~r"lib/lokal_web/templates/.*(eex|leex)$" | ||||||
|  |     ] | ||||||
|  |   ] | ||||||
|  |  | ||||||
|  | # Do not include metadata nor timestamps in development logs | ||||||
|  | config :logger, :console, format: "[$level] $message\n" | ||||||
|  |  | ||||||
|  | # Set a higher stacktrace during development. Avoid configuring such | ||||||
|  | # in production as building large stacktraces may be expensive. | ||||||
|  | config :phoenix, :stacktrace_depth, 20 | ||||||
|  |  | ||||||
|  | # Initialize plugs at runtime for faster development compilation | ||||||
|  | config :phoenix, :plug_init_mode, :runtime | ||||||
							
								
								
									
										55
									
								
								config/prod.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								config/prod.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | use Mix.Config | ||||||
|  |  | ||||||
|  | # For production, don't forget to configure the url host | ||||||
|  | # to something meaningful, Phoenix uses this information | ||||||
|  | # when generating URLs. | ||||||
|  | # | ||||||
|  | # Note we also include the path to a cache manifest | ||||||
|  | # containing the digested version of static files. This | ||||||
|  | # manifest is generated by the `mix phx.digest` task, | ||||||
|  | # which you should run after static files are built and | ||||||
|  | # before starting your production server. | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   url: [host: "example.com", port: 80], | ||||||
|  |   cache_static_manifest: "priv/static/cache_manifest.json" | ||||||
|  |  | ||||||
|  | # Do not print debug messages in production | ||||||
|  | config :logger, level: :info | ||||||
|  |  | ||||||
|  | # ## SSL Support | ||||||
|  | # | ||||||
|  | # To get SSL working, you will need to add the `https` key | ||||||
|  | # to the previous section and set your `:url` port to 443: | ||||||
|  | # | ||||||
|  | #     config :lokal, LokalWeb.Endpoint, | ||||||
|  | #       ... | ||||||
|  | #       url: [host: "example.com", port: 443], | ||||||
|  | #       https: [ | ||||||
|  | #         port: 443, | ||||||
|  | #         cipher_suite: :strong, | ||||||
|  | #         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), | ||||||
|  | #         certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), | ||||||
|  | #         transport_options: [socket_opts: [:inet6]] | ||||||
|  | #       ] | ||||||
|  | # | ||||||
|  | # The `cipher_suite` is set to `:strong` to support only the | ||||||
|  | # latest and more secure SSL ciphers. This means old browsers | ||||||
|  | # and clients may not be supported. You can set it to | ||||||
|  | # `:compatible` for wider support. | ||||||
|  | # | ||||||
|  | # `:keyfile` and `:certfile` expect an absolute path to the key | ||||||
|  | # and cert in disk or a relative path inside priv, for example | ||||||
|  | # "priv/ssl/server.key". For all supported SSL configuration | ||||||
|  | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 | ||||||
|  | # | ||||||
|  | # We also recommend setting `force_ssl` in your endpoint, ensuring | ||||||
|  | # no data is ever sent via http, always redirecting to https: | ||||||
|  | # | ||||||
|  | #     config :lokal, LokalWeb.Endpoint, | ||||||
|  | #       force_ssl: [hsts: true] | ||||||
|  | # | ||||||
|  | # Check `Plug.SSL` for all available options in `force_ssl`. | ||||||
|  |  | ||||||
|  | # Finally import the config/prod.secret.exs which loads secrets | ||||||
|  | # and configuration from environment variables. | ||||||
|  | import_config "prod.secret.exs" | ||||||
							
								
								
									
										41
									
								
								config/prod.secret.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								config/prod.secret.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | # In this file, we load production configuration and secrets | ||||||
|  | # from environment variables. You can also hardcode secrets, | ||||||
|  | # although such is generally not recommended and you have to | ||||||
|  | # remember to add this file to your .gitignore. | ||||||
|  | use Mix.Config | ||||||
|  |  | ||||||
|  | database_url = | ||||||
|  |   System.get_env("DATABASE_URL") || | ||||||
|  |     raise """ | ||||||
|  |     environment variable DATABASE_URL is missing. | ||||||
|  |     For example: ecto://USER:PASS@HOST/DATABASE | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  | config :lokal, Lokal.Repo, | ||||||
|  |   # ssl: true, | ||||||
|  |   url: database_url, | ||||||
|  |   pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") | ||||||
|  |  | ||||||
|  | secret_key_base = | ||||||
|  |   System.get_env("SECRET_KEY_BASE") || | ||||||
|  |     raise """ | ||||||
|  |     environment variable SECRET_KEY_BASE is missing. | ||||||
|  |     You can generate one by calling: mix phx.gen.secret | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   http: [ | ||||||
|  |     port: String.to_integer(System.get_env("PORT") || "4000"), | ||||||
|  |     transport_options: [socket_opts: [:inet6]] | ||||||
|  |   ], | ||||||
|  |   secret_key_base: secret_key_base | ||||||
|  |  | ||||||
|  | # ## Using releases (Elixir v1.9+) | ||||||
|  | # | ||||||
|  | # If you are doing OTP releases, you need to instruct Phoenix | ||||||
|  | # to start each relevant endpoint: | ||||||
|  | # | ||||||
|  | #     config :lokal, LokalWeb.Endpoint, server: true | ||||||
|  | # | ||||||
|  | # Then you can assemble a release by calling `mix release`. | ||||||
|  | # See `mix help release` for more information. | ||||||
							
								
								
									
										25
									
								
								config/test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								config/test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | use Mix.Config | ||||||
|  |  | ||||||
|  | # Only in tests, remove the complexity from the password hashing algorithm | ||||||
|  | config :bcrypt_elixir, :log_rounds, 1 | ||||||
|  |  | ||||||
|  | # Configure your database | ||||||
|  | # | ||||||
|  | # The MIX_TEST_PARTITION environment variable can be used | ||||||
|  | # to provide built-in test partitioning in CI environment. | ||||||
|  | # Run `mix help test` for more information. | ||||||
|  | config :lokal, Lokal.Repo, | ||||||
|  |   username: "postgres", | ||||||
|  |   password: "postgres", | ||||||
|  |   database: "lokal_test#{System.get_env("MIX_TEST_PARTITION")}", | ||||||
|  |   hostname: "localhost", | ||||||
|  |   pool: Ecto.Adapters.SQL.Sandbox | ||||||
|  |  | ||||||
|  | # We don't run a server during test. If one is required, | ||||||
|  | # you can enable the server option below. | ||||||
|  | config :lokal, LokalWeb.Endpoint, | ||||||
|  |   http: [port: 4002], | ||||||
|  |   server: false | ||||||
|  |  | ||||||
|  | # Print only warnings and errors during test | ||||||
|  | config :logger, level: :warn | ||||||
							
								
								
									
										9
									
								
								lib/lokal.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/lokal.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | defmodule Lokal do | ||||||
|  |   @moduledoc """ | ||||||
|  |   Lokal keeps the contexts that define your domain | ||||||
|  |   and business logic. | ||||||
|  |  | ||||||
|  |   Contexts are also responsible for managing your data, regardless | ||||||
|  |   if it comes from the database, an external API or others. | ||||||
|  |   """ | ||||||
|  | end | ||||||
							
								
								
									
										349
									
								
								lib/lokal/accounts.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								lib/lokal/accounts.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,349 @@ | |||||||
|  | defmodule Lokal.Accounts do | ||||||
|  |   @moduledoc """ | ||||||
|  |   The Accounts context. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   import Ecto.Query, warn: false | ||||||
|  |   alias Lokal.Repo | ||||||
|  |   alias Lokal.Accounts.{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) | ||||||
|  |   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) | ||||||
|  |   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.user_and_contexts_query(user, [context])) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Delivers the update email instructions to the given user. | ||||||
|  |  | ||||||
|  |   ## Examples | ||||||
|  |  | ||||||
|  |       iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1)) | ||||||
|  |       {:ok, %{to: ..., body: ...}} | ||||||
|  |  | ||||||
|  |   """ | ||||||
|  |   def deliver_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.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_session_token(token) do | ||||||
|  |     Repo.delete_all(UserToken.token_and_context_query(token, "session")) | ||||||
|  |     :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   ## Confirmation | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Delivers the confirmation email instructions to the given user. | ||||||
|  |  | ||||||
|  |   ## Examples | ||||||
|  |  | ||||||
|  |       iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :confirm, &1)) | ||||||
|  |       {:ok, %{to: ..., body: ...}} | ||||||
|  |  | ||||||
|  |       iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :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.user_and_contexts_query(user, ["confirm"])) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   ## Reset password | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Delivers the reset password email to the given user. | ||||||
|  |  | ||||||
|  |   ## Examples | ||||||
|  |  | ||||||
|  |       iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &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.user_and_contexts_query(user, :all)) | ||||||
|  |     |> Repo.transaction() | ||||||
|  |     |> case do | ||||||
|  |       {:ok, %{user: user}} -> {:ok, user} | ||||||
|  |       {:error, :user, changeset, _} -> {:error, changeset} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										141
									
								
								lib/lokal/accounts/user.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								lib/lokal/accounts/user.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | defmodule Lokal.Accounts.User do | ||||||
|  |   use Ecto.Schema | ||||||
|  |   import Ecto.Changeset | ||||||
|  |  | ||||||
|  |   @derive {Inspect, except: [:password]} | ||||||
|  |   @primary_key {:id, :binary_id, autogenerate: true} | ||||||
|  |   @foreign_key_type :binary_id | ||||||
|  |   schema "users" do | ||||||
|  |     field :email, :string | ||||||
|  |     field :password, :string, virtual: true | ||||||
|  |     field :hashed_password, :string | ||||||
|  |     field :confirmed_at, :naive_datetime | ||||||
|  |  | ||||||
|  |     timestamps() | ||||||
|  |   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`. | ||||||
|  |   """ | ||||||
|  |   def registration_changeset(user, attrs, opts \\ []) do | ||||||
|  |     user | ||||||
|  |     |> cast(attrs, [:email, :password]) | ||||||
|  |     |> validate_email() | ||||||
|  |     |> validate_password(opts) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp validate_email(changeset) do | ||||||
|  |     changeset | ||||||
|  |     |> validate_required([:email]) | ||||||
|  |     |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") | ||||||
|  |     |> validate_length(:email, max: 160) | ||||||
|  |     |> unsafe_validate_unique(:email, Lokal.Repo) | ||||||
|  |     |> unique_constraint(:email) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp validate_password(changeset, opts) do | ||||||
|  |     changeset | ||||||
|  |     |> validate_required([:password]) | ||||||
|  |     |> validate_length(:password, min: 12, max: 80) | ||||||
|  |     # |> 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 | ||||||
|  |       |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password)) | ||||||
|  |       |> delete_change(:password) | ||||||
|  |     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) do | ||||||
|  |     user | ||||||
|  |     |> cast(attrs, [:email]) | ||||||
|  |     |> validate_email() | ||||||
|  |     |> 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 = NaiveDateTime.utc_now() |> NaiveDateTime.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?(%Lokal.Accounts.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 | ||||||
|  |     if valid_password?(changeset.data, password) do | ||||||
|  |       changeset | ||||||
|  |     else | ||||||
|  |       add_error(changeset, :current_password, "is not valid") | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										73
									
								
								lib/lokal/accounts/user_notifier.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/lokal/accounts/user_notifier.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | defmodule Lokal.Accounts.UserNotifier do | ||||||
|  |   # For simplicity, this module simply logs messages to the terminal. | ||||||
|  |   # You should replace it by a proper email or notification tool, such as: | ||||||
|  |   # | ||||||
|  |   #   * Swoosh - https://hexdocs.pm/swoosh | ||||||
|  |   #   * Bamboo - https://hexdocs.pm/bamboo | ||||||
|  |   # | ||||||
|  |   defp deliver(to, body) do | ||||||
|  |     require Logger | ||||||
|  |     Logger.debug(body) | ||||||
|  |     {:ok, %{to: to, body: body}} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Deliver instructions to confirm account. | ||||||
|  |   """ | ||||||
|  |   def deliver_confirmation_instructions(user, url) do | ||||||
|  |     deliver(user.email, """ | ||||||
|  |  | ||||||
|  |     ============================== | ||||||
|  |  | ||||||
|  |     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, """ | ||||||
|  |  | ||||||
|  |     ============================== | ||||||
|  |  | ||||||
|  |     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, """ | ||||||
|  |  | ||||||
|  |     ============================== | ||||||
|  |  | ||||||
|  |     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 | ||||||
							
								
								
									
										141
									
								
								lib/lokal/accounts/user_token.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								lib/lokal/accounts/user_token.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | defmodule Lokal.Accounts.UserToken do | ||||||
|  |   use Ecto.Schema | ||||||
|  |   import Ecto.Query | ||||||
|  |  | ||||||
|  |   @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 | ||||||
|  |  | ||||||
|  |   @primary_key {:id, :binary_id, autogenerate: true} | ||||||
|  |   @foreign_key_type :binary_id | ||||||
|  |   schema "users_tokens" do | ||||||
|  |     field :token, :binary | ||||||
|  |     field :context, :string | ||||||
|  |     field :sent_to, :string | ||||||
|  |     belongs_to :user, Lokal.Accounts.User | ||||||
|  |  | ||||||
|  |     timestamps(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. | ||||||
|  |   """ | ||||||
|  |   def build_session_token(user) do | ||||||
|  |     token = :crypto.strong_rand_bytes(@rand_size) | ||||||
|  |     {token, %Lokal.Accounts.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. | ||||||
|  |   """ | ||||||
|  |   def verify_session_token_query(token) do | ||||||
|  |     query = | ||||||
|  |       from token in 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 with a hashed counter part. | ||||||
|  |  | ||||||
|  |   The non-hashed token is sent to the user email while the | ||||||
|  |   hashed part is stored in the database, to avoid reconstruction. | ||||||
|  |   The token is valid for a week as long as users don't change | ||||||
|  |   their email. | ||||||
|  |   """ | ||||||
|  |   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), | ||||||
|  |      %Lokal.Accounts.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. | ||||||
|  |   """ | ||||||
|  |   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 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 token record. | ||||||
|  |   """ | ||||||
|  |   def verify_change_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) | ||||||
|  |  | ||||||
|  |         query = | ||||||
|  |           from token in 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 given token with the given context. | ||||||
|  |   """ | ||||||
|  |   def token_and_context_query(token, context) do | ||||||
|  |     from Lokal.Accounts.UserToken, where: [token: ^token, context: ^context] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Gets all tokens for the given user for the given contexts. | ||||||
|  |   """ | ||||||
|  |   def user_and_contexts_query(user, :all) do | ||||||
|  |     from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def user_and_contexts_query(user, [_ | _] = contexts) do | ||||||
|  |     from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										34
									
								
								lib/lokal/application.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/lokal/application.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | defmodule Lokal.Application do | ||||||
|  |   # See https://hexdocs.pm/elixir/Application.html | ||||||
|  |   # for more information on OTP Applications | ||||||
|  |   @moduledoc false | ||||||
|  |  | ||||||
|  |   use Application | ||||||
|  |  | ||||||
|  |   def start(_type, _args) do | ||||||
|  |     children = [ | ||||||
|  |       # Start the Ecto repository | ||||||
|  |       Lokal.Repo, | ||||||
|  |       # Start the Telemetry supervisor | ||||||
|  |       LokalWeb.Telemetry, | ||||||
|  |       # Start the PubSub system | ||||||
|  |       {Phoenix.PubSub, name: Lokal.PubSub}, | ||||||
|  |       # Start the Endpoint (http/https) | ||||||
|  |       LokalWeb.Endpoint | ||||||
|  |       # Start a worker by calling: Lokal.Worker.start_link(arg) | ||||||
|  |       # {Lokal.Worker, arg} | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     # See https://hexdocs.pm/elixir/Supervisor.html | ||||||
|  |     # for other strategies and supported options | ||||||
|  |     opts = [strategy: :one_for_one, name: Lokal.Supervisor] | ||||||
|  |     Supervisor.start_link(children, opts) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Tell Phoenix to update the endpoint configuration | ||||||
|  |   # whenever the application is updated. | ||||||
|  |   def config_change(changed, _new, removed) do | ||||||
|  |     LokalWeb.Endpoint.config_change(changed, removed) | ||||||
|  |     :ok | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										5
									
								
								lib/lokal/repo.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal/repo.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | defmodule Lokal.Repo do | ||||||
|  |   use Ecto.Repo, | ||||||
|  |     otp_app: :lokal, | ||||||
|  |     adapter: Ecto.Adapters.Postgres | ||||||
|  | end | ||||||
							
								
								
									
										102
									
								
								lib/lokal_web.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/lokal_web.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | defmodule LokalWeb do | ||||||
|  |   @moduledoc """ | ||||||
|  |   The entrypoint for defining your web interface, such | ||||||
|  |   as controllers, views, channels and so on. | ||||||
|  |  | ||||||
|  |   This can be used in your application as: | ||||||
|  |  | ||||||
|  |       use LokalWeb, :controller | ||||||
|  |       use LokalWeb, :view | ||||||
|  |  | ||||||
|  |   The definitions below will be executed for every view, | ||||||
|  |   controller, etc, so keep them short and clean, focused | ||||||
|  |   on imports, uses and aliases. | ||||||
|  |  | ||||||
|  |   Do NOT define functions inside the quoted expressions | ||||||
|  |   below. Instead, define any helper function in modules | ||||||
|  |   and import those modules here. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   def controller do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.Controller, namespace: LokalWeb | ||||||
|  |  | ||||||
|  |       import Plug.Conn | ||||||
|  |       import LokalWeb.Gettext | ||||||
|  |       alias LokalWeb.Router.Helpers, as: Routes | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def view do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.View, | ||||||
|  |         root: "lib/lokal_web/templates", | ||||||
|  |         namespace: LokalWeb | ||||||
|  |  | ||||||
|  |       # Import convenience functions from controllers | ||||||
|  |       import Phoenix.Controller, | ||||||
|  |         only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] | ||||||
|  |  | ||||||
|  |       # Include shared imports and aliases for views | ||||||
|  |       unquote(view_helpers()) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def live_view do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.LiveView, | ||||||
|  |         layout: {LokalWeb.LayoutView, "live.html"} | ||||||
|  |  | ||||||
|  |       unquote(view_helpers()) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def live_component do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.LiveComponent | ||||||
|  |  | ||||||
|  |       unquote(view_helpers()) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def router do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.Router | ||||||
|  |  | ||||||
|  |       import Plug.Conn | ||||||
|  |       import Phoenix.Controller | ||||||
|  |       import Phoenix.LiveView.Router | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def channel do | ||||||
|  |     quote do | ||||||
|  |       use Phoenix.Channel | ||||||
|  |       import LokalWeb.Gettext | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp view_helpers do | ||||||
|  |     quote do | ||||||
|  |       # Use all HTML functionality (forms, tags, etc) | ||||||
|  |       use Phoenix.HTML | ||||||
|  |  | ||||||
|  |       # Import LiveView helpers (live_render, live_component, live_patch, etc) | ||||||
|  |       import Phoenix.LiveView.Helpers | ||||||
|  |  | ||||||
|  |       # Import basic rendering functionality (render, render_layout, etc) | ||||||
|  |       import Phoenix.View | ||||||
|  |  | ||||||
|  |       import LokalWeb.ErrorHelpers | ||||||
|  |       import LokalWeb.Gettext | ||||||
|  |       alias LokalWeb.Router.Helpers, as: Routes | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   When used, dispatch to the appropriate controller/view/etc. | ||||||
|  |   """ | ||||||
|  |   defmacro __using__(which) when is_atom(which) do | ||||||
|  |     apply(__MODULE__, which, []) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										35
									
								
								lib/lokal_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								lib/lokal_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | defmodule LokalWeb.UserSocket do | ||||||
|  |   use Phoenix.Socket | ||||||
|  |  | ||||||
|  |   ## Channels | ||||||
|  |   # channel "room:*", LokalWeb.RoomChannel | ||||||
|  |  | ||||||
|  |   # Socket params are passed from the client and can | ||||||
|  |   # be used to verify and authenticate a user. After | ||||||
|  |   # verification, you can put default assigns into | ||||||
|  |   # the socket that will be set for all channels, ie | ||||||
|  |   # | ||||||
|  |   #     {:ok, assign(socket, :user_id, verified_user_id)} | ||||||
|  |   # | ||||||
|  |   # To deny connection, return `:error`. | ||||||
|  |   # | ||||||
|  |   # See `Phoenix.Token` documentation for examples in | ||||||
|  |   # performing token verification on connect. | ||||||
|  |   @impl true | ||||||
|  |   def connect(_params, socket, _connect_info) do | ||||||
|  |     {:ok, socket} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Socket id's are topics that allow you to identify all sockets for a given user: | ||||||
|  |   # | ||||||
|  |   #     def id(socket), do: "user_socket:#{socket.assigns.user_id}" | ||||||
|  |   # | ||||||
|  |   # Would allow you to broadcast a "disconnect" event and terminate | ||||||
|  |   # all active sockets and channels for a given user: | ||||||
|  |   # | ||||||
|  |   #     LokalWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) | ||||||
|  |   # | ||||||
|  |   # Returning `nil` makes this socket anonymous. | ||||||
|  |   @impl true | ||||||
|  |   def id(_socket), do: nil | ||||||
|  | end | ||||||
							
								
								
									
										7
									
								
								lib/lokal_web/controllers/page_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/lokal_web/controllers/page_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | defmodule LokalWeb.PageController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   def index(conn, _params) do | ||||||
|  |     render(conn, "index.html") | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										149
									
								
								lib/lokal_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								lib/lokal_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | defmodule LokalWeb.UserAuth do | ||||||
|  |   import Plug.Conn | ||||||
|  |   import Phoenix.Controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias LokalWeb.Router.Helpers, as: Routes | ||||||
|  |  | ||||||
|  |   # 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 "_lokal_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 = Accounts.generate_user_session_token(user) | ||||||
|  |     user_return_to = get_session(conn, :user_return_to) | ||||||
|  |  | ||||||
|  |     conn | ||||||
|  |     |> renew_session() | ||||||
|  |     |> put_session(:user_token, token) | ||||||
|  |     |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(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 | ||||||
|  |     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 && Accounts.delete_session_token(user_token) | ||||||
|  |  | ||||||
|  |     if live_socket_id = get_session(conn, :live_socket_id) do | ||||||
|  |       LokalWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     conn | ||||||
|  |     |> renew_session() | ||||||
|  |     |> delete_resp_cookie(@remember_me_cookie) | ||||||
|  |     |> redirect(to: "/") | ||||||
|  |   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 && Accounts.get_user_by_session_token(user_token) | ||||||
|  |     assign(conn, :current_user, user) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp ensure_user_token(conn) do | ||||||
|  |     if user_token = get_session(conn, :user_token) do | ||||||
|  |       {user_token, conn} | ||||||
|  |     else | ||||||
|  |       conn = fetch_cookies(conn, signed: [@remember_me_cookie]) | ||||||
|  |  | ||||||
|  |       if user_token = conn.cookies[@remember_me_cookie] do | ||||||
|  |         {user_token, put_session(conn, :user_token, user_token)} | ||||||
|  |       else | ||||||
|  |         {nil, conn} | ||||||
|  |       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: Routes.user_session_path(conn, :new)) | ||||||
|  |       |> halt() | ||||||
|  |     end | ||||||
|  |   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: "/" | ||||||
|  | end | ||||||
							
								
								
									
										53
									
								
								lib/lokal_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/lokal_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | defmodule LokalWeb.UserConfirmationController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |  | ||||||
|  |   def new(conn, _params) do | ||||||
|  |     render(conn, "new.html") | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create(conn, %{"user" => %{"email" => email}}) do | ||||||
|  |     if user = Accounts.get_user_by_email(email) do | ||||||
|  |       Accounts.deliver_user_confirmation_instructions( | ||||||
|  |         user, | ||||||
|  |         &Routes.user_confirmation_url(conn, :confirm, &1) | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     # Regardless of the outcome, show an impartial success/error message. | ||||||
|  |     conn | ||||||
|  |     |> put_flash( | ||||||
|  |       :info, | ||||||
|  |       "If your email is in our system and it has not been confirmed yet, " <> | ||||||
|  |         "you will receive an email with instructions shortly." | ||||||
|  |     ) | ||||||
|  |     |> redirect(to: "/") | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Do not log in the user after confirmation to avoid a | ||||||
|  |   # leaked token giving the user access to the account. | ||||||
|  |   def confirm(conn, %{"token" => token}) do | ||||||
|  |     case Accounts.confirm_user(token) do | ||||||
|  |       {:ok, _} -> | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:info, "User confirmed successfully.") | ||||||
|  |         |> redirect(to: "/") | ||||||
|  |  | ||||||
|  |       :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 conn.assigns do | ||||||
|  |           %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> | ||||||
|  |             redirect(conn, to: "/") | ||||||
|  |  | ||||||
|  |           %{} -> | ||||||
|  |             conn | ||||||
|  |             |> put_flash(:error, "User confirmation link is invalid or it has expired.") | ||||||
|  |             |> redirect(to: "/") | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										30
									
								
								lib/lokal_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/lokal_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | defmodule LokalWeb.UserRegistrationController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias Lokal.Accounts.User | ||||||
|  |   alias LokalWeb.UserAuth | ||||||
|  |  | ||||||
|  |   def new(conn, _params) do | ||||||
|  |     changeset = Accounts.change_user_registration(%User{}) | ||||||
|  |     render(conn, "new.html", changeset: changeset) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create(conn, %{"user" => user_params}) do | ||||||
|  |     case Accounts.register_user(user_params) do | ||||||
|  |       {:ok, user} -> | ||||||
|  |         {:ok, _} = | ||||||
|  |           Accounts.deliver_user_confirmation_instructions( | ||||||
|  |             user, | ||||||
|  |             &Routes.user_confirmation_url(conn, :confirm, &1) | ||||||
|  |           ) | ||||||
|  |  | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:info, "User created successfully.") | ||||||
|  |         |> UserAuth.log_in_user(user) | ||||||
|  |  | ||||||
|  |       {:error, %Ecto.Changeset{} = changeset} -> | ||||||
|  |         render(conn, "new.html", changeset: changeset) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										59
									
								
								lib/lokal_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								lib/lokal_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | defmodule LokalWeb.UserResetPasswordController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |  | ||||||
|  |   plug :get_user_by_reset_password_token when action in [:edit, :update] | ||||||
|  |  | ||||||
|  |   def new(conn, _params) do | ||||||
|  |     render(conn, "new.html") | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create(conn, %{"user" => %{"email" => email}}) do | ||||||
|  |     if user = Accounts.get_user_by_email(email) do | ||||||
|  |       Accounts.deliver_user_reset_password_instructions( | ||||||
|  |         user, | ||||||
|  |         &Routes.user_reset_password_url(conn, :edit, &1) | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     # Regardless of the outcome, show an impartial success/error message. | ||||||
|  |     conn | ||||||
|  |     |> put_flash( | ||||||
|  |       :info, | ||||||
|  |       "If your email is in our system, you will receive instructions to reset your password shortly." | ||||||
|  |     ) | ||||||
|  |     |> redirect(to: "/") | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def edit(conn, _params) do | ||||||
|  |     render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user)) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Do not log in the user after reset password to avoid a | ||||||
|  |   # leaked token giving the user access to the account. | ||||||
|  |   def update(conn, %{"user" => user_params}) do | ||||||
|  |     case Accounts.reset_user_password(conn.assigns.user, user_params) do | ||||||
|  |       {:ok, _} -> | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:info, "Password reset successfully.") | ||||||
|  |         |> redirect(to: Routes.user_session_path(conn, :new)) | ||||||
|  |  | ||||||
|  |       {:error, changeset} -> | ||||||
|  |         render(conn, "edit.html", changeset: changeset) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp get_user_by_reset_password_token(conn, _opts) do | ||||||
|  |     %{"token" => token} = conn.params | ||||||
|  |  | ||||||
|  |     if user = Accounts.get_user_by_reset_password_token(token) do | ||||||
|  |       conn |> assign(:user, user) |> assign(:token, token) | ||||||
|  |     else | ||||||
|  |       conn | ||||||
|  |       |> put_flash(:error, "Reset password link is invalid or it has expired.") | ||||||
|  |       |> redirect(to: "/") | ||||||
|  |       |> halt() | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										26
									
								
								lib/lokal_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | defmodule LokalWeb.UserSessionController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias LokalWeb.UserAuth | ||||||
|  |  | ||||||
|  |   def new(conn, _params) do | ||||||
|  |     render(conn, "new.html", error_message: nil) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create(conn, %{"user" => user_params}) do | ||||||
|  |     %{"email" => email, "password" => password} = user_params | ||||||
|  |  | ||||||
|  |     if user = Accounts.get_user_by_email_and_password(email, password) do | ||||||
|  |       UserAuth.log_in_user(conn, user, user_params) | ||||||
|  |     else | ||||||
|  |       render(conn, "new.html", error_message: "Invalid email or password") | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def delete(conn, _params) do | ||||||
|  |     conn | ||||||
|  |     |> put_flash(:info, "Logged out successfully.") | ||||||
|  |     |> UserAuth.log_out_user() | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										74
									
								
								lib/lokal_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								lib/lokal_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | defmodule LokalWeb.UserSettingsController do | ||||||
|  |   use LokalWeb, :controller | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias LokalWeb.UserAuth | ||||||
|  |  | ||||||
|  |   plug :assign_email_and_password_changesets | ||||||
|  |  | ||||||
|  |   def edit(conn, _params) do | ||||||
|  |     render(conn, "edit.html") | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def update(conn, %{"action" => "update_email"} = params) do | ||||||
|  |     %{"current_password" => password, "user" => user_params} = params | ||||||
|  |     user = conn.assigns.current_user | ||||||
|  |  | ||||||
|  |     case Accounts.apply_user_email(user, password, user_params) do | ||||||
|  |       {:ok, applied_user} -> | ||||||
|  |         Accounts.deliver_update_email_instructions( | ||||||
|  |           applied_user, | ||||||
|  |           user.email, | ||||||
|  |           &Routes.user_settings_url(conn, :confirm_email, &1) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         conn | ||||||
|  |         |> put_flash( | ||||||
|  |           :info, | ||||||
|  |           "A link to confirm your email change has been sent to the new address." | ||||||
|  |         ) | ||||||
|  |         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||||
|  |  | ||||||
|  |       {:error, changeset} -> | ||||||
|  |         render(conn, "edit.html", email_changeset: changeset) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def update(conn, %{"action" => "update_password"} = params) do | ||||||
|  |     %{"current_password" => password, "user" => user_params} = params | ||||||
|  |     user = conn.assigns.current_user | ||||||
|  |  | ||||||
|  |     case Accounts.update_user_password(user, password, user_params) do | ||||||
|  |       {:ok, user} -> | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:info, "Password updated successfully.") | ||||||
|  |         |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) | ||||||
|  |         |> UserAuth.log_in_user(user) | ||||||
|  |  | ||||||
|  |       {:error, changeset} -> | ||||||
|  |         render(conn, "edit.html", password_changeset: changeset) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def confirm_email(conn, %{"token" => token}) do | ||||||
|  |     case Accounts.update_user_email(conn.assigns.current_user, token) do | ||||||
|  |       :ok -> | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:info, "Email changed successfully.") | ||||||
|  |         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||||
|  |  | ||||||
|  |       :error -> | ||||||
|  |         conn | ||||||
|  |         |> put_flash(:error, "Email change link is invalid or it has expired.") | ||||||
|  |         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp assign_email_and_password_changesets(conn, _opts) do | ||||||
|  |     user = conn.assigns.current_user | ||||||
|  |  | ||||||
|  |     conn | ||||||
|  |     |> assign(:email_changeset, Accounts.change_user_email(user)) | ||||||
|  |     |> assign(:password_changeset, Accounts.change_user_password(user)) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										54
									
								
								lib/lokal_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/lokal_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | defmodule LokalWeb.Endpoint do | ||||||
|  |   use Phoenix.Endpoint, otp_app: :lokal | ||||||
|  |  | ||||||
|  |   # The session will be stored in the cookie and signed, | ||||||
|  |   # this means its contents can be read but not tampered with. | ||||||
|  |   # Set :encryption_salt if you would also like to encrypt it. | ||||||
|  |   @session_options [ | ||||||
|  |     store: :cookie, | ||||||
|  |     key: "_lokal_key", | ||||||
|  |     signing_salt: "fxAnJltS" | ||||||
|  |   ] | ||||||
|  |  | ||||||
|  |   socket "/socket", LokalWeb.UserSocket, | ||||||
|  |     websocket: true, | ||||||
|  |     longpoll: false | ||||||
|  |  | ||||||
|  |   socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] | ||||||
|  |  | ||||||
|  |   # Serve at "/" the static files from "priv/static" directory. | ||||||
|  |   # | ||||||
|  |   # You should set gzip to true if you are running phx.digest | ||||||
|  |   # when deploying your static files in production. | ||||||
|  |   plug Plug.Static, | ||||||
|  |     at: "/", | ||||||
|  |     from: :lokal, | ||||||
|  |     gzip: false, | ||||||
|  |     only: ~w(css fonts images js favicon.ico robots.txt) | ||||||
|  |  | ||||||
|  |   # Code reloading can be explicitly enabled under the | ||||||
|  |   # :code_reloader configuration of your endpoint. | ||||||
|  |   if code_reloading? do | ||||||
|  |     socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket | ||||||
|  |     plug Phoenix.LiveReloader | ||||||
|  |     plug Phoenix.CodeReloader | ||||||
|  |     plug Phoenix.Ecto.CheckRepoStatus, otp_app: :lokal | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   plug Phoenix.LiveDashboard.RequestLogger, | ||||||
|  |     param_key: "request_logger", | ||||||
|  |     cookie_key: "request_logger" | ||||||
|  |  | ||||||
|  |   plug Plug.RequestId | ||||||
|  |   plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] | ||||||
|  |  | ||||||
|  |   plug Plug.Parsers, | ||||||
|  |     parsers: [:urlencoded, :multipart, :json], | ||||||
|  |     pass: ["*/*"], | ||||||
|  |     json_decoder: Phoenix.json_library() | ||||||
|  |  | ||||||
|  |   plug Plug.MethodOverride | ||||||
|  |   plug Plug.Head | ||||||
|  |   plug Plug.Session, @session_options | ||||||
|  |   plug LokalWeb.Router | ||||||
|  | end | ||||||
							
								
								
									
										24
									
								
								lib/lokal_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/lokal_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | defmodule LokalWeb.Gettext do | ||||||
|  |   @moduledoc """ | ||||||
|  |   A module providing Internationalization with a gettext-based API. | ||||||
|  |  | ||||||
|  |   By using [Gettext](https://hexdocs.pm/gettext), | ||||||
|  |   your module gains a set of macros for translations, for example: | ||||||
|  |  | ||||||
|  |       import LokalWeb.Gettext | ||||||
|  |  | ||||||
|  |       # Simple translation | ||||||
|  |       gettext("Here is the string to translate") | ||||||
|  |  | ||||||
|  |       # Plural translation | ||||||
|  |       ngettext("Here is the string to translate", | ||||||
|  |                "Here are the strings to translate", | ||||||
|  |                3) | ||||||
|  |  | ||||||
|  |       # Domain-based translation | ||||||
|  |       dgettext("errors", "Here is the error message to translate") | ||||||
|  |  | ||||||
|  |   See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. | ||||||
|  |   """ | ||||||
|  |   use Gettext, otp_app: :lokal | ||||||
|  | end | ||||||
							
								
								
									
										75
									
								
								lib/lokal_web/live/component/topbar.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/lokal_web/live/component/topbar.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | defmodule LokalWeb.Live.Component.Topbar do | ||||||
|  |   use LokalWeb, :live_component | ||||||
|  |    | ||||||
|  |   def mount(socket), do: {:ok, socket |> assign(results: [])} | ||||||
|  |  | ||||||
|  |   def render(assigns) do | ||||||
|  |     ~L""" | ||||||
|  |     <header class="mb-4 px-8 py-4 w-full bg-primary-400"> | ||||||
|  |       <nav role="navigation"> | ||||||
|  |         <div class="flex flex-row justify-between items-center space-x-4"> | ||||||
|  |           <h1 class="leading-5 text-xl text-white">Lokal</h1> | ||||||
|  |          | ||||||
|  |           <ul class="flex flex-row flex-wrap justify-center items-center | ||||||
|  |             text-lg space-x-4 text-lg text-white"> | ||||||
|  |             <%# search %> | ||||||
|  |             <form phx-change="suggest" phx-submit="search"> | ||||||
|  |               <input type="text" name="q" class="input" | ||||||
|  |                 placeholder="Search" list="results" autocomplete="off"/> | ||||||
|  |               <datalist id="results"> | ||||||
|  |                 <%= for {app, _vsn} <- @results do %> | ||||||
|  |                   <option value="<%= app %>"><%= app %></option> | ||||||
|  |                 <% end %> | ||||||
|  |               </datalist> | ||||||
|  |             </form> | ||||||
|  |            | ||||||
|  |             <%# user settings %> | ||||||
|  |             <%= if assigns |> Map.has_key?(:current_user) do %> | ||||||
|  |               <li> | ||||||
|  |                 <%= @current_user.email %></li> | ||||||
|  |               <li> | ||||||
|  |                 <%= link "Settings", class: "hover:underline", | ||||||
|  |                   to: Routes.user_settings_path(LokalWeb.Endpoint, :edit) %> | ||||||
|  |               </li> | ||||||
|  |               <li> | ||||||
|  |                 <%= link "Log out", class: "hover:underline", | ||||||
|  |                   to: Routes.user_session_path(LokalWeb.Endpoint, :delete), method: :delete %> | ||||||
|  |               </li> | ||||||
|  |                | ||||||
|  |               <%= if function_exported?(Routes, :live_dashboard_path, 2) do %> | ||||||
|  |                 <li> | ||||||
|  |                   <%= link "LiveDashboard", class: "hover:underline", | ||||||
|  |                     to: Routes.live_dashboard_path(LokalWeb.Endpoint, :home) %> | ||||||
|  |                 </li> | ||||||
|  |               <% end %> | ||||||
|  |             <% else %> | ||||||
|  |               <li> | ||||||
|  |                 <%= link "Register", class: "hover:underline", | ||||||
|  |                   to: Routes.user_registration_path(LokalWeb.Endpoint, :new) %> | ||||||
|  |               </li> | ||||||
|  |               <li> | ||||||
|  |                 <%= link "Log in", class: "hover:underline", | ||||||
|  |                   to: Routes.user_session_path(LokalWeb.Endpoint, :new) %> | ||||||
|  |               </li> | ||||||
|  |             <% end %> | ||||||
|  |           </ul> | ||||||
|  |         </div> | ||||||
|  |       </nav> | ||||||
|  |        | ||||||
|  |       <%= if live_flash(@flash, :info) do %> | ||||||
|  |         <p class="alert alert-info" role="alert" | ||||||
|  |           phx-click="lv:clear-flash" phx-value-key="info"> | ||||||
|  |           <%= live_flash(@flash, :info) %> | ||||||
|  |         </p> | ||||||
|  |       <% end %> | ||||||
|  |  | ||||||
|  |       <%= if live_flash(@flash, :error) do %> | ||||||
|  |         <p class="alert alert-danger" role="alert" | ||||||
|  |           phx-click="lv:clear-flash" phx-value-key="error"> | ||||||
|  |           <%= live_flash(@flash, :error) %> | ||||||
|  |         </p> | ||||||
|  |       <% end %> | ||||||
|  |     </header> | ||||||
|  |     """ | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										39
									
								
								lib/lokal_web/live/page_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								lib/lokal_web/live/page_live.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | defmodule LokalWeb.PageLive do | ||||||
|  |   use LokalWeb, :live_view | ||||||
|  |  | ||||||
|  |   @impl true | ||||||
|  |   def mount(_params, _session, socket) do | ||||||
|  |     {:ok, assign(socket, query: "", results: %{})} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @impl true | ||||||
|  |   def handle_event("suggest", %{"q" => query}, socket) do | ||||||
|  |     {:noreply, assign(socket, results: search(query), query: query)} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @impl true | ||||||
|  |   def handle_event("search", %{"q" => query}, socket) do | ||||||
|  |     case search(query) do | ||||||
|  |       %{^query => vsn} -> | ||||||
|  |         {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")} | ||||||
|  |  | ||||||
|  |       _ -> | ||||||
|  |         {:noreply, | ||||||
|  |          socket | ||||||
|  |          |> put_flash(:error, "No dependencies found matching \"#{query}\"") | ||||||
|  |          |> assign(results: %{}, query: query)} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp search(query) do | ||||||
|  |     if not LokalWeb.Endpoint.config(:code_reloader) do | ||||||
|  |       raise "action disabled when not in development" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     for {app, desc, vsn} <- Application.started_applications(), | ||||||
|  |         app = to_string(app), | ||||||
|  |         String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"), | ||||||
|  |         into: %{}, | ||||||
|  |         do: {app, vsn} | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										5
									
								
								lib/lokal_web/live/page_live.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal_web/live/page_live.html.leex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <div class="flex flex-col justify-center items-center text-center"> | ||||||
|  |   <p> | ||||||
|  |     Welcome to Lokal! | ||||||
|  |   </p> | ||||||
|  | </div> | ||||||
							
								
								
									
										73
									
								
								lib/lokal_web/router.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/lokal_web/router.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | defmodule LokalWeb.Router do | ||||||
|  |   use LokalWeb, :router | ||||||
|  |  | ||||||
|  |   import LokalWeb.UserAuth | ||||||
|  |  | ||||||
|  |   pipeline :browser do | ||||||
|  |     plug :accepts, ["html"] | ||||||
|  |     plug :fetch_session | ||||||
|  |     plug :fetch_live_flash | ||||||
|  |     plug :put_root_layout, {LokalWeb.LayoutView, :root} | ||||||
|  |     plug :protect_from_forgery | ||||||
|  |     plug :put_secure_browser_headers | ||||||
|  |     plug :fetch_current_user | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   pipeline :api do | ||||||
|  |     plug :accepts, ["json"] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   scope "/", LokalWeb do | ||||||
|  |     pipe_through :browser | ||||||
|  |  | ||||||
|  |     live "/", PageLive, :index | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Enables LiveDashboard only for development | ||||||
|  |   # | ||||||
|  |   # If you want to use the LiveDashboard in production, you should put | ||||||
|  |   # it behind authentication and allow only admins to access it. | ||||||
|  |   # If your application does not have an admins-only section yet, | ||||||
|  |   # you can use Plug.BasicAuth to set up some basic authentication | ||||||
|  |   # as long as you are also using SSL (which you should anyway). | ||||||
|  |   if Mix.env() in [:dev, :test] do | ||||||
|  |     import Phoenix.LiveDashboard.Router | ||||||
|  |  | ||||||
|  |     scope "/" do | ||||||
|  |       pipe_through :browser | ||||||
|  |       live_dashboard "/dashboard", metrics: LokalWeb.Telemetry | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   ## Authentication routes | ||||||
|  |  | ||||||
|  |   scope "/", LokalWeb do | ||||||
|  |     pipe_through [:browser, :redirect_if_user_is_authenticated] | ||||||
|  |  | ||||||
|  |     get "/users/register", UserRegistrationController, :new | ||||||
|  |     post "/users/register", UserRegistrationController, :create | ||||||
|  |     get "/users/log_in", UserSessionController, :new | ||||||
|  |     post "/users/log_in", UserSessionController, :create | ||||||
|  |     get "/users/reset_password", UserResetPasswordController, :new | ||||||
|  |     post "/users/reset_password", UserResetPasswordController, :create | ||||||
|  |     get "/users/reset_password/:token", UserResetPasswordController, :edit | ||||||
|  |     put "/users/reset_password/:token", UserResetPasswordController, :update | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   scope "/", LokalWeb do | ||||||
|  |     pipe_through [:browser, :require_authenticated_user] | ||||||
|  |  | ||||||
|  |     get "/users/settings", UserSettingsController, :edit | ||||||
|  |     put "/users/settings", UserSettingsController, :update | ||||||
|  |     get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   scope "/", LokalWeb do | ||||||
|  |     pipe_through [:browser] | ||||||
|  |  | ||||||
|  |     delete "/users/log_out", UserSessionController, :delete | ||||||
|  |     get "/users/confirm", UserConfirmationController, :new | ||||||
|  |     post "/users/confirm", UserConfirmationController, :create | ||||||
|  |     get "/users/confirm/:token", UserConfirmationController, :confirm | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										55
									
								
								lib/lokal_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								lib/lokal_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | defmodule LokalWeb.Telemetry do | ||||||
|  |   use Supervisor | ||||||
|  |   import Telemetry.Metrics | ||||||
|  |  | ||||||
|  |   def start_link(arg) do | ||||||
|  |     Supervisor.start_link(__MODULE__, arg, name: __MODULE__) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @impl true | ||||||
|  |   def init(_arg) do | ||||||
|  |     children = [ | ||||||
|  |       # Telemetry poller will execute the given period measurements | ||||||
|  |       # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics | ||||||
|  |       {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} | ||||||
|  |       # Add reporters as children of your supervision tree. | ||||||
|  |       # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     Supervisor.init(children, strategy: :one_for_one) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def metrics do | ||||||
|  |     [ | ||||||
|  |       # Phoenix Metrics | ||||||
|  |       summary("phoenix.endpoint.stop.duration", | ||||||
|  |         unit: {:native, :millisecond} | ||||||
|  |       ), | ||||||
|  |       summary("phoenix.router_dispatch.stop.duration", | ||||||
|  |         tags: [:route], | ||||||
|  |         unit: {:native, :millisecond} | ||||||
|  |       ), | ||||||
|  |  | ||||||
|  |       # Database Metrics | ||||||
|  |       summary("lokal.repo.query.total_time", unit: {:native, :millisecond}), | ||||||
|  |       summary("lokal.repo.query.decode_time", unit: {:native, :millisecond}), | ||||||
|  |       summary("lokal.repo.query.query_time", unit: {:native, :millisecond}), | ||||||
|  |       summary("lokal.repo.query.queue_time", unit: {:native, :millisecond}), | ||||||
|  |       summary("lokal.repo.query.idle_time", unit: {:native, :millisecond}), | ||||||
|  |  | ||||||
|  |       # VM Metrics | ||||||
|  |       summary("vm.memory.total", unit: {:byte, :kilobyte}), | ||||||
|  |       summary("vm.total_run_queue_lengths.total"), | ||||||
|  |       summary("vm.total_run_queue_lengths.cpu"), | ||||||
|  |       summary("vm.total_run_queue_lengths.io") | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   defp periodic_measurements do | ||||||
|  |     [ | ||||||
|  |       # A module, function and arguments to be invoked periodically. | ||||||
|  |       # This function must call :telemetry.execute/3 and a metric must be added above. | ||||||
|  |       # {LokalWeb, :count_users, []} | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										11
									
								
								lib/lokal_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/lokal_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | <main role="main" class="container min-h-full min-w-full"> | ||||||
|  |   <p class="alert alert-info" role="alert"> | ||||||
|  |     <%= get_flash(@conn, :info) %> | ||||||
|  |   </p> | ||||||
|  |    | ||||||
|  |   <p class="alert alert-danger" role="alert"> | ||||||
|  |     <%= get_flash(@conn, :error) %> | ||||||
|  |   </p> | ||||||
|  |    | ||||||
|  |   <%= @inner_content %> | ||||||
|  | </main> | ||||||
							
								
								
									
										5
									
								
								lib/lokal_web/templates/layout/live.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal_web/templates/layout/live.html.leex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <main role="main" class="container min-w-full min-h-full"> | ||||||
|  |   <%= live_component LokalWeb.Live.Component.Topbar %> | ||||||
|  |  | ||||||
|  |   <%= @inner_content %> | ||||||
|  | </main> | ||||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/layout/root.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/layout/root.html.leex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="utf-8"/> | ||||||
|  |     <meta http-equiv="X-UA-Compatible" content="IE=edge"/> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||||||
|  |     <%= csrf_meta_tag() %> | ||||||
|  |     <%= live_title_tag assigns[:page_title] || "Lokal", suffix: "" %> | ||||||
|  |     <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> | ||||||
|  |     <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> | ||||||
|  |   </head> | ||||||
|  |   <body class="m-0 p-0 min-w-full min-h-full"> | ||||||
|  |     <%= @inner_content %> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										4
									
								
								lib/lokal_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/lokal_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <div class="flex flex-col space-y-8 text-center"> | ||||||
|  |   <h1 class="">Welcome to Lokal</h1> | ||||||
|  |   <p>Shop from your community</p> | ||||||
|  | </div> | ||||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/user_confirmation/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/user_confirmation/new.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <h1>Resend confirmation instructions</h1> | ||||||
|  |  | ||||||
|  | <%= form_for :user, Routes.user_confirmation_path(@conn, :create), fn f -> %> | ||||||
|  |   <%= label f, :email %> | ||||||
|  |   <%= email_input f, :email, required: true %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Resend confirmation instructions" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | | ||||||
|  |   <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> | ||||||
|  | </p> | ||||||
							
								
								
									
										26
									
								
								lib/lokal_web/templates/user_registration/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/templates/user_registration/new.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <h1>Register</h1> | ||||||
|  |  | ||||||
|  | <%= form_for @changeset, Routes.user_registration_path(@conn, :create), fn f -> %> | ||||||
|  |   <%= if @changeset.action do %> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <p>Oops, something went wrong! Please check the errors below.</p> | ||||||
|  |     </div> | ||||||
|  |   <% end %> | ||||||
|  |  | ||||||
|  |   <%= label f, :email %> | ||||||
|  |   <%= email_input f, :email, required: true %> | ||||||
|  |   <%= error_tag f, :email %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password %> | ||||||
|  |   <%= password_input f, :password, required: true %> | ||||||
|  |   <%= error_tag f, :password %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Register" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> | | ||||||
|  |   <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %> | ||||||
|  | </p> | ||||||
							
								
								
									
										26
									
								
								lib/lokal_web/templates/user_reset_password/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/templates/user_reset_password/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <h1>Reset password</h1> | ||||||
|  |  | ||||||
|  | <%= form_for @changeset, Routes.user_reset_password_path(@conn, :update, @token), fn f -> %> | ||||||
|  |   <%= if @changeset.action do %> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <p>Oops, something went wrong! Please check the errors below.</p> | ||||||
|  |     </div> | ||||||
|  |   <% end %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password, "New password" %> | ||||||
|  |   <%= password_input f, :password, required: true %> | ||||||
|  |   <%= error_tag f, :password %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password_confirmation, "Confirm new password" %> | ||||||
|  |   <%= password_input f, :password_confirmation, required: true %> | ||||||
|  |   <%= error_tag f, :password_confirmation %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Reset password" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | | ||||||
|  |   <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> | ||||||
|  | </p> | ||||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/user_reset_password/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/user_reset_password/new.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <h1>Forgot your password?</h1> | ||||||
|  |  | ||||||
|  | <%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %> | ||||||
|  |   <%= label f, :email %> | ||||||
|  |   <%= email_input f, :email, required: true %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Send instructions to reset password" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | | ||||||
|  |   <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> | ||||||
|  | </p> | ||||||
							
								
								
									
										27
									
								
								lib/lokal_web/templates/user_session/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/lokal_web/templates/user_session/new.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <h1>Log in</h1> | ||||||
|  |  | ||||||
|  | <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %> | ||||||
|  |   <%= if @error_message do %> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <p><%= @error_message %></p> | ||||||
|  |     </div> | ||||||
|  |   <% end %> | ||||||
|  |  | ||||||
|  |   <%= label f, :email %> | ||||||
|  |   <%= email_input f, :email, required: true %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password %> | ||||||
|  |   <%= password_input f, :password, required: true %> | ||||||
|  |  | ||||||
|  |   <%= label f, :remember_me, "Keep me logged in for 60 days" %> | ||||||
|  |   <%= checkbox f, :remember_me %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Log in" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <p> | ||||||
|  |   <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> | | ||||||
|  |   <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %> | ||||||
|  | </p> | ||||||
							
								
								
									
										53
									
								
								lib/lokal_web/templates/user_settings/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/lokal_web/templates/user_settings/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | <h1>Settings</h1> | ||||||
|  |  | ||||||
|  | <h3>Change email</h3> | ||||||
|  |  | ||||||
|  | <%= form_for @email_changeset, Routes.user_settings_path(@conn, :update), fn f -> %> | ||||||
|  |   <%= if @email_changeset.action do %> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <p>Oops, something went wrong! Please check the errors below.</p> | ||||||
|  |     </div> | ||||||
|  |   <% end %> | ||||||
|  |  | ||||||
|  |   <%= hidden_input f, :action, name: "action", value: "update_email" %> | ||||||
|  |  | ||||||
|  |   <%= label f, :email %> | ||||||
|  |   <%= email_input f, :email, required: true %> | ||||||
|  |   <%= error_tag f, :email %> | ||||||
|  |  | ||||||
|  |   <%= label f, :current_password, for: "current_password_for_email" %> | ||||||
|  |   <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %> | ||||||
|  |   <%= error_tag f, :current_password %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Change email" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
|  |  | ||||||
|  | <h3>Change password</h3> | ||||||
|  |  | ||||||
|  | <%= form_for @password_changeset, Routes.user_settings_path(@conn, :update), fn f -> %> | ||||||
|  |   <%= if @password_changeset.action do %> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <p>Oops, something went wrong! Please check the errors below.</p> | ||||||
|  |     </div> | ||||||
|  |   <% end %> | ||||||
|  |  | ||||||
|  |   <%= hidden_input f, :action, name: "action", value: "update_password" %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password, "New password" %> | ||||||
|  |   <%= password_input f, :password, required: true %> | ||||||
|  |   <%= error_tag f, :password %> | ||||||
|  |  | ||||||
|  |   <%= label f, :password_confirmation, "Confirm new password" %> | ||||||
|  |   <%= password_input f, :password_confirmation, required: true %> | ||||||
|  |   <%= error_tag f, :password_confirmation %> | ||||||
|  |  | ||||||
|  |   <%= label f, :current_password, for: "current_password_for_password" %> | ||||||
|  |   <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %> | ||||||
|  |   <%= error_tag f, :current_password %> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <%= submit "Change password" %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
							
								
								
									
										47
									
								
								lib/lokal_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/lokal_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | defmodule LokalWeb.ErrorHelpers do | ||||||
|  |   @moduledoc """ | ||||||
|  |   Conveniences for translating and building error messages. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use Phoenix.HTML | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Generates tag for inlined form input errors. | ||||||
|  |   """ | ||||||
|  |   def error_tag(form, field) do | ||||||
|  |     Enum.map(Keyword.get_values(form.errors, field), fn error -> | ||||||
|  |       content_tag(:span, translate_error(error), | ||||||
|  |         class: "invalid-feedback", | ||||||
|  |         phx_feedback_for: input_name(form, field) | ||||||
|  |       ) | ||||||
|  |     end) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Translates an error message using gettext. | ||||||
|  |   """ | ||||||
|  |   def translate_error({msg, opts}) do | ||||||
|  |     # When using gettext, we typically pass the strings we want | ||||||
|  |     # to translate as a static argument: | ||||||
|  |     # | ||||||
|  |     #     # Translate "is invalid" in the "errors" domain | ||||||
|  |     #     dgettext("errors", "is invalid") | ||||||
|  |     # | ||||||
|  |     #     # Translate the number of files with plural rules | ||||||
|  |     #     dngettext("errors", "1 file", "%{count} files", count) | ||||||
|  |     # | ||||||
|  |     # Because the error messages we show in our forms and APIs | ||||||
|  |     # are defined inside Ecto, we need to translate them dynamically. | ||||||
|  |     # This requires us to call the Gettext module passing our gettext | ||||||
|  |     # backend as first argument. | ||||||
|  |     # | ||||||
|  |     # Note we use the "errors" domain, which means translations | ||||||
|  |     # should be written to the errors.po file. The :count option is | ||||||
|  |     # set by Ecto and indicates we should also apply plural rules. | ||||||
|  |     if count = opts[:count] do | ||||||
|  |       Gettext.dngettext(LokalWeb.Gettext, "errors", msg, msg, count, opts) | ||||||
|  |     else | ||||||
|  |       Gettext.dgettext(LokalWeb.Gettext, "errors", msg, opts) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										16
									
								
								lib/lokal_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/lokal_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | defmodule LokalWeb.ErrorView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  |  | ||||||
|  |   # If you want to customize a particular status code | ||||||
|  |   # for a certain format, you may uncomment below. | ||||||
|  |   # def render("500.html", _assigns) do | ||||||
|  |   #   "Internal Server Error" | ||||||
|  |   # end | ||||||
|  |  | ||||||
|  |   # By default, Phoenix returns the status message from | ||||||
|  |   # the template name. For example, "404.html" becomes | ||||||
|  |   # "Not Found". | ||||||
|  |   def template_not_found(template, _assigns) do | ||||||
|  |     Phoenix.Controller.status_message_from_template(template) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										11
									
								
								lib/lokal_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/lokal_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | defmodule LokalWeb.LayoutView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  |    | ||||||
|  |   def get_title(conn) do | ||||||
|  |     if conn.assigns |> Map.has_key?(:title) do | ||||||
|  |       "Lokal | #{conn.assigns.title}" | ||||||
|  |     else | ||||||
|  |       "Lokal" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.PageView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.UserConfirmationView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.UserRegistrationView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.UserResetPasswordView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.UserSessionView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.UserSettingsView do | ||||||
|  |   use LokalWeb, :view | ||||||
|  | end | ||||||
							
								
								
									
										69
									
								
								mix.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								mix.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | defmodule Lokal.MixProject do | ||||||
|  |   use Mix.Project | ||||||
|  |  | ||||||
|  |   def project do | ||||||
|  |     [ | ||||||
|  |       app: :lokal, | ||||||
|  |       version: "0.1.0", | ||||||
|  |       elixir: "~> 1.7", | ||||||
|  |       elixirc_paths: elixirc_paths(Mix.env()), | ||||||
|  |       compilers: [:phoenix, :gettext] ++ Mix.compilers(), | ||||||
|  |       start_permanent: Mix.env() == :prod, | ||||||
|  |       aliases: aliases(), | ||||||
|  |       deps: deps() | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Configuration for the OTP application. | ||||||
|  |   # | ||||||
|  |   # Type `mix help compile.app` for more information. | ||||||
|  |   def application do | ||||||
|  |     [ | ||||||
|  |       mod: {Lokal.Application, []}, | ||||||
|  |       extra_applications: [:logger, :runtime_tools] | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Specifies which paths to compile per environment. | ||||||
|  |   defp elixirc_paths(:test), do: ["lib", "test/support"] | ||||||
|  |   defp elixirc_paths(_), do: ["lib"] | ||||||
|  |  | ||||||
|  |   # Specifies your project dependencies. | ||||||
|  |   # | ||||||
|  |   # Type `mix help deps` for examples and options. | ||||||
|  |   defp deps do | ||||||
|  |     [ | ||||||
|  |       {:bcrypt_elixir, "~> 2.0"}, | ||||||
|  |       {:phoenix, "~> 1.5.10"}, | ||||||
|  |       {:phoenix_ecto, "~> 4.1"}, | ||||||
|  |       {:ecto_sql, "~> 3.4"}, | ||||||
|  |       {:postgrex, ">= 0.0.0"}, | ||||||
|  |       {:phoenix_live_view, "~> 0.15.1"}, | ||||||
|  |       {:floki, ">= 0.30.0", only: :test}, | ||||||
|  |       {:phoenix_html, "~> 2.11"}, | ||||||
|  |       {:phoenix_live_reload, "~> 1.2", only: :dev}, | ||||||
|  |       {:phoenix_live_dashboard, "~> 0.4"}, | ||||||
|  |       {:telemetry_metrics, "~> 0.4"}, | ||||||
|  |       {:telemetry_poller, "~> 0.4"}, | ||||||
|  |       {:gettext, "~> 0.11"}, | ||||||
|  |       {:jason, "~> 1.0"}, | ||||||
|  |       {:plug_cowboy, "~> 2.0"}, | ||||||
|  |       {:phx_gen_auth, "~> 0.7", only: [:dev], runtime: false} | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Aliases are shortcuts or tasks specific to the current project. | ||||||
|  |   # For example, to install project dependencies and perform other setup tasks, run: | ||||||
|  |   # | ||||||
|  |   #     $ mix setup | ||||||
|  |   # | ||||||
|  |   # See the documentation for `Mix` for more info on aliases. | ||||||
|  |   defp aliases do | ||||||
|  |     [ | ||||||
|  |       setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"], | ||||||
|  |       "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], | ||||||
|  |       "ecto.reset": ["ecto.drop", "ecto.setup"], | ||||||
|  |       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] | ||||||
|  |     ] | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										35
									
								
								mix.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								mix.lock
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | %{ | ||||||
|  |   "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"}, | ||||||
|  |   "comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"}, | ||||||
|  |   "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, | ||||||
|  |   "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, | ||||||
|  |   "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, | ||||||
|  |   "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, | ||||||
|  |   "db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"}, | ||||||
|  |   "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, | ||||||
|  |   "ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"}, | ||||||
|  |   "ecto_sql": {:hex, :ecto_sql, "3.6.2", "9526b5f691701a5181427634c30655ac33d11e17e4069eff3ae1176c764e0ba3", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.6.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ec9d7e6f742ea39b63aceaea9ac1d1773d574ea40df5a53ef8afbd9242fdb6b"}, | ||||||
|  |   "elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"}, | ||||||
|  |   "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, | ||||||
|  |   "floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"}, | ||||||
|  |   "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, | ||||||
|  |   "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, | ||||||
|  |   "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, | ||||||
|  |   "mime": {:hex, :mime, "2.0.0", "dea770ba85a5a17878c81fb395bfbfda46d8a882f5e93f9566ba47207ff4d956", [:mix], [], "hexpm", "78ba962513a989de60968db1bdbe26006417f804c6a94a53c32b29e892e3f1bc"}, | ||||||
|  |   "phoenix": {:hex, :phoenix, "1.5.10", "3ee7d5c17ff9626d72d374d8fc8909bf00f4323fd15549fbe3abbbd38b5299c8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9c2eaa5a8fe5a412610c6aa84ccdb6f3e92f333d4df7fbaeb0d5a157dbfb48d"}, | ||||||
|  |   "phoenix_ecto": {:hex, :phoenix_ecto, "4.3.0", "2c69a452c2e0ee8c93345ae1cdc1696ef4877ff9cbb15c305def41960c3c4ebf", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0ac491924217550c8f42c81c1f390b5d81517d12ceaf9abf3e701156760a848e"}, | ||||||
|  |   "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, | ||||||
|  |   "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"}, | ||||||
|  |   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, | ||||||
|  |   "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.7", "09720b8e5151b3ca8ef739cd7626d4feb987c69ba0b509c9bbdb861d5a365881", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a756cf662420272d0f1b3b908cce5222163b5a95aa9bab404f9d29aff53276e"}, | ||||||
|  |   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, | ||||||
|  |   "phx_gen_auth": {:hex, :phx_gen_auth, "0.7.0", "2e10e9527b6b71abbfbb4601c7dc4aa4fb9f2db6f9a6be457c468b7f2b0f6319", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b9dc3e3b866e67c5db8f00f4a2adb28fc8636e794f78600e35aba0e55bdac209"}, | ||||||
|  |   "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, | ||||||
|  |   "plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"}, | ||||||
|  |   "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, | ||||||
|  |   "postgrex": {:hex, :postgrex, "0.15.10", "2809dee1b1d76f7cbabe570b2a9285c2e7b41be60cf792f5f2804a54b838a067", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1560ca427542f6b213f8e281633ae1a3b31cdbcd84ebd7f50628765b8f6132be"}, | ||||||
|  |   "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, | ||||||
|  |   "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, | ||||||
|  |   "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, | ||||||
|  |   "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								priv/gettext/en/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								priv/gettext/en/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | ## `msgid`s in this file come from POT (.pot) files. | ||||||
|  | ## | ||||||
|  | ## Do not add, change, or remove `msgid`s manually here as | ||||||
|  | ## they're tied to the ones in the corresponding POT file | ||||||
|  | ## (with the same domain). | ||||||
|  | ## | ||||||
|  | ## Use `mix gettext.extract --merge` or `mix gettext.merge` | ||||||
|  | ## to merge POT files into PO files. | ||||||
|  | msgid "" | ||||||
|  | msgstr "" | ||||||
|  | "Language: en\n" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.cast/4 | ||||||
|  | msgid "can't be blank" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.unique_constraint/3 | ||||||
|  | msgid "has already been taken" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.put_change/3 | ||||||
|  | msgid "is invalid" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_acceptance/3 | ||||||
|  | msgid "must be accepted" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_format/3 | ||||||
|  | msgid "has invalid format" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_subset/3 | ||||||
|  | msgid "has an invalid entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_exclusion/3 | ||||||
|  | msgid "is reserved" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_confirmation/3 | ||||||
|  | msgid "does not match confirmation" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.no_assoc_constraint/3 | ||||||
|  | msgid "is still associated with this entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "are still associated with this entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_length/3 | ||||||
|  | msgid "should be %{count} character(s)" | ||||||
|  | msgid_plural "should be %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have %{count} item(s)" | ||||||
|  | msgid_plural "should have %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should be at least %{count} character(s)" | ||||||
|  | msgid_plural "should be at least %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have at least %{count} item(s)" | ||||||
|  | msgid_plural "should have at least %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should be at most %{count} character(s)" | ||||||
|  | msgid_plural "should be at most %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have at most %{count} item(s)" | ||||||
|  | msgid_plural "should have at most %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_number/3 | ||||||
|  | msgid "must be less than %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be greater than %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be less than or equal to %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be greater than or equal to %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be equal to %{number}" | ||||||
|  | msgstr "" | ||||||
							
								
								
									
										95
									
								
								priv/gettext/errors.pot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								priv/gettext/errors.pot
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | ## This is a PO Template file. | ||||||
|  | ## | ||||||
|  | ## `msgid`s here are often extracted from source code. | ||||||
|  | ## Add new translations manually only if they're dynamic | ||||||
|  | ## translations that can't be statically extracted. | ||||||
|  | ## | ||||||
|  | ## Run `mix gettext.extract` to bring this file up to | ||||||
|  | ## date. Leave `msgstr`s empty as changing them here has no | ||||||
|  | ## effect: edit them in PO (`.po`) files instead. | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.cast/4 | ||||||
|  | msgid "can't be blank" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.unique_constraint/3 | ||||||
|  | msgid "has already been taken" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.put_change/3 | ||||||
|  | msgid "is invalid" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_acceptance/3 | ||||||
|  | msgid "must be accepted" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_format/3 | ||||||
|  | msgid "has invalid format" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_subset/3 | ||||||
|  | msgid "has an invalid entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_exclusion/3 | ||||||
|  | msgid "is reserved" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_confirmation/3 | ||||||
|  | msgid "does not match confirmation" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.no_assoc_constraint/3 | ||||||
|  | msgid "is still associated with this entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "are still associated with this entry" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_length/3 | ||||||
|  | msgid "should be %{count} character(s)" | ||||||
|  | msgid_plural "should be %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have %{count} item(s)" | ||||||
|  | msgid_plural "should have %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should be at least %{count} character(s)" | ||||||
|  | msgid_plural "should be at least %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have at least %{count} item(s)" | ||||||
|  | msgid_plural "should have at least %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should be at most %{count} character(s)" | ||||||
|  | msgid_plural "should be at most %{count} character(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | msgid "should have at most %{count} item(s)" | ||||||
|  | msgid_plural "should have at most %{count} item(s)" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  |  | ||||||
|  | ## From Ecto.Changeset.validate_number/3 | ||||||
|  | msgid "must be less than %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be greater than %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be less than or equal to %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be greater than or equal to %{number}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | msgid "must be equal to %{number}" | ||||||
|  | msgstr "" | ||||||
							
								
								
									
										4
									
								
								priv/repo/migrations/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								priv/repo/migrations/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | [ | ||||||
|  |   import_deps: [:ecto_sql], | ||||||
|  |   inputs: ["*.exs"] | ||||||
|  | ] | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | defmodule Lokal.Repo.Migrations.CreateUsersAuthTables do | ||||||
|  |   use Ecto.Migration | ||||||
|  |  | ||||||
|  |   def change do | ||||||
|  |     execute "CREATE EXTENSION IF NOT EXISTS citext", "" | ||||||
|  |  | ||||||
|  |     create table(:users, primary_key: false) do | ||||||
|  |       add :id, :binary_id, primary_key: true | ||||||
|  |       add :email, :citext, null: false | ||||||
|  |       add :hashed_password, :string, null: false | ||||||
|  |       add :confirmed_at, :naive_datetime | ||||||
|  |       timestamps() | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     create unique_index(:users, [:email]) | ||||||
|  |  | ||||||
|  |     create table(:users_tokens, primary_key: false) do | ||||||
|  |       add :id, :binary_id, primary_key: true | ||||||
|  |       add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false | ||||||
|  |       add :token, :binary, null: false | ||||||
|  |       add :context, :string, null: false | ||||||
|  |       add :sent_to, :string | ||||||
|  |       timestamps(updated_at: false) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     create index(:users_tokens, [:user_id]) | ||||||
|  |     create unique_index(:users_tokens, [:context, :token]) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										11
									
								
								priv/repo/seeds.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								priv/repo/seeds.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | # Script for populating the database. You can run it as: | ||||||
|  | # | ||||||
|  | #     mix run priv/repo/seeds.exs | ||||||
|  | # | ||||||
|  | # Inside the script, you can read and write to any of your | ||||||
|  | # repositories directly: | ||||||
|  | # | ||||||
|  | #     Lokal.Repo.insert!(%Lokal.SomeSchema{}) | ||||||
|  | # | ||||||
|  | # We recommend using the bang functions (`insert!`, `update!` | ||||||
|  | # and so on) as they will fail if something goes wrong. | ||||||
							
								
								
									
										1508
									
								
								test/lokal/accounts_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1508
									
								
								test/lokal/accounts_test.exs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										8
									
								
								test/lokal_web/controllers/page_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/lokal_web/controllers/page_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | defmodule LokalWeb.PageControllerTest do | ||||||
|  |   use LokalWeb.ConnCase | ||||||
|  |  | ||||||
|  |   test "GET /", %{conn: conn} do | ||||||
|  |     conn = get(conn, "/") | ||||||
|  |     assert html_response(conn, 200) =~ "Welcome to Phoenix!" | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										173
									
								
								test/lokal_web/controllers/user_auth_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								test/lokal_web/controllers/user_auth_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | |||||||
|  | defmodule LokalWeb.UserAuthTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias LokalWeb.UserAuth | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   @remember_me_cookie "_lokal_web_user_remember_me" | ||||||
|  |  | ||||||
|  |   setup %{conn: conn} do | ||||||
|  |     conn = | ||||||
|  |       conn | ||||||
|  |       |> Map.replace!(:secret_key_base, LokalWeb.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) == "/" | ||||||
|  |       assert Accounts.get_user_by_session_token(token) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "clears everything previously stored in the session", %{conn: conn, user: user} do | ||||||
|  |       conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user) | ||||||
|  |       refute get_session(conn, :to_be_removed) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "redirects to the configured path", %{conn: conn, user: user} do | ||||||
|  |       conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user) | ||||||
|  |       assert redirected_to(conn) == "/hello" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do | ||||||
|  |       conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"}) | ||||||
|  |       assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie] | ||||||
|  |  | ||||||
|  |       assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie] | ||||||
|  |       assert signed_token != get_session(conn, :user_token) | ||||||
|  |       assert max_age == 5_184_000 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "logout_user/1" do | ||||||
|  |     test "erases session and cookies", %{conn: conn, user: user} do | ||||||
|  |       user_token = Accounts.generate_user_session_token(user) | ||||||
|  |  | ||||||
|  |       conn = | ||||||
|  |         conn | ||||||
|  |         |> put_session(:user_token, user_token) | ||||||
|  |         |> put_req_cookie(@remember_me_cookie, user_token) | ||||||
|  |         |> fetch_cookies() | ||||||
|  |         |> UserAuth.log_out_user() | ||||||
|  |  | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       refute conn.cookies[@remember_me_cookie] | ||||||
|  |       assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie] | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       refute Accounts.get_user_by_session_token(user_token) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "broadcasts to the given live_socket_id", %{conn: conn} do | ||||||
|  |       live_socket_id = "users_sessions:abcdef-token" | ||||||
|  |       LokalWeb.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: "users_sessions:abcdef-token" | ||||||
|  |       } | ||||||
|  |     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) == "/" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "fetch_current_user/2" do | ||||||
|  |     test "authenticates user from session", %{conn: conn, user: user} do | ||||||
|  |       user_token = Accounts.generate_user_session_token(user) | ||||||
|  |       conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([]) | ||||||
|  |       assert conn.assigns.current_user.id == user.id | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "authenticates user from cookies", %{conn: conn, user: user} do | ||||||
|  |       logged_in_conn = | ||||||
|  |         conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"}) | ||||||
|  |  | ||||||
|  |       user_token = logged_in_conn.cookies[@remember_me_cookie] | ||||||
|  |       %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie] | ||||||
|  |  | ||||||
|  |       conn = | ||||||
|  |         conn | ||||||
|  |         |> put_req_cookie(@remember_me_cookie, signed_token) | ||||||
|  |         |> UserAuth.fetch_current_user([]) | ||||||
|  |  | ||||||
|  |       assert get_session(conn, :user_token) == user_token | ||||||
|  |       assert conn.assigns.current_user.id == user.id | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not authenticate if data is missing", %{conn: conn, user: user} do | ||||||
|  |       _ = Accounts.generate_user_session_token(user) | ||||||
|  |       conn = UserAuth.fetch_current_user(conn, []) | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       refute conn.assigns.current_user | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "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) == "/" | ||||||
|  |     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) == Routes.user_session_path(conn, :new) | ||||||
|  |       assert get_flash(conn, :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 | request_path: "/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 | request_path: "/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 | request_path: "/foo?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 | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | defmodule LokalWeb.UserConfirmationControllerTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias Lokal.Repo | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   setup do | ||||||
|  |     %{user: user_fixture()} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/confirm" do | ||||||
|  |     test "renders the confirmation page", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_confirmation_path(conn, :new)) | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Resend confirmation instructions</h1>" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "POST /users/confirm" do | ||||||
|  |     @tag :capture_log | ||||||
|  |     test "sends a new confirmation token", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_confirmation_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => user.email} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "If your email is in our system" | ||||||
|  |       assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not send confirmation token if User is confirmed", %{conn: conn, user: user} do | ||||||
|  |       Repo.update!(Accounts.User.confirm_changeset(user)) | ||||||
|  |  | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_confirmation_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => user.email} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "If your email is in our system" | ||||||
|  |       refute Repo.get_by(Accounts.UserToken, user_id: user.id) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not send confirmation token if email is invalid", %{conn: conn} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_confirmation_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => "unknown@example.com"} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "If your email is in our system" | ||||||
|  |       assert Repo.all(Accounts.UserToken) == [] | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/confirm/:token" do | ||||||
|  |     test "confirms the given token once", %{conn: conn, user: user} do | ||||||
|  |       token = | ||||||
|  |         extract_user_token(fn url -> | ||||||
|  |           Accounts.deliver_user_confirmation_instructions(user, url) | ||||||
|  |         end) | ||||||
|  |  | ||||||
|  |       conn = get(conn, Routes.user_confirmation_path(conn, :confirm, token)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "User confirmed successfully" | ||||||
|  |       assert Accounts.get_user!(user.id).confirmed_at | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       assert Repo.all(Accounts.UserToken) == [] | ||||||
|  |  | ||||||
|  |       # When not logged in | ||||||
|  |       conn = get(conn, Routes.user_confirmation_path(conn, :confirm, token)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired" | ||||||
|  |  | ||||||
|  |       # When logged in | ||||||
|  |       conn = | ||||||
|  |         build_conn() | ||||||
|  |         |> log_in_user(user) | ||||||
|  |         |> get(Routes.user_confirmation_path(conn, :confirm, token)) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       refute get_flash(conn, :error) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not confirm email with invalid token", %{conn: conn, user: user} do | ||||||
|  |       conn = get(conn, Routes.user_confirmation_path(conn, :confirm, "oops")) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired" | ||||||
|  |       refute Accounts.get_user!(user.id).confirmed_at | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | defmodule LokalWeb.UserRegistrationControllerTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   describe "GET /users/register" do | ||||||
|  |     test "renders registration page", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_registration_path(conn, :new)) | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Register</h1>" | ||||||
|  |       assert response =~ "Log in</a>" | ||||||
|  |       assert response =~ "Register</a>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "redirects if already logged in", %{conn: conn} do | ||||||
|  |       conn = conn |> log_in_user(user_fixture()) |> get(Routes.user_registration_path(conn, :new)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "POST /users/register" do | ||||||
|  |     @tag :capture_log | ||||||
|  |     test "creates account and logs the user in", %{conn: conn} do | ||||||
|  |       email = unique_user_email() | ||||||
|  |  | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_registration_path(conn, :create), %{ | ||||||
|  |           "user" => valid_user_attributes(email: email) | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert get_session(conn, :user_token) | ||||||
|  |       assert redirected_to(conn) =~ "/" | ||||||
|  |  | ||||||
|  |       # 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</a>" | ||||||
|  |       assert response =~ "Log out</a>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "render errors for invalid data", %{conn: conn} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_registration_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => "with spaces", "password" => "too short"} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Register</h1>" | ||||||
|  |       assert response =~ "must have the @ sign and no spaces" | ||||||
|  |       assert response =~ "should be at least 12 character" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -0,0 +1,113 @@ | |||||||
|  | defmodule LokalWeb.UserResetPasswordControllerTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   alias Lokal.Repo | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   setup do | ||||||
|  |     %{user: user_fixture()} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/reset_password" do | ||||||
|  |     test "renders the reset password page", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_reset_password_path(conn, :new)) | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Forgot your password?</h1>" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "POST /users/reset_password" do | ||||||
|  |     @tag :capture_log | ||||||
|  |     test "sends a new reset password token", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_reset_password_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => user.email} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "If your email is in our system" | ||||||
|  |       assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "reset_password" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not send reset password token if email is invalid", %{conn: conn} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_reset_password_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => "unknown@example.com"} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :info) =~ "If your email is in our system" | ||||||
|  |       assert Repo.all(Accounts.UserToken) == [] | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/reset_password/:token" do | ||||||
|  |     setup %{user: user} do | ||||||
|  |       token = | ||||||
|  |         extract_user_token(fn url -> | ||||||
|  |           Accounts.deliver_user_reset_password_instructions(user, url) | ||||||
|  |         end) | ||||||
|  |  | ||||||
|  |       %{token: token} | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "renders reset password", %{conn: conn, token: token} do | ||||||
|  |       conn = get(conn, Routes.user_reset_password_path(conn, :edit, token)) | ||||||
|  |       assert html_response(conn, 200) =~ "<h1>Reset password</h1>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not render reset password with invalid token", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_reset_password_path(conn, :edit, "oops")) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :error) =~ "Reset password link is invalid or it has expired" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "PUT /users/reset_password/:token" do | ||||||
|  |     setup %{user: user} do | ||||||
|  |       token = | ||||||
|  |         extract_user_token(fn url -> | ||||||
|  |           Accounts.deliver_user_reset_password_instructions(user, url) | ||||||
|  |         end) | ||||||
|  |  | ||||||
|  |       %{token: token} | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "resets password once", %{conn: conn, user: user, token: token} do | ||||||
|  |       conn = | ||||||
|  |         put(conn, Routes.user_reset_password_path(conn, :update, token), %{ | ||||||
|  |           "user" => %{ | ||||||
|  |             "password" => "new valid password", | ||||||
|  |             "password_confirmation" => "new valid password" | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == Routes.user_session_path(conn, :new) | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       assert get_flash(conn, :info) =~ "Password reset successfully" | ||||||
|  |       assert Accounts.get_user_by_email_and_password(user.email, "new valid password") | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not reset password on invalid data", %{conn: conn, token: token} do | ||||||
|  |       conn = | ||||||
|  |         put(conn, Routes.user_reset_password_path(conn, :update, token), %{ | ||||||
|  |           "user" => %{ | ||||||
|  |             "password" => "too short", | ||||||
|  |             "password_confirmation" => "does not match" | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Reset password</h1>" | ||||||
|  |       assert response =~ "should be at least 12 character(s)" | ||||||
|  |       assert response =~ "does not match password" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not reset password with invalid token", %{conn: conn} do | ||||||
|  |       conn = put(conn, Routes.user_reset_password_path(conn, :update, "oops")) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       assert get_flash(conn, :error) =~ "Reset password link is invalid or it has expired" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										98
									
								
								test/lokal_web/controllers/user_session_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								test/lokal_web/controllers/user_session_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | defmodule LokalWeb.UserSessionControllerTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   setup do | ||||||
|  |     %{user: user_fixture()} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/log_in" do | ||||||
|  |     test "renders log in page", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_session_path(conn, :new)) | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Log in</h1>" | ||||||
|  |       assert response =~ "Log in</a>" | ||||||
|  |       assert response =~ "Register</a>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "redirects if already logged in", %{conn: conn, user: user} do | ||||||
|  |       conn = conn |> log_in_user(user) |> get(Routes.user_session_path(conn, :new)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "POST /users/log_in" do | ||||||
|  |     test "logs the user in", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_session_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => user.email, "password" => valid_user_password()} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert get_session(conn, :user_token) | ||||||
|  |       assert redirected_to(conn) =~ "/" | ||||||
|  |  | ||||||
|  |       # Now do a logged in request and assert on the menu | ||||||
|  |       conn = get(conn, "/") | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ user.email | ||||||
|  |       assert response =~ "Settings</a>" | ||||||
|  |       assert response =~ "Log out</a>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "logs the user in with remember me", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_session_path(conn, :create), %{ | ||||||
|  |           "user" => %{ | ||||||
|  |             "email" => user.email, | ||||||
|  |             "password" => valid_user_password(), | ||||||
|  |             "remember_me" => "true" | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert conn.resp_cookies["_lokal_web_user_remember_me"] | ||||||
|  |       assert redirected_to(conn) =~ "/" | ||||||
|  |     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(Routes.user_session_path(conn, :create), %{ | ||||||
|  |           "user" => %{ | ||||||
|  |             "email" => user.email, | ||||||
|  |             "password" => valid_user_password() | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == "/foo/bar" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "emits error message with invalid credentials", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         post(conn, Routes.user_session_path(conn, :create), %{ | ||||||
|  |           "user" => %{"email" => user.email, "password" => "invalid_password"} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Log in</h1>" | ||||||
|  |       assert response =~ "Invalid email or password" | ||||||
|  |     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(Routes.user_session_path(conn, :delete)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       assert get_flash(conn, :info) =~ "Logged out successfully" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "succeeds even if the user is not logged in", %{conn: conn} do | ||||||
|  |       conn = delete(conn, Routes.user_session_path(conn, :delete)) | ||||||
|  |       assert redirected_to(conn) == "/" | ||||||
|  |       refute get_session(conn, :user_token) | ||||||
|  |       assert get_flash(conn, :info) =~ "Logged out successfully" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										129
									
								
								test/lokal_web/controllers/user_settings_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								test/lokal_web/controllers/user_settings_controller_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | defmodule LokalWeb.UserSettingsControllerTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   alias Lokal.Accounts | ||||||
|  |   import Lokal.AccountsFixtures | ||||||
|  |  | ||||||
|  |   setup :register_and_log_in_user | ||||||
|  |  | ||||||
|  |   describe "GET /users/settings" do | ||||||
|  |     test "renders settings page", %{conn: conn} do | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :edit)) | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Settings</h1>" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "redirects if user is not logged in" do | ||||||
|  |       conn = build_conn() | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :edit)) | ||||||
|  |       assert redirected_to(conn) == Routes.user_session_path(conn, :new) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "PUT /users/settings (change password form)" do | ||||||
|  |     test "updates the user password and resets tokens", %{conn: conn, user: user} do | ||||||
|  |       new_password_conn = | ||||||
|  |         put(conn, Routes.user_settings_path(conn, :update), %{ | ||||||
|  |           "action" => "update_password", | ||||||
|  |           "current_password" => valid_user_password(), | ||||||
|  |           "user" => %{ | ||||||
|  |             "password" => "new valid password", | ||||||
|  |             "password_confirmation" => "new valid password" | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(new_password_conn) == Routes.user_settings_path(conn, :edit) | ||||||
|  |       assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token) | ||||||
|  |       assert get_flash(new_password_conn, :info) =~ "Password updated successfully" | ||||||
|  |       assert Accounts.get_user_by_email_and_password(user.email, "new valid password") | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not update password on invalid data", %{conn: conn} do | ||||||
|  |       old_password_conn = | ||||||
|  |         put(conn, Routes.user_settings_path(conn, :update), %{ | ||||||
|  |           "action" => "update_password", | ||||||
|  |           "current_password" => "invalid", | ||||||
|  |           "user" => %{ | ||||||
|  |             "password" => "too short", | ||||||
|  |             "password_confirmation" => "does not match" | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       response = html_response(old_password_conn, 200) | ||||||
|  |       assert response =~ "<h1>Settings</h1>" | ||||||
|  |       assert response =~ "should be at least 12 character(s)" | ||||||
|  |       assert response =~ "does not match password" | ||||||
|  |       assert response =~ "is not valid" | ||||||
|  |  | ||||||
|  |       assert get_session(old_password_conn, :user_token) == get_session(conn, :user_token) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "PUT /users/settings (change email form)" do | ||||||
|  |     @tag :capture_log | ||||||
|  |     test "updates the user email", %{conn: conn, user: user} do | ||||||
|  |       conn = | ||||||
|  |         put(conn, Routes.user_settings_path(conn, :update), %{ | ||||||
|  |           "action" => "update_email", | ||||||
|  |           "current_password" => valid_user_password(), | ||||||
|  |           "user" => %{"email" => unique_user_email()} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       assert redirected_to(conn) == Routes.user_settings_path(conn, :edit) | ||||||
|  |       assert get_flash(conn, :info) =~ "A link to confirm your email" | ||||||
|  |       assert Accounts.get_user_by_email(user.email) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not update email on invalid data", %{conn: conn} do | ||||||
|  |       conn = | ||||||
|  |         put(conn, Routes.user_settings_path(conn, :update), %{ | ||||||
|  |           "action" => "update_email", | ||||||
|  |           "current_password" => "invalid", | ||||||
|  |           "user" => %{"email" => "with spaces"} | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |       response = html_response(conn, 200) | ||||||
|  |       assert response =~ "<h1>Settings</h1>" | ||||||
|  |       assert response =~ "must have the @ sign and no spaces" | ||||||
|  |       assert response =~ "is not valid" | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   describe "GET /users/settings/confirm_email/:token" do | ||||||
|  |     setup %{user: user} do | ||||||
|  |       email = unique_user_email() | ||||||
|  |  | ||||||
|  |       token = | ||||||
|  |         extract_user_token(fn url -> | ||||||
|  |           Accounts.deliver_update_email_instructions(%{user | email: email}, user.email, url) | ||||||
|  |         end) | ||||||
|  |  | ||||||
|  |       %{token: token, email: email} | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token)) | ||||||
|  |       assert redirected_to(conn) == Routes.user_settings_path(conn, :edit) | ||||||
|  |       assert get_flash(conn, :info) =~ "Email changed successfully" | ||||||
|  |       refute Accounts.get_user_by_email(user.email) | ||||||
|  |       assert Accounts.get_user_by_email(email) | ||||||
|  |  | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token)) | ||||||
|  |       assert redirected_to(conn) == Routes.user_settings_path(conn, :edit) | ||||||
|  |       assert get_flash(conn, :error) =~ "Email change link is invalid or it has expired" | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "does not update email with invalid token", %{conn: conn, user: user} do | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :confirm_email, "oops")) | ||||||
|  |       assert redirected_to(conn) == Routes.user_settings_path(conn, :edit) | ||||||
|  |       assert get_flash(conn, :error) =~ "Email change link is invalid or it has expired" | ||||||
|  |       assert Accounts.get_user_by_email(user.email) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     test "redirects if user is not logged in", %{token: token} do | ||||||
|  |       conn = build_conn() | ||||||
|  |       conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token)) | ||||||
|  |       assert redirected_to(conn) == Routes.user_session_path(conn, :new) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										11
									
								
								test/lokal_web/live/page_live_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test/lokal_web/live/page_live_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | defmodule LokalWeb.PageLiveTest do | ||||||
|  |   use LokalWeb.ConnCase | ||||||
|  |  | ||||||
|  |   import Phoenix.LiveViewTest | ||||||
|  |  | ||||||
|  |   test "disconnected and connected render", %{conn: conn} do | ||||||
|  |     {:ok, page_live, disconnected_html} = live(conn, "/") | ||||||
|  |     assert disconnected_html =~ "Welcome to Phoenix!" | ||||||
|  |     assert render(page_live) =~ "Welcome to Phoenix!" | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										14
									
								
								test/lokal_web/views/error_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/lokal_web/views/error_view_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | defmodule LokalWeb.ErrorViewTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   # Bring render/3 and render_to_string/3 for testing custom views | ||||||
|  |   import Phoenix.View | ||||||
|  |  | ||||||
|  |   test "renders 404.html" do | ||||||
|  |     assert render_to_string(LokalWeb.ErrorView, "404.html", []) == "Not Found" | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   test "renders 500.html" do | ||||||
|  |     assert render_to_string(LokalWeb.ErrorView, "500.html", []) == "Internal Server Error" | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										8
									
								
								test/lokal_web/views/layout_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/lokal_web/views/layout_view_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | defmodule LokalWeb.LayoutViewTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  |  | ||||||
|  |   # When testing helpers, you may want to import Phoenix.HTML and | ||||||
|  |   # use functions such as safe_to_string() to convert the helper | ||||||
|  |   # result into an HTML string. | ||||||
|  |   # import Phoenix.HTML | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								test/lokal_web/views/page_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/lokal_web/views/page_view_test.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | defmodule LokalWeb.PageViewTest do | ||||||
|  |   use LokalWeb.ConnCase, async: true | ||||||
|  | end | ||||||
							
								
								
									
										40
									
								
								test/support/channel_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								test/support/channel_case.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | defmodule LokalWeb.ChannelCase do | ||||||
|  |   @moduledoc """ | ||||||
|  |   This module defines the test case to be used by | ||||||
|  |   channel tests. | ||||||
|  |  | ||||||
|  |   Such tests rely on `Phoenix.ChannelTest` and also | ||||||
|  |   import other functionality to make it easier | ||||||
|  |   to build common data structures and query the data layer. | ||||||
|  |  | ||||||
|  |   Finally, if the test case interacts with the database, | ||||||
|  |   we enable the SQL sandbox, so changes done to the database | ||||||
|  |   are reverted at the end of every test. If you are using | ||||||
|  |   PostgreSQL, you can even run database tests asynchronously | ||||||
|  |   by setting `use LokalWeb.ChannelCase, async: true`, although | ||||||
|  |   this option is not recommended for other databases. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use ExUnit.CaseTemplate | ||||||
|  |  | ||||||
|  |   using do | ||||||
|  |     quote do | ||||||
|  |       # Import conveniences for testing with channels | ||||||
|  |       import Phoenix.ChannelTest | ||||||
|  |       import LokalWeb.ChannelCase | ||||||
|  |  | ||||||
|  |       # The default endpoint for testing | ||||||
|  |       @endpoint LokalWeb.Endpoint | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   setup tags do | ||||||
|  |     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Lokal.Repo) | ||||||
|  |  | ||||||
|  |     unless tags[:async] do | ||||||
|  |       Ecto.Adapters.SQL.Sandbox.mode(Lokal.Repo, {:shared, self()}) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     :ok | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										69
									
								
								test/support/conn_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								test/support/conn_case.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | defmodule LokalWeb.ConnCase do | ||||||
|  |   @moduledoc """ | ||||||
|  |   This module defines the test case to be used by | ||||||
|  |   tests that require setting up a connection. | ||||||
|  |  | ||||||
|  |   Such tests rely on `Phoenix.ConnTest` and also | ||||||
|  |   import other functionality to make it easier | ||||||
|  |   to build common data structures and query the data layer. | ||||||
|  |  | ||||||
|  |   Finally, if the test case interacts with the database, | ||||||
|  |   we enable the SQL sandbox, so changes done to the database | ||||||
|  |   are reverted at the end of every test. If you are using | ||||||
|  |   PostgreSQL, you can even run database tests asynchronously | ||||||
|  |   by setting `use LokalWeb.ConnCase, async: true`, although | ||||||
|  |   this option is not recommended for other databases. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use ExUnit.CaseTemplate | ||||||
|  |  | ||||||
|  |   using do | ||||||
|  |     quote do | ||||||
|  |       # Import conveniences for testing with connections | ||||||
|  |       import Plug.Conn | ||||||
|  |       import Phoenix.ConnTest | ||||||
|  |       import LokalWeb.ConnCase | ||||||
|  |  | ||||||
|  |       alias LokalWeb.Router.Helpers, as: Routes | ||||||
|  |  | ||||||
|  |       # The default endpoint for testing | ||||||
|  |       @endpoint LokalWeb.Endpoint | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   setup tags do | ||||||
|  |     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Lokal.Repo) | ||||||
|  |  | ||||||
|  |     unless tags[:async] do | ||||||
|  |       Ecto.Adapters.SQL.Sandbox.mode(Lokal.Repo, {:shared, self()}) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     {:ok, conn: Phoenix.ConnTest.build_conn()} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Setup helper that registers and logs in users. | ||||||
|  |  | ||||||
|  |       setup :register_and_log_in_user | ||||||
|  |  | ||||||
|  |   It stores an updated connection and a registered user in the | ||||||
|  |   test context. | ||||||
|  |   """ | ||||||
|  |   def register_and_log_in_user(%{conn: conn}) do | ||||||
|  |     user = Lokal.AccountsFixtures.user_fixture() | ||||||
|  |     %{conn: log_in_user(conn, user), user: user} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   Logs the given `user` into the `conn`. | ||||||
|  |  | ||||||
|  |   It returns an updated `conn`. | ||||||
|  |   """ | ||||||
|  |   def log_in_user(conn, user) do | ||||||
|  |     token = Lokal.Accounts.generate_user_session_token(user) | ||||||
|  |  | ||||||
|  |     conn | ||||||
|  |     |> Phoenix.ConnTest.init_test_session(%{}) | ||||||
|  |     |> Plug.Conn.put_session(:user_token, token) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										55
									
								
								test/support/data_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								test/support/data_case.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | defmodule Lokal.DataCase do | ||||||
|  |   @moduledoc """ | ||||||
|  |   This module defines the setup for tests requiring | ||||||
|  |   access to the application's data layer. | ||||||
|  |  | ||||||
|  |   You may define functions here to be used as helpers in | ||||||
|  |   your tests. | ||||||
|  |  | ||||||
|  |   Finally, if the test case interacts with the database, | ||||||
|  |   we enable the SQL sandbox, so changes done to the database | ||||||
|  |   are reverted at the end of every test. If you are using | ||||||
|  |   PostgreSQL, you can even run database tests asynchronously | ||||||
|  |   by setting `use Lokal.DataCase, async: true`, although | ||||||
|  |   this option is not recommended for other databases. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   use ExUnit.CaseTemplate | ||||||
|  |  | ||||||
|  |   using do | ||||||
|  |     quote do | ||||||
|  |       alias Lokal.Repo | ||||||
|  |  | ||||||
|  |       import Ecto | ||||||
|  |       import Ecto.Changeset | ||||||
|  |       import Ecto.Query | ||||||
|  |       import Lokal.DataCase | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   setup tags do | ||||||
|  |     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Lokal.Repo) | ||||||
|  |  | ||||||
|  |     unless tags[:async] do | ||||||
|  |       Ecto.Adapters.SQL.Sandbox.mode(Lokal.Repo, {:shared, self()}) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   @doc """ | ||||||
|  |   A helper that transforms changeset errors into a map of messages. | ||||||
|  |  | ||||||
|  |       assert {:error, changeset} = Accounts.create_user(%{password: "short"}) | ||||||
|  |       assert "password is too short" in errors_on(changeset).password | ||||||
|  |       assert %{password: ["password is too short"]} = errors_on(changeset) | ||||||
|  |  | ||||||
|  |   """ | ||||||
|  |   def errors_on(changeset) do | ||||||
|  |     Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> | ||||||
|  |       Regex.replace(~r"%{(\w+)}", message, fn _, key -> | ||||||
|  |         opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() | ||||||
|  |       end) | ||||||
|  |     end) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										52
									
								
								test/support/fixtures/accounts_fixtures.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								test/support/fixtures/accounts_fixtures.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | defmodule Lokal.AccountsFixtures do | ||||||
|  |   @moduledoc """ | ||||||
|  |   This module defines test helpers for creating | ||||||
|  |   entities via the `Lokal.Accounts` context. | ||||||
|  |   """ | ||||||
|  |  | ||||||
|  |   def unique_user_email, do: "user#{System.unique_integer()}@example.com" | ||||||
|  |   def valid_user_password, do: "hello world!" | ||||||
|  |  | ||||||
|  |   def user_fixture(attrs \\ %{}) do | ||||||
|  |     {:ok, user} = | ||||||
|  |       attrs | ||||||
|  |       |> Enum.into(%{ | ||||||
|  |         email: unique_user_email(), | ||||||
|  |         password: valid_user_password() | ||||||
|  |       }) | ||||||
|  |       |> Lokal.Accounts.register_user() | ||||||
|  |  | ||||||
|  |     user | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def extract_user_token(fun) do | ||||||
|  |     {:ok, captured} = fun.(&"[TOKEN]#{&1}[TOKEN]") | ||||||
|  |     [_, token, _] = String.split(captured.body, "[TOKEN]") | ||||||
|  |     token | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   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() | ||||||
|  |       |> Lokal.Accounts.register_user() | ||||||
|  |  | ||||||
|  |     user | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def extract_user_token(fun) do | ||||||
|  |     {:ok, captured} = fun.(&"[TOKEN]#{&1}[TOKEN]") | ||||||
|  |     [_, token, _] = String.split(captured.body, "[TOKEN]") | ||||||
|  |     token | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										2
									
								
								test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | ExUnit.start() | ||||||
|  | Ecto.Adapters.SQL.Sandbox.mode(Lokal.Repo, :manual) | ||||||
		Reference in New Issue
	
	Block a user