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